# # # Nim's Runtime Library # (c) Copyright 2015 Andreas Rumpf # # See the file "copying.txt", included in this # distribution, for details about the copyright. # ## This module implements an advanced facility for executing OS processes ## and process communication. ## ## **See also:** ## * `os module `_ ## * `streams module `_ ## * `memfiles module `_ include "system/inclrtl" import strutils, os, strtabs, streams, cpuinfo, streamwrapper, std/private/since export quoteShell, quoteShellWindows, quoteShellPosix when defined(windows): import winlean else: import posix when defined(linux) and defined(useClone): import linux when defined(nimPreviewSlimSystem): import std/[syncio, assertions] when defined(windows): import std/widestrs type ProcessOption* = enum ## Options that can be passed to `startProcess proc ## <#startProcess,string,string,openArray[string],StringTableRef,set[ProcessOption]>`_. poEchoCmd, ## Echo the command before execution. poUsePath, ## Asks system to search for executable using PATH environment ## variable. ## On Windows, this is the default. poEvalCommand, ## Pass `command` directly to the shell, without quoting. ## Use it only if `command` comes from trusted source. poStdErrToStdOut, ## Merge stdout and stderr to the stdout stream. poParentStreams, ## Use the parent's streams. poInteractive, ## Optimize the buffer handling for responsiveness for ## UI applications. Currently this only affects ## Windows: Named pipes are used so that you can peek ## at the process' output streams. poDaemon ## Windows: The program creates no Window. ## Unix: Start the program as a daemon. This is still ## work in progress! ProcessObj = object of RootObj when defined(windows): fProcessHandle: Handle fThreadHandle: Handle inHandle, outHandle, errHandle: FileHandle id: Handle else: inHandle, outHandle, errHandle: FileHandle id: Pid inStream, outStream, errStream: owned(Stream) exitStatus: cint exitFlag: bool options: set[ProcessOption] Process* = ref ProcessObj ## Represents an operating system process. proc execProcess*(command: string, workingDir: string = "", args: openArray[string] = [], env: StringTableRef = nil, options: set[ProcessOption] = {poStdErrToStdOut, poUsePath, poEvalCommand}): string {.rtl, extern: "nosp$1", tags: [ExecIOEffect, ReadIOEffect, RootEffect].} ## A convenience procedure that executes ``command`` with ``startProcess`` ## and returns its output as a string. ## ## .. warning:: This function uses `poEvalCommand` by default for backwards ## compatibility. Make sure to pass options explicitly. ## ## See also: ## * `startProcess proc ## <#startProcess,string,string,openArray[string],StringTableRef,set[ProcessOption]>`_ ## * `execProcesses proc <#execProcesses,openArray[string],proc(int),proc(int,Process)>`_ ## * `execCmd proc <#execCmd,string>`_ ## ## Example: ## ## .. code-block:: Nim ## let outp = execProcess("nim", args=["c", "-r", "mytestfile.nim"], options={poUsePath}) ## let outp_shell = execProcess("nim c -r mytestfile.nim") ## # Note: outp may have an interleave of text from the nim compile ## # and any output from mytestfile when it runs proc execCmd*(command: string): int {.rtl, extern: "nosp$1", tags: [ExecIOEffect, ReadIOEffect, RootEffect].} ## Executes ``command`` and returns its error code. ## ## Standard input, output, error streams are inherited from the calling process. ## This operation is also often called `system`:idx:. ## ## See also: ## * `execCmdEx proc <#execCmdEx,string,set[ProcessOption],StringTableRef,string,string>`_ ## * `startProcess proc ## <#startProcess,string,string,openArray[string],StringTableRef,set[ProcessOption]>`_ ## * `execProcess proc ## <#execProcess,string,string,openArray[string],StringTableRef,set[ProcessOption]>`_ ## ## Example: ## ## .. code-block:: Nim ## let errC = execCmd("nim c -r mytestfile.nim") proc startProcess*(command: string, workingDir: string = "", args: openArray[string] = [], env: StringTableRef = nil, options: set[ProcessOption] = {poStdErrToStdOut}): owned(Process) {.rtl, extern: "nosp$1", tags: [ExecIOEffect, ReadEnvEffect, RootEffect].} ## Starts a process. `Command` is the executable file, `workingDir` is the ## process's working directory. If ``workingDir == ""`` the current directory ## is used (default). `args` are the command line arguments that are passed to the ## process. On many operating systems, the first command line argument is the ## name of the executable. `args` should *not* contain this argument! ## `env` is the environment that will be passed to the process. ## If ``env == nil`` (default) the environment is inherited of ## the parent process. `options` are additional flags that may be passed ## to `startProcess`. See the documentation of `ProcessOption<#ProcessOption>`_ ## for the meaning of these flags. ## ## You need to `close <#close,Process>`_ the process when done. ## ## Note that you can't pass any `args` if you use the option ## ``poEvalCommand``, which invokes the system shell to run the specified ## `command`. In this situation you have to concatenate manually the contents ## of `args` to `command` carefully escaping/quoting any special characters, ##
#
#
#           The Nim Compiler
#        (c) Copyright 2023 Andreas Rumpf
#
#    See the file "copying.txt", included in this
#    distribution, for details about the copyright.
#

