# # Chronos' OS helpers # # (c) Copyright 2022-Present Status Research & Development GmbH # # Licensed under either of # Apache License, version 2.0, (LICENSE-APACHEv2) # MIT license (LICENSE-MIT) import results import "."/[osdefs, oserrno] export results {.push raises: [].} when defined(windows) or defined(nimdoc): import stew/base10 const PipeHeaderName* = r"\\.\pipe\LOCAL\chronos\" SignalPrefixName* = cstring(r"Local\chronos-events-") MaxSignalEventLength* = 64 MaxSignalSuffixLength* = MaxSignalEventLength - (len(SignalPrefixName) + Base10.maxLen(uint64) + 2) type DescriptorFlag* {.pure.} = enum CloseOnExec, NonBlock const AsyncDescriptorDefault* = { DescriptorFlag.CloseOnExec, DescriptorFlag.NonBlock} when defined(windows): type WINDESCRIPTOR* = SocketHandle|HANDLE template handleEintr*(body: untyped): untyped = discard proc setDescriptorInheritance*(s: WINDESCRIPTOR, value: bool): Result[void, OSErrorCode] = var flags = 0'u32 let fd = when s is SocketHandle: HANDLE(s) else: s if getHandleInformation(fd, flags) == FALSE: return err(osLastError()) if value != ((flags and HANDLE_FLAG_INHERIT) == HANDLE_FLAG_INHERIT): let mode = if value: HANDLE_FLAG_INHERIT else: 0'u32 if setHandleInformation(fd, HANDLE_FLAG_INHERIT, mode) == FALSE: return err(osLastError()) ok() proc getDescriptorInheritance*(s: WINDESCRIPTOR ): Result[bool, OSErrorCode] = var flags = 0'u32 let fd = when s is SocketHandle: HANDLE(s) else: s if getHandleInformation(fd, flags) == FALSE: return err(osLastError()) ok((flags and HANDLE_FLAG_INHERIT) == HANDLE_FLAG_INHERIT) proc setDescriptorBlocking*(s: SocketHandle, value: bool): Result[void, OSErrorCode] = var mode = clong(ord(not value)) if ioctlsocket(s, osdefs.FIONBIO, addr(mode)) == -1: return err(osLastError()) ok() proc setDescriptorFlags*(s: WINDESCRIPTOR, nonblock, cloexec: bool): Result[void, OSErrorCode] = ? setDescriptorBlocking(s, not(nonblock)) ? setDescriptorInheritance(s, not(cloexec)) ok() proc closeFd*(s: SocketHandle): int = int(osdefs.closesocket(s)) proc closeFd*(s: HANDLE): int = if osdefs.closeHandle(s) == TRUE: 0 else: -1 proc toWideBuffer*(s: openArray[char], d: var openArray[WCHAR]): Result[int, OSErrorCode] = if len(s) == 0: return ok(0) let res = multiByteToWideChar(CP_UTF8, 0'u32, unsafeAddr s[0], cint(-1), addr d[0], cint(len(d))) if res == 0: err(osLastError()) else: ok(res) proc toMultibyteBuffer*(s: openArray[WCHAR], d: var openArray[char]): Result[int, OSErrorCode] = if len(s) == 0: return ok(0) let res = wideCharToMultiByte(CP_UTF8, 0'u32, unsafeAddr s[0], cint(-1), addr d[0], cint(len(d)), nil, nil) if res == 0: err(osLastError()) else: ok(res) proc toWideString*(s: string): Result[LPWSTR, OSErrorCode] = if len(s) == 0: ok(cast[LPWSTR](alloc0(sizeof(WCHAR)))) else: let charsNeeded = multiByteToWideChar(CP_UTF8, 0'u32, cast[ptr char](unsafeAddr s[0]), cint(len(s)), nil, cint(0)) if charsNeeded <= cint(0): return err(osLastError()) var buffer = cast[LPWSTR](alloc0((charsNeeded + 1) * sizeof(WCHAR))) let res = multiByteToWideChar(CP_UTF8, 0'u32, cast[ptr char](unsafeAddr s[0]), cint(len(s)), buffer, charsNeeded) if res != charsNeeded: err(osLastError()) else: ok(buffer) proc toString*(w: LPWSTR): Result[string, OSErrorCode] = if isNil(w): ok("") else: let bytesNeeded = wideCharToMultiByte(CP_UTF8, 0'u32, w, cint(-1), nil, cint(0), nil, nil) if bytesNeeded <= cint(0): return err(osLastError()) var buffer = newString(bytesNeeded) let res = wideCharToMultiByte(CP_UTF8, 0'u32, w, cint(-1), addr buffer[0], cint(len(buffer)), nil, nil) if res != bytesNeeded: err(osLastError()) else: # We need to strip trailing `\x00`. for i in countdown(len(buffer) - 1, 0): if buffer[i] != '\x00': buffer.setLen(i + 1) break ok(buffer) proc free*(w: LPWSTR) = if not(isNil(w)): dealloc(cast[pointer](w)) proc createOsPipe*(readset, writeset: set[DescriptorFlag] ): Result[tuple[read: HANDLE, write: HANDLE], OSErrorCode] = var pipeIn, pipeOut: HANDLE widePipeName: LPWSTR uniq = 0'u64 rsa = getSecurityAttributes(DescriptorFlag.CloseOnExec notin readset) wsa = getSecurityAttributes(DescriptorFlag.CloseOnExec notin writeset) while true: queryPerformanceCounter(uniq) let pipeName = PipeHeaderName & Base10.toString(uniq) let openMode = if DescriptorFlag.NonBlock in readset: osdefs.FILE_FLAG_FIRST_PIPE_INSTANCE or osdefs.FILE_FLAG_OVERLAPPED or osdefs.PIPE_ACCESS_INBOUND else: osdefs.FILE_FLAG_FIRST_PIPE_INSTANCE or osdefs.PIPE_ACCESS_INBOUND let pipeMode = osdefs.PIPE_TYPE_BYTE or osdefs.PIPE_READMODE_BYTE or osdefs.PIPE_WAIT widePipeName = block: let res = pipeName.toWideString() if res.isErr(): return err(res.error()) res.get() pipeIn = createNamedPipe(widePipeName, openMode, pipeMode, 1'u32, osdefs.DEFAULT_PIPE_SIZE, osdefs.DEFAULT_PIPE_SIZE, 0'u32, addr rsa) if pipeIn == osdefs.INVALID_HANDLE_VALUE: let errorCode = osLastError() free(widePipeName) # If error in {ERROR_ACCESS_DENIED, ERROR_PIPE_BUSY}, then named pipe # with such name already exists. if (errorCode == osdefs.ERROR_ACCESS_DENIED) or (errorCode == osdefs.ERROR_PIPE_BUSY): continue return err(errorCode) else: break let openMode = osdefs.GENERIC_WRITE or osdefs.FILE_WRITE_DATA or osdefs.SYNCHRONIZE let openFlags = if DescriptorFlag.NonBlock in writeset: osdefs.FILE_FLAG_OVERLAPPED else: DWORD(0) pipeOut = createFile(widePipeName, openMode, 0, addr wsa, osdefs.OPEN_EXISTING, openFlags, HANDLE(0)) if pipeOut == osdefs.INVALID_HANDLE_VALUE: let errorCode = osLastError() free(widePipeName) discard closeFd(pipeIn) return err(errorCode) var ovl = osdefs.OVERLAPPED() let res = if DescriptorFlag.NonBlock in writeset: connectNamedPipe(pipeIn, addr ovl) else: connectNamedPipe(pipeIn, nil) if res == 0: let cleanupFlag = block: let errorCode = osLastError() case errorCode of ERROR_PIPE_CONNECTED: false of ERROR_IO_PENDING: if DescriptorFlag.NonBlock in writeset: var bytesRead = 0.DWORD if getOverlappedResult(pipeIn, addr ovl, bytesRead, 1) == FALSE: true else: false else: true else: true if cleanupFlag: let errorCode = osLastError() free(widePipeName) discard closeFd(pipeIn) discard closeFd(pipeOut) return err(errorCode) ok((read: pipeIn, write: pipeOut)) proc getSignalName*(signal: int): cstring = ## Convert Windows SIGNAL identifier to string representation. ## ## This procedure supports only SIGINT, SIGTERM and SIGQUIT values. case signal of SIGINT: cstring("sigint") of SIGTERM: cstring("sigterm") of SIGQUIT: cstring("sigquit") else: raiseAssert "Signal is not supported" proc getEventPath*(suffix: cstring): array[MaxSignalEventLength, WCHAR] = ## Create Windows' Event object name suffixed by ``suffix``. This name ## is create in local session namespace with name like this: ## ``Local\chronos-events--``. ## ## This procedure is GC-free, so it could be used in other threads. doAssert(len(suffix) < MaxSignalSuffixLength) var resMc: array[MaxSignalEventLength, char] resWc: array[MaxSignalEventLength, WCHAR] var offset = 0 let pid = osdefs.getCurrentProcessId() pid10 = Base10.toBytes(uint64(pid)) copyMem(addr resMc[offset], SignalPrefixName, len(SignalPrefixName)) offset += len(SignalPrefixName) copyMem(addr resMc[offset], unsafeAddr pid10.data[0], pid10.len) offset += pid10.len resMc[offset] = '-' offset += 1 copyMem(addr resMc[offset], suffix, len(suffix)) offset += len(suffix) resMc[offset] = '\x00' let res = toWideBuffer(resMc, resWc) if res.isErr(): raiseAssert "Invalid suffix value, got " & osErrorMsg(res.error()) resWc proc raiseEvent(suffix: cstring): Result[bool, OSErrorCode] = var sa = getSecurityAttributes() let eventName = getEventPath(suffix) # We going to fire event, so we can try to create it already signaled. event = createEvent(addr sa, FALSE, TRUE, unsafeAddr eventName[0]) errorCode = osLastError() if event == HANDLE(0): err(errorCode) else: if errorCode == ERROR_ALREADY_EXISTS: let res = setEvent(event) if res == FALSE: err(osLastError()) else: ok(true) else: ok(false) proc raiseSignal*(signal: cint): Result[bool, OSErrorCode] = ## This is helper procedure which could help to raise Unix signals in ## Windows GUI / Service application. Console applications are handled ## automatically. ## ## This procedure does not use Nim's GC, so it can be placed in any handler ## of your application even in code which is running in different thread. raiseEvent(getSignalName(signal)) proc raiseConsoleCtrlSignal*(groupId = 0'u32): Result[void, OSErrorCode] = ## Raise CTRL+C event in current console. if generateConsoleCtrlEvent(CTRL_C_EVENT, groupId) == FALSE: err(osLastError()) else: ok() proc openEvent*(suffix: string): Result[HANDLE, OSErrorCode] = ## Open or create Windows event object with suffix ``suffix``. var sa = getSecurityAttributes() let # We going to wait for created event, so we don't need to create it in # signaled state. eventName = getEventPath(suffix) event = createEvent(addr sa, FALSE, FALSE, unsafeAddr eventName[0]) if event == HANDLE(0): let errorCode = osLastError() err(errorCode) else: ok(event) else: template handleEintr*(body: untyped): untyped = var res = 0 while true: res = body if not((res == -1) and (osLastError() == oserrno.EINTR)): break res proc setDescriptorBlocking*(s: cint, value: bool): Result[void, OSErrorCode] = let flags = handleEintr(osdefs.fcntl(s, osdefs.F_GETFL)) if flags == -1: return err(osLastError()) if value != not((flags and osdefs.O_NONBLOCK) == osdefs.O_NONBLOCK): let mode = if value: flags and not(osdefs.O_NONBLOCK) else: flags or osdefs.O_NONBLOCK if handleEintr(osdefs.fcntl(s, osdefs.F_SETFL, mode)) == -1: return err(osLastError()) ok() proc setDescriptorBlocking*(s: SocketHandle, value: bool): Result[void, OSErrorCode] = setDescriptorBlocking(cint(s), value) proc setDescriptorInheritance*(s: cint, value: bool): Result[void, OSErrorCode] = let flags = handleEintr(osdefs.fcntl(s, osdefs.F_GETFD)) if flags == -1: return err(osLastError()) if value != not((flags and osdefs.FD_CLOEXEC) == osdefs.FD_CLOEXEC): let mode = if value: flags and not(osdefs.FD_CLOEXEC) else: flags or osdefs.FD_CLOEXEC if handleEintr(osdefs.fcntl(s, osdefs.F_SETFD, mode)) == -1: return err(osLastError()) ok() proc getDescriptorInheritance*(s: cint): Result[bool, OSErrorCode] = let flags = handleEintr(osdefs.fcntl(s, osdefs.F_GETFD)) if flags == -1: return err(osLastError()) ok((flags and osdefs.FD_CLOEXEC) == osdefs.FD_CLOEXEC) proc setDescriptorFlags*(s: cint, nonblock, cloexec: bool): Result[void, OSErrorCode] = ? setDescriptorBlocking(s, not(nonblock)) ? setDescriptorInheritance(s, not(cloexec)) ok() proc closeFd*(s: cint): int = handleEintr(osdefs.close(s)) proc closeFd*(s: SocketHandle): int = handleEintr(osdefs.close(cint(s))) proc acceptConn*(a1: cint, a2: ptr SockAddr, a3: ptr SockLen, a4: set[DescriptorFlag]): Result[cint, OSErrorCode] = when declared(accept4): let flags = block: var res: cint = 0 if DescriptorFlag.CloseOnExec in a4: res = res or osdefs.SOCK_CLOEXEC if DescriptorFlag.NonBlock in a4: res = res or osdefs.SOCK_NONBLOCK res let res = cint(handleEintr(accept4(a1, a2, a3, flags))) if res == -1: return err(osLastError()) ok(res) else: let sock = cint(handleEintr(cint(accept(SocketHandle(a1), a2, a3)))) if sock == -1: return err(osLastError()) let cloexec = DescriptorFlag.CloseOnExec in a4 nonblock = DescriptorFlag.NonBlock in a4 let res = setDescriptorFlags(sock, nonblock, cloexec) if res.isErr(): discard closeFd(sock) return err(res.error()) ok(sock) proc createOsPipe*(readset, writeset: set[DescriptorFlag] ): Result[tuple[read: cint, write: cint], OSErrorCode] = when declared(pipe2): var fds: array[2, cint] let readFlags = block: var res = cint(0) if DescriptorFlag.CloseOnExec in readset: res = res or osdefs.O_CLOEXEC if DescriptorFlag.NonBlock in readset: res = res or osdefs.O_NONBLOCK res if osdefs.pipe2(fds, readFlags) == -1: return err(osLastError()) if readset != writeset: let res = setDescriptorFlags(fds[1], DescriptorFlag.NonBlock in writeset, DescriptorFlag.CloseOnExec in writeset) if res.isErr(): discard closeFd(fds[0]) discard closeFd(fds[1]) return err(res.error()) ok((read: fds[0], write: fds[1])) else: var fds: array[2, cint] if osdefs.pipe(fds) == -1: return err(osLastError()) block: let res = setDescriptorFlags(fds[0], DescriptorFlag.NonBlock in readset, DescriptorFlag.CloseOnExec in readset) if res.isErr(): discard closeFd(fds[0]) discard closeFd(fds[1]) return err(res.error()) block: let res = setDescriptorFlags(fds[1], DescriptorFlag.NonBlock in writeset, DescriptorFlag.CloseOnExec in writeset) if res.isErr(): discard closeFd(fds[0]) discard closeFd(fds[1]) return err(res.error()) ok((read: fds[0], write: fds[1]))