import ".." / ic / [bitabs, rodfiles]
import nirinsts, nirtypes, nirlineinfos

type
  NirModule* = object
    code*: Tree
    man*: LineInfoManager
    types*: TypeGraph
    lit*: Literals
    namespace*: LitId
    intbits*: uint32
    symnames*: SymNames

proc load*(filename: string): NirModule =
  let lit = Literals()
  result = NirModule(lit: lit, types: initTypeGraph(lit))
  var r = rodfiles.open(filename)
  try:
    r.loadHeader(nirCookie)
    r.loadSection stringsSection
    r.load result.lit.strings

    r.loadSection numbersSection
    r.load result.lit.numbers

    r.loadSection bodiesSection
    r.load result.code

    r.loadSection typesSection
    r.load result.types

    r.loadSection sideChannelSection
    r.load result.man

    r.loadSection namespaceSection
    r.loadPrim result.namespace
    r.loadPrim result.intbits

    r.loadSection symnamesSection
    r.load result.symnames

  finally:
    r.close()

proc store*(m: NirModule; outp: string) =
  var r = rodfiles.create(outp)
  try:
    r.storeHeader(nirCookie)
    r.storeSection stringsSection
    r.store m.lit.strings

    r.storeSection numbersSection
    r.store m.lit.numbers

    r.storeSection bodiesSection
    r.store m.code

    r.storeSection typesSection
    r.store m.types

    r.storeSection sideChannelSection
    r.store m.man

    r.storeSection namespaceSection
    r.storePrim m.namespace
    r.storePrim m.intbits

    r.storeSection symnamesSection
    r.store m.symnames

  finally:
    r.close()
  if r.err != ok:
    raise newException(IOError, "could store into: " & outp)
NG, # very important flag! FILE_ATTRIBUTE_NORMAL, 0 # no template file for OPEN_EXISTING ) if si.hStdOutput == INVALID_HANDLE_VALUE: raiseOSError(osLastError()) si.hStdError = myDup(si.hStdOutput) si.hStdInput = createFileW(pipeInName, FILE_READ_DATA or SYNCHRONIZE, 0, addr sa, OPEN_EXISTING, # very important flag! FILE_ATTRIBUTE_NORMAL, 0 # no template file for OPEN_EXISTING ) if si.hStdInput == INVALID_HANDLE_VALUE: raiseOSError(osLastError()) stdin = myDup(pipeIn, 0) stdout = myDup(pipeOut, 0) closeHandleCheck(pipeIn) closeHandleCheck(pipeOut) stderr = stdout proc createPipeHandles(rdHandle, wrHandle: var Handle) = var sa: SECURITY_ATTRIBUTES sa.nLength = sizeof(SECURITY_ATTRIBUTES).cint sa.lpSecurityDescriptor = nil sa.bInheritHandle = 1 if createPipe(rdHandle, wrHandle, sa, 0) == 0'i32: raiseOSError(osLastError()) proc startProcess(command: string, workingDir: string = "", args: openArray[string] = [], env: StringTableRef = nil, options: set[ProcessOption] = {poStdErrToStdOut}): owned Process = var si: STARTUPINFO procInfo: PROCESS_INFORMATION success: int hi, ho, he: Handle new(result) result.options = options result.exitFlag = true si.cb = sizeof(si).cint if poParentStreams notin options: si.dwFlags = STARTF_USESTDHANDLES # STARTF_USESHOWWINDOW or if poInteractive notin options: createPipeHandles(si.hStdInput, hi) createPipeHandles(ho, si.hStdOutput) if poStdErrToStdOut in options: si.hStdError = si.hStdOutput he = ho else: createPipeHandles(he, si.hStdError) if setHandleInformation(he, DWORD(1), DWORD(0)) == 0'i32: raiseOSError(osLastError()) if setHandleInformation(hi, DWORD(1), DWORD(0)) == 0'i32: raiseOSError(osLastError()) if setHandleInformation(ho, DWORD(1), DWORD(0)) == 0'i32: raiseOSError(osLastError()) else: createAllPipeHandles(si, hi, ho, he, cast[int](result)) result.inHandle = FileHandle(hi) result.outHandle = FileHandle(ho) result.errHandle = FileHandle(he) else: si.hStdError = getStdHandle(STD_ERROR_HANDLE) si.hStdInput = getStdHandle(STD_INPUT_HANDLE) si.hStdOutput = getStdHandle(STD_OUTPUT_HANDLE) result.inHandle = FileHandle(si.hStdInput) result.outHandle = FileHandle(si.hStdOutput) result.errHandle = FileHandle(si.hStdError) var cmdl: cstring var cmdRoot: string if poEvalCommand in options: cmdl = command assert args.len == 0 else: cmdRoot = buildCommandLine(command, args) cmdl = cstring(cmdRoot) var wd: cstring = nil var e = (str: nil.cstring, len: -1) if len(workingDir) > 0: wd = workingDir if env != nil: e = buildEnv(env) if poEchoCmd in options: echo($cmdl) when useWinUnicode: var tmp = newWideCString(cmdl) var ee = if e.str.isNil: newWideCString(cstring(nil)) else: newWideCString(e.str, e.len) var wwd = newWideCString(wd) var flags = NORMAL_PRIORITY_CLASS or CREATE_UNICODE_ENVIRONMENT if poDaemon in options: flags = flags or CREATE_NO_WINDOW success = winlean.createProcessW(nil, tmp, nil, nil, 1, flags, ee, wwd, si, procInfo) else: var ee = if e.str.isNil: cstring(nil) else: cstring(e.str) success = winlean.createProcessA(nil, cmdl, nil, nil, 1, NORMAL_PRIORITY_CLASS, ee, wd, si, procInfo) let lastError = osLastError() if poParentStreams notin options: fileClose(si.hStdInput) fileClose(si.hStdOutput) if poStdErrToStdOut notin options: fileClose(si.hStdError) if e.str != nil: dealloc(e.str) if success == 0: if poInteractive in result.options: close(result) const errInvalidParameter = 87.int const errFileNotFound = 2.int if lastError.int in {errInvalidParameter, errFileNotFound}: raiseOSError(lastError, "Requested command not found: '$1'. OS error:" % command) else: raiseOSError(lastError, command) result.fProcessHandle = procInfo.hProcess result.fThreadHandle = procInfo.hThread result.id = procInfo.dwProcessId result.exitFlag = false proc closeThreadAndProcessHandle(p: Process) = if p.fThreadHandle != 0: closeHandleCheck(p.fThreadHandle) p.fThreadHandle = 0 if p.fProcessHandle != 0: closeHandleCheck(p.fProcessHandle) p.fProcessHandle = 0 proc close(p: Process) = if poParentStreams notin p.options: if p.inStream == nil: p.inHandle.fileClose() else: # p.inHandle can be already closed via inputStream. p.inStream.close # You may NOT close outputStream and errorStream. assert p.outStream == nil or FileHandleStream(p.outStream).handle != INVALID_HANDLE_VALUE assert p.errStream == nil or FileHandleStream(p.errStream).handle != INVALID_HANDLE_VALUE if p.outHandle != p.errHandle: p.errHandle.fileClose() p.outHandle.fileClose() p.closeThreadAndProcessHandle() proc suspend(p: Process) = discard suspendThread(p.fThreadHandle) proc resume(p: Process) = discard resumeThread(p.fThreadHandle) proc running(p: Process): bool = if p.exitFlag: return false else: var x = waitForSingleObject(p.fProcessHandle, 0) return x == WAIT_TIMEOUT proc terminate(p: Process) = if running(p): discard terminateProcess(p.fProcessHandle, 0) proc kill(p: Process) = terminate(p) proc waitForExit(p: Process, timeout: int = -1): int = if p.exitFlag: return p.exitStatus let res = waitForSingleObject(p.fProcessHandle, timeout.int32) if res == WAIT_TIMEOUT: terminate(p) var status: int32 discard getExitCodeProcess(p.fProcessHandle, status) if status != STILL_ACTIVE: p.exitFlag = true p.exitStatus = status p.closeThreadAndProcessHandle() result = status else: result = -1 proc peekExitCode(p: Process): int = if p.exitFlag: return p.exitStatus result = -1 var b = waitForSingleObject(p.fProcessHandle, 0) == WAIT_TIMEOUT if not b: var status: int32 discard getExitCodeProcess(p.fProcessHandle, status) p.exitFlag = true p.exitStatus = status p.closeThreadAndProcessHandle() result = status proc inputStream(p: Process): Stream = streamAccess(p) if p.inStream == nil: p.inStream = newFileHandleStream(p.inHandle) result = p.inStream proc outputStream(p: Process): Stream = streamAccess(p) if p.outStream == nil: p.outStream = newFileHandleStream(p.outHandle) result = p.outStream proc errorStream(p: Process): Stream = streamAccess(p) if p.errStream == nil: p.errStream = newFileHandleStream(p.errHandle) result = p.errStream proc peekableOutputStream(p: Process): Stream = streamAccess(p) if p.outStream == nil: p.outStream = newFileHandleStream(p.outHandle).newPipeOutStream result = p.outStream proc peekableErrorStream(p: Process): Stream = streamAccess(p) if p.errStream == nil: p.errStream = newFileHandleStream(p.errHandle).newPipeOutStream result = p.errStream proc execCmd(command: string): int = var si: STARTUPINFO procInfo: PROCESS_INFORMATION process: Handle L: int32 si.cb = sizeof(si).cint si.hStdError = getStdHandle(STD_ERROR_HANDLE) si.hStdInput = getStdHandle(STD_INPUT_HANDLE) si.hStdOutput = getStdHandle(STD_OUTPUT_HANDLE) when useWinUnicode: var c = newWideCString(command) var res = winlean.createProcessW(nil, c, nil, nil, 0, NORMAL_PRIORITY_CLASS, nil, nil, si, procInfo) else: var res = winlean.createProcessA(nil, command, nil, nil, 0, NORMAL_PRIORITY_CLASS, nil, nil, si, procInfo) if res == 0: raiseOSError(osLastError()) else: process = procInfo.hProcess discard closeHandle(procInfo.hThread) if waitForSingleObject(process, INFINITE) != -1: discard getExitCodeProcess(process, L) result = int(L) else: result = -1 discard closeHandle(process) proc select(readfds: var seq[Process], timeout = 500): int = assert readfds.len <= MAXIMUM_WAIT_OBJECTS var rfds: WOHandleArray for i in 0..readfds.len()-1: rfds[i] = readfds[i].outHandle #fProcessHandle var ret = waitForMultipleObjects(readfds.len.int32, addr(rfds), 0'i32, timeout.int32) case ret of WAIT_TIMEOUT: return 0 of WAIT_FAILED: raiseOSError(osLastError()) else: var i = ret - WAIT_OBJECT_0 readfds.del(i) return 1 proc hasData*(p: Process): bool = var x: int32 if peekNamedPipe(p.outHandle, lpTotalBytesAvail = addr x): result = x > 0 elif not defined(useNimRtl): const readIdx = 0 writeIdx = 1 proc isExitStatus(status: cint): bool = WIFEXITED(status) or WIFSIGNALED(status) proc envToCStringArray(t: StringTableRef): cstringArray = result = cast[cstringArray](alloc0((t.len + 1) * sizeof(cstring))) var i = 0 for key, val in pairs(t): var x = key & "=" & val result[i] = cast[cstring](alloc(x.len+1)) copyMem(result[i], addr(x[0]), x.len+1) inc(i) proc envToCStringArray(): cstringArray = var counter = 0 for key, val in envPairs(): inc counter result = cast[cstringArray](alloc0((counter + 1) * sizeof(cstring))) var i = 0 for key, val in envPairs(): var x = key & "=" & val result[i] = cast[cstring](alloc(x.len+1)) copyMem(result[i], addr(x[0]), x.len+1) inc(i) type StartProcessData = object sysCommand: string sysArgs: cstringArray sysEnv: cstringArray workingDir: cstring pStdin, pStdout, pStderr, pErrorPipe: array[0..1, cint] options: set[ProcessOption] const useProcessAuxSpawn = declared(posix_spawn) and not defined(useFork) and not defined(useClone) and not defined(linux) when useProcessAuxSpawn: proc startProcessAuxSpawn(data: StartProcessData): Pid {. tags: [ExecIOEffect, ReadEnvEffect, ReadDirEffect, RootEffect], gcsafe.} else: proc startProcessAuxFork(data: StartProcessData): Pid {. tags: [ExecIOEffect, ReadEnvEffect, ReadDirEffect, RootEffect], gcsafe.} {.push stacktrace: off, profiler: off.} proc startProcessAfterFork(data: ptr StartProcessData) {. tags: [ExecIOEffect, ReadEnvEffect, ReadDirEffect, RootEffect], cdecl, gcsafe.} {.pop.} proc startProcess(command: string, workingDir: string = "", args: openArray[string] = [], env: StringTableRef = nil, options: set[ProcessOption] = {poStdErrToStdOut}): owned Process = var pStdin, pStdout, pStderr: array[0..1, cint] new(result) result.options = options result.exitFlag = true if poParentStreams notin options: if pipe(pStdin) != 0'i32 or pipe(pStdout) != 0'i32 or pipe(pStderr) != 0'i32: raiseOSError(osLastError()) var data: StartProcessData var sysArgsRaw: seq[string] if poEvalCommand in options: const useShPath {.strdefine.} = when not defined(android): "/bin/sh" else: "/system/bin/sh" data.sysCommand = useShPath sysArgsRaw = @[useShPath, "-c", command] assert args.len == 0, "`args` has to be empty when using poEvalCommand." else: data.sysCommand = command sysArgsRaw = @[command] for arg in args.items: sysArgsRaw.add arg var pid: Pid var sysArgs = allocCStringArray(sysArgsRaw) defer: deallocCStringArray(sysArgs) var sysEnv = if env == nil: envToCStringArray() else: envToCStringArray(env) defer: deallocCStringArray(sysEnv) data.sysArgs = sysArgs data.sysEnv = sysEnv data.pStdin = pStdin data.pStdout = pStdout data.pStderr = pStderr data.workingDir = workingDir data.options = options when useProcessAuxSpawn: var currentDir = getCurrentDir() pid = startProcessAuxSpawn(data) if workingDir.len > 0: setCurrentDir(currentDir) else: pid = startProcessAuxFork(data) # Parent process. Copy process information. if poEchoCmd in options: echo(command, " ", join(args, " ")) result.id = pid result.exitFlag = false if poParentStreams in options: # does not make much sense, but better than nothing: result.inHandle = 0 result.outHandle = 1 if poStdErrToStdOut in options: result.errHandle = result.outHandle else: result.errHandle = 2 else: result.inHandle = pStdin[writeIdx] result.outHandle = pStdout[readIdx] if poStdErrToStdOut in options: result.errHandle = result.outHandle discard close(pStderr[readIdx]) else: result.errHandle = pStderr[readIdx] discard close(pStderr[writeIdx]) discard close(pStdin[readIdx]) discard close(pStdout[writeIdx]) when useProcessAuxSpawn: proc startProcessAuxSpawn(data: StartProcessData): Pid = var attr: Tposix_spawnattr var fops: Tposix_spawn_file_actions template chck(e: untyped) = if e != 0'i32: raiseOSError(osLastError()) chck posix_spawn_file_actions_init(fops) chck posix_spawnattr_init(attr) var mask: Sigset chck sigemptyset(mask) chck posix_spawnattr_setsigmask(attr, mask) if poDaemon in data.options: chck posix_spawnattr_setpgroup(attr, 0'i32) var flags = POSIX_SPAWN_USEVFORK or POSIX_SPAWN_SETSIGMASK if poDaemon in data.options: flags = flags or POSIX_SPAWN_SETPGROUP chck posix_spawnattr_setflags(attr, flags) if not (poParentStreams in data.options): chck posix_spawn_file_actions_addclose(fops, data.pStdin[writeIdx]) chck posix_spawn_file_actions_adddup2(fops, data.pStdin[readIdx], readIdx) chck posix_spawn_file_actions_addclose(fops, data.pStdout[readIdx]) chck posix_spawn_file_actions_adddup2(fops, data.pStdout[writeIdx], writeIdx) chck posix_spawn_file_actions_addclose(fops, data.pStderr[readIdx]) if poStdErrToStdOut in data.options: chck posix_spawn_file_actions_adddup2(fops, data.pStdout[writeIdx], 2) else: chck posix_spawn_file_actions_adddup2(fops, data.pStderr[writeIdx], 2) var res: cint if data.workingDir.len > 0: setCurrentDir($data.workingDir) var pid: Pid if (poUsePath in data.options): res = posix_spawnp(pid, data.sysCommand.cstring, fops, attr, data.sysArgs, data.sysEnv) else: res = posix_spawn(pid, data.sysCommand.cstring, fops, attr, data.sysArgs, data.sysEnv) discard posix_spawn_file_actions_destroy(fops) discard posix_spawnattr_destroy(attr) if res != 0'i32: raiseOSError(OSErrorCode(res), data.sysCommand) return pid else: proc startProcessAuxFork(data: StartProcessData): Pid = if pipe(data.pErrorPipe) != 0: raiseOSError(osLastError()) defer: discard close(data.pErrorPipe[readIdx]) var pid: Pid var dataCopy = data when defined(useClone): const stackSize = 65536 let stackEnd = cast[clong](alloc(stackSize)) let stack = cast[pointer](stackEnd + stackSize) let fn: pointer = startProcessAfterFork pid = clone(fn, stack, cint(CLONE_VM or CLONE_VFORK or SIGCHLD), pointer(addr dataCopy), nil, nil, nil) discard close(data.pErrorPipe[writeIdx]) dealloc(stack) else: pid = fork() if pid == 0: startProcessAfterFork(addr(dataCopy)) exitnow(1) discard close(data.pErrorPipe[writeIdx]) if pid < 0: raiseOSError(osLastError()) var error: cint let sizeRead = read(data.pErrorPipe[readIdx], addr error, sizeof(error)) if sizeRead == sizeof(error): raiseOSError(osLastError(), "Could not find command: '$1'. OS error: $2" % [$data.sysCommand, $strerror(error)]) return pid {.push stacktrace: off, profiler: off.} proc startProcessFail(data: ptr StartProcessData) = var error: cint = errno discard write(data.pErrorPipe[writeIdx], addr error, sizeof(error)) exitnow(1) when not defined(uClibc) and (not defined(linux) or defined(android)) and not defined(haiku): var environ {.importc.}: cstringArray proc startProcessAfterFork(data: ptr StartProcessData) = # Warning: no GC here! # Or anything that touches global structures - all called nim procs # must be marked with stackTrace:off. Inspect C code after making changes. if not (poParentStreams in data.options): discard close(data.pStdin[writeIdx]) if dup2(data.pStdin[readIdx], readIdx) < 0: startProcessFail(data) discard close(data.pStdout[readIdx]) if dup2(data.pStdout[writeIdx], writeIdx) < 0: startProcessFail(data) discard close(data.pStderr[readIdx]) if (poStdErrToStdOut in data.options): if dup2(data.pStdout[writeIdx], 2) < 0: startProcessFail(data) else: if dup2(data.pStderr[writeIdx], 2) < 0: startProcessFail(data) if data.workingDir.len > 0: if chdir(data.workingDir) < 0: startProcessFail(data) discard close(data.pErrorPipe[readIdx]) discard fcntl(data.pErrorPipe[writeIdx], F_SETFD, FD_CLOEXEC) if (poUsePath in data.options): when defined(uClibc) or defined(linux) or defined(haiku): # uClibc environment (OpenWrt included) doesn't have the full execvpe let exe = findExe(data.sysCommand) discard execve(exe.cstring, data.sysArgs, data.sysEnv) else: # MacOSX doesn't have execvpe, so we need workaround. # On MacOSX we can arrive here only from fork, so this is safe: environ = data.sysEnv discard execvp(data.sysCommand.cstring, data.sysArgs) else: discard execve(data.sysCommand.cstring, data.sysArgs, data.sysEnv) startProcessFail(data) {.pop.} proc close(p: Process) = if poParentStreams notin p.options: if p.inStream != nil: close(p.inStream) else: discard close(p.inHandle) if p.outStream != nil: close(p.outStream) else: discard close(p.outHandle) if p.errStream != nil: close(p.errStream) else: discard close(p.errHandle) proc suspend(p: Process) = if kill(p.id, SIGSTOP) != 0'i32: raiseOSError(osLastError()) proc resume(p: Process) = if kill(p.id, SIGCONT) != 0'i32: raiseOSError(osLastError()) proc running(p: Process): bool = if p.exitFlag: return false else: var status: cint = 1 let ret = waitpid(p.id, status, WNOHANG) if ret == int(p.id): if isExitStatus(status): p.exitFlag = true p.exitStatus = status return false else: return true elif ret == 0: return true # Can't establish status. Assume running. else: raiseOSError(osLastError()) proc terminate(p: Process) = if kill(p.id, SIGTERM) != 0'i32: raiseOSError(osLastError()) proc kill(p: Process) = if kill(p.id, SIGKILL) != 0'i32: raiseOSError(osLastError()) when defined(macosx) or defined(freebsd) or defined(netbsd) or defined(openbsd) or defined(dragonfly): import kqueue proc waitForExit(p: Process, timeout: int = -1): int = if p.exitFlag: return exitStatusLikeShell(p.exitStatus) if timeout == -1: var status: cint = 1 if waitpid(p.id, status, 0) < 0: raiseOSError(osLastError()) p.exitFlag = true p.exitStatus = status else: var kqFD = kqueue() if kqFD == -1: raiseOSError(osLastError()) var kevIn = KEvent(ident: p.id.uint, filter: EVFILT_PROC, flags: EV_ADD, fflags: NOTE_EXIT) var kevOut: KEvent var tmspec: Timespec if timeout >= 1000: tmspec.tv_sec = posix.Time(timeout div 1_000) tmspec.tv_nsec = (timeout %% 1_000) * 1_000_000 else: tmspec.tv_sec = posix.Time(0) tmspec.tv_nsec = (timeout * 1_000_000) try: while true: var status: cint = 1 var count = kevent(kqFD, addr(kevIn), 1, addr(kevOut), 1, addr(tmspec)) if count < 0: let err = osLastError() if err.cint != EINTR: raiseOSError(osLastError()) elif count == 0: # timeout expired, so we trying to kill process if posix.kill(p.id, SIGKILL) == -1: raiseOSError(osLastError()) if waitpid(p.id, status, 0) < 0: raiseOSError(osLastError()) p.exitFlag = true p.exitStatus = status break else: if kevOut.ident == p.id.uint and kevOut.filter == EVFILT_PROC: if waitpid(p.id, status, 0) < 0: raiseOSError(osLastError()) p.exitFlag = true p.exitStatus = status break else: raiseOSError(osLastError()) finally: discard posix.close(kqFD) result = exitStatusLikeShell(p.exitStatus) elif defined(haiku): const B_OBJECT_TYPE_THREAD = 3 B_EVENT_INVALID = 0x1000 B_RELATIVE_TIMEOUT = 0x8 type ObjectWaitInfo {.importc: "object_wait_info", header: "OS.h".} = object obj {.importc: "object".}: int32 typ {.importc: "type".}: uint16 events: uint16 proc waitForObjects(infos: ptr ObjectWaitInfo, numInfos: cint, flags: uint32, timeout: int64): clong {.importc: "wait_for_objects_etc", header: "OS.h".} proc waitForExit(p: Process, timeout: int = -1): int = if p.exitFlag: return exitStatusLikeShell(p.exitStatus) if timeout == -1: var status: cint = 1 if waitpid(p.id, status, 0) < 0: raiseOSError(osLastError()) p.exitFlag = true p.exitStatus = status else: var info = ObjectWaitInfo( obj: p.id, # Haiku's PID is actually the main thread ID. typ: B_OBJECT_TYPE_THREAD, events: B_EVENT_INVALID # notify when the thread die. ) while true: var status: cint = 1 let count = waitForObjects(addr info, 1, B_RELATIVE_TIMEOUT, timeout) if count < 0: let err = count.cint if err == ETIMEDOUT: # timeout expired, so we try to kill the process if posix.kill(p.id, SIGKILL) == -1: raiseOSError(osLastError()) if waitpid(p.id, status, 0) < 0: raiseOSError(osLastError()) p.exitFlag = true p.exitStatus = status break elif err != EINTR: raiseOSError(err.OSErrorCode) elif count > 0: if waitpid(p.id, status, 0) < 0: raiseOSError(osLastError()) p.exitFlag = true p.exitStatus = status break else: doAssert false, "unreachable!" result = exitStatusLikeShell(p.exitStatus) else: import times const hasThreadSupport = compileOption("threads") and not defined(nimscript) proc waitForExit(p: Process, timeout: int = -1): int = template adjustTimeout(t, s, e: Timespec) = var diff: int var b: Timespec b.tv_sec = e.tv_sec b.tv_nsec = e.tv_nsec e.tv_sec = e.tv_sec - s.tv_sec if e.tv_nsec >= s.tv_nsec: e.tv_nsec -= s.tv_nsec else: if e.tv_sec == posix.Time(0): raise newException(ValueError, "System time was modified") else: diff = s.tv_nsec - e.tv_nsec e.tv_nsec = 1_000_000_000 - diff t.tv_sec = t.tv_sec - e.tv_sec if t.tv_nsec >= e.tv_nsec: t.tv_nsec -= e.tv_nsec else: t.tv_sec = t.tv_sec - posix.Time(1) diff = e.tv_nsec - t.tv_nsec t.tv_nsec = 1_000_000_000 - diff s.tv_sec = b.tv_sec s.tv_nsec = b.tv_nsec if p.exitFlag: return exitStatusLikeShell(p.exitStatus) if timeout == -1: var status: cint = 1 if waitpid(p.id, status, 0) < 0: raiseOSError(osLastError()) p.exitFlag = true p.exitStatus = status else: var nmask, omask: Sigset var sinfo: SigInfo var stspec, enspec, tmspec: Timespec discard sigemptyset(nmask) discard sigemptyset(omask) discard sigaddset(nmask, SIGCHLD) when hasThreadSupport: if pthread_sigmask(SIG_BLOCK, nmask, omask) == -1: raiseOSError(osLastError()) else: if sigprocmask(SIG_BLOCK, nmask, omask) == -1: raiseOSError(osLastError()) if timeout >= 1000: tmspec.tv_sec = posix.Time(timeout div 1_000) tmspec.tv_nsec = (timeout %% 1_000) * 1_000_000 else: tmspec.tv_sec = posix.Time(0) tmspec.tv_nsec = (timeout * 1_000_000) try: if clock_gettime(CLOCK_REALTIME, stspec) == -1: raiseOSError(osLastError()) while true: let res = sigtimedwait(nmask, sinfo, tmspec) if res == SIGCHLD: if sinfo.si_pid == p.id: var status: cint = 1 if waitpid(p.id, status, 0) < 0: raiseOSError(osLastError()) p.exitFlag = true p.exitStatus = status break else: # we have SIGCHLD, but not for process we are waiting, # so we need to adjust timeout value and continue if clock_gettime(CLOCK_REALTIME, enspec) == -1: raiseOSError(osLastError()) adjustTimeout(tmspec, stspec, enspec) elif res < 0: let err = osLastError() if err.cint == EINTR: # we have received another signal, so we need to # adjust timeout and continue if clock_gettime(CLOCK_REALTIME, enspec) == -1: raiseOSError(osLastError()) adjustTimeout(tmspec, stspec, enspec) elif err.cint == EAGAIN: # timeout expired, so we trying to kill process if posix.kill(p.id, SIGKILL) == -1: raiseOSError(osLastError()) var status: cint = 1 if waitpid(p.id, status, 0) < 0: raiseOSError(osLastError()) p.exitFlag = true p.exitStatus = status break else: raiseOSError(err) finally: when hasThreadSupport: if pthread_sigmask(SIG_UNBLOCK, nmask, omask) == -1: raiseOSError(osLastError()) else: if sigprocmask(SIG_UNBLOCK, nmask, omask) == -1: raiseOSError(osLastError()) result = exitStatusLikeShell(p.exitStatus) proc peekExitCode(p: Process): int = var status = cint(0) result = -1 if p.exitFlag: return exitStatusLikeShell(p.exitStatus) var ret = waitpid(p.id, status, WNOHANG) if ret > 0: if isExitStatus(status): p.exitFlag = true p.exitStatus = status result = exitStatusLikeShell(status) proc createStream(handle: var FileHandle, fileMode: FileMode): owned FileStream = var f: File if not open(f, handle, fileMode): raiseOSError(osLastError()) return newFileStream(f) proc inputStream(p: Process): Stream = streamAccess(p) if p.inStream == nil: p.inStream = createStream(p.inHandle, fmWrite) return p.inStream proc outputStream(p: Process): Stream = streamAccess(p) if p.outStream == nil: p.outStream = createStream(p.outHandle, fmRead) return p.outStream proc errorStream(p: Process): Stream = streamAccess(p) if p.errStream == nil: p.errStream = createStream(p.errHandle, fmRead) return p.errStream proc peekableOutputStream(p: Process): Stream = streamAccess(p) if p.outStream == nil: p.outStream = createStream(p.outHandle, fmRead).newPipeOutStream return p.outStream proc peekableErrorStream(p: Process): Stream = streamAccess(p) if p.errStream == nil: p.errStream = createStream(p.errHandle, fmRead).newPipeOutStream return p.errStream proc csystem(cmd: cstring): cint {.nodecl, importc: "system", header: "".} proc execCmd(command: string): int = when defined(posix): let tmp = csystem(command) result = if tmp == -1: tmp else: exitStatusLikeShell(tmp) else: result = csystem(command) proc createFdSet(fd: var TFdSet, s: seq[Process], m: var int) = FD_ZERO(fd) for i in items(s): m = max(m, int(i.outHandle)) FD_SET(cint(i.outHandle), fd) proc pruneProcessSet(s: var seq[Process], fd: var TFdSet) = var i = 0 var L = s.len while i < L: if FD_ISSET(cint(s[i].outHandle), fd) == 0'i32: s[i] = s[L-1] dec(L) else: inc(i) setLen(s, L) proc select(readfds: var seq[Process], timeout = 500): int = var tv: Timeval tv.tv_sec = posix.Time(0) tv.tv_usec = Suseconds(timeout * 1000) var rd: TFdSet var m = 0 createFdSet((rd), readfds, m) if timeout != -1: result = int(select(cint(m+1), addr(rd), nil, nil, addr(tv))) else: result = int(select(cint(m+1), addr(rd), nil, nil, nil)) pruneProcessSet(readfds, (rd)) proc hasData*(p: Process): bool = var rd: TFdSet FD_ZERO(rd) let m = max(0, int(p.outHandle)) FD_SET(cint(p.outHandle), rd) result = int(select(cint(m+1), addr(rd), nil, nil, nil)) == 1 proc execCmdEx*(command: string, options: set[ProcessOption] = { poStdErrToStdOut, poUsePath}, env: StringTableRef = nil, workingDir = "", input = ""): tuple[ output: string, exitCode: int] {.tags: [ExecIOEffect, ReadIOEffect, RootEffect], gcsafe.} = ## A convenience proc that runs the `command`, and returns its `output` and ## `exitCode`. `env` and `workingDir` params behave as for `startProcess`. ## If `input.len > 0`, it is passed as stdin. ## ## Note: this could block if `input.len` is greater than your OS's maximum ## pipe buffer size. ## ## See also: ## * `execCmd proc <#execCmd,string>`_ ## * `startProcess proc ## <#startProcess,string,string,openArray[string],StringTableRef,set[ProcessOption]>`_ ## * `execProcess proc ## <#execProcess,string,string,openArray[string],StringTableRef,set[ProcessOption]>`_ ## ## Example: ## ## .. code-block:: Nim ## var result = execCmdEx("nim r --hints:off -", options = {}, input = "echo 3*4") ## import std/[strutils, strtabs] ## stripLineEnd(result[0]) ## portable way to remove trailing newline, if any ## doAssert result == ("12", 0) ## doAssert execCmdEx("ls --nonexistent").exitCode != 0 ## when defined(posix): ## assert execCmdEx("echo $FO", env = newStringTable({"FO": "B"})) == ("B\n", 0) ## assert execCmdEx("echo $PWD", workingDir = "/") == ("/\n", 0) when (NimMajor, NimMinor, NimPatch) < (1, 3, 5): doAssert input.len == 0 doAssert workingDir.len == 0 doAssert env == nil var p = startProcess(command, options = options + {poEvalCommand}, workingDir = workingDir, env = env) var outp = outputStream(p) if input.len > 0: # There is no way to provide input for the child process # anymore. Closing it will create EOF on stdin instead of eternal # blocking. # Writing in chunks would require a selectors (eg kqueue/epoll) to avoid # blocking on io. inputStream(p).write(input) close inputStream(p) # consider `p.lines(keepNewLines=true)` to avoid exit code test result = ("", -1) var line = newStringOfCap(120) while true: if outp.readLine(line): result[0].add(line) result[0].add("\n") else: result[1] = peekExitCode(p) if result[1] != -1: break close(p)