summary refs log tree commit diff stats
path: root/lib/pure/osproc.nim
diff options
context:
space:
mode:
Diffstat (limited to 'lib/pure/osproc.nim')
-rw-r--r--[-rwxr-xr-x]lib/pure/osproc.nim1884
1 files changed, 1461 insertions, 423 deletions
diff --git a/lib/pure/osproc.nim b/lib/pure/osproc.nim
index bbdea1eee..c304ecca6 100755..100644
--- a/lib/pure/osproc.nim
+++ b/lib/pure/osproc.nim
@@ -1,7 +1,7 @@
 #
 #
-#            Nimrod's Runtime Library
-#        (c) Copyright 2010 Andreas Rumpf
+#            Nim's Runtime Library
+#        (c) Copyright 2015 Andreas Rumpf
 #
 #    See the file "copying.txt", included in this
 #    distribution, for details about the copyright.
@@ -9,423 +9,911 @@
 
 ## This module implements an advanced facility for executing OS processes
 ## and process communication.
+##
+## **See also:**
+## * `os module <os.html>`_
+## * `streams module <streams.html>`_
+## * `memfiles module <memfiles.html>`_
+
+include "system/inclrtl"
 
 import
-  strutils, os, strtabs, streams
+  std/[strutils, os, strtabs, streams, cpuinfo, streamwrapper,
+  private/since]
+
+export quoteShell, quoteShellWindows, quoteShellPosix
 
 when defined(windows):
-  import winlean
+  import std/winlean
 else:
-  import posix
+  import std/posix
+
+when defined(linux) and defined(useClone):
+  import std/linux
+
+when defined(nimPreviewSlimSystem):
+  import std/[syncio, assertions]
+  when defined(windows):
+    import std/widestrs
+
 
 type
-  TProcess = object of TObject
+  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: Thandle
-      inputHandle, outputHandle, errorHandle: TFileHandle
+      fProcessHandle: Handle
+      fThreadHandle: Handle
+      inHandle, outHandle, errHandle: FileHandle
+      id: Handle
     else:
-      inputHandle, outputHandle, errorHandle: TFileHandle
-    id: cint
-    exitCode: cint
-
-  PProcess* = ref TProcess ## represents an operating system process
-
-  TProcessOption* = enum ## options that can be passed `startProcess`
-    poEchoCmd,           ## echo the command before execution
-    poUseShell,          ## use the shell to execute the command; NOTE: This
-                         ## often creates a security hole!
-    poStdErrToStdOut,    ## merge stdout and stderr to the stdout stream
-    poParentStreams      ## use the parent's streams
-
-proc execProcess*(command: string,
-                  options: set[TProcessOption] = {poStdErrToStdOut,
-                                                  poUseShell}): string
-  ## A convience procedure that executes ``command`` with ``startProcess``
+      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", raises: [OSError, IOError],
+                  tags: [ExecIOEffect, ReadIOEffect, RootEffect].}
+  ## A convenience procedure that executes ``command`` with ``startProcess``
   ## and returns its output as a string.
-
-proc executeProcess*(command: string,
-                     options: set[TProcessOption] = {poStdErrToStdOut,
-                                                     poUseShell}): string {.
-                                                     deprecated.} =
-  ## **Deprecated since version 0.8.2**: Use `execProcess` instead.
-  result = execProcess(command, options)
-
-proc execCmd*(command: string): int
-  ## Executes ``command`` and returns its error code. Standard input, output,
-  ## error streams are inherited from the calling process.
-
-proc executeCommand*(command: string): int {.deprecated.} =
-  ## **Deprecated since version 0.8.2**: Use `execCmd` instead.
-  result = execCmd(command)
-  
-
-proc startProcess*(command: string,
-                   workingDir: string = "",
-                   args: openarray[string] = [],
-                   env: PStringTable = nil,
-                   options: set[TProcessOption] = {poStdErrToStdOut}): PProcess
+  ##
+  ## .. 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:
+  ##   ```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:
+  ##   ```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", raises: [OSError, IOError],
+                   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. `args` are the command line arguments that are passed to the
+  ## 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!
+  ## name of the executable. `args` should *not* contain this argument!
   ## `env` is the environment that will be passed to the process.
-  ## If ``env == nil`` the environment is inherited of
+  ## 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 ``TProcessOption`` for the
-  ## meaning of these flags.
+  ## 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,
+  ## since it will be passed *as is* to the system shell. Each system/shell may
+  ## feature different escaping rules, so try to avoid this kind of shell
+  ## invocation if possible as it leads to non portable software.
   ##
   ## Return value: The newly created process object. Nil is never returned,
-  ## but ``EOS`` is raised in case of an error.
+  ## but ``OSError`` is raised in case of an error.
+  ##
+  ## See also:
+  ## * `execProcesses proc <#execProcesses,openArray[string],proc(int),proc(int,Process)>`_
+  ## * `execProcess proc
+  ##   <#execProcess,string,string,openArray[string],StringTableRef,set[ProcessOption]>`_
+  ## * `execCmd proc <#execCmd,string>`_
+
+proc close*(p: Process) {.rtl, extern: "nosp$1", raises: [IOError, OSError], tags: [WriteIOEffect].}
+  ## When the process has finished executing, cleanup related handles.
+  ##
+  ## .. warning:: If the process has not finished executing, this will forcibly
+  ##   terminate the process. Doing so may result in zombie processes and
+  ##   `pty leaks <http://stackoverflow.com/questions/27021641/how-to-fix-request-failed-on-channel-0>`_.
 
-proc suspend*(p: PProcess)
+proc suspend*(p: Process) {.rtl, extern: "nosp$1", tags: [].}
   ## Suspends the process `p`.
+  ##
+  ## See also:
+  ## * `resume proc <#resume,Process>`_
+  ## * `terminate proc <#terminate,Process>`_
+  ## * `kill proc <#kill,Process>`_
 
-proc resume*(p: PProcess)
+
+proc resume*(p: Process) {.rtl, extern: "nosp$1", tags: [].}
   ## Resumes the process `p`.
+  ##
+  ## See also:
+  ## * `suspend proc <#suspend,Process>`_
+  ## * `terminate proc <#terminate,Process>`_
+  ## * `kill proc <#kill,Process>`_
 
-proc terminate*(p: PProcess)
-  ## Terminates the process `p`.
+proc terminate*(p: Process) {.rtl, extern: "nosp$1", tags: [].}
+  ## Stop the process `p`.
+  ##
+  ## On Posix OSes the procedure sends ``SIGTERM`` to the process.
+  ## On Windows the Win32 API function ``TerminateProcess()``
+  ## is called to stop the process.
+  ##
+  ## See also:
+  ## * `suspend proc <#suspend,Process>`_
+  ## * `resume proc <#resume,Process>`_
+  ## * `kill proc <#kill,Process>`_
+  ## * `posix_utils.sendSignal(pid: Pid, signal: int) <posix_utils.html#sendSignal,Pid,int>`_
+
+proc kill*(p: Process) {.rtl, extern: "nosp$1", tags: [].}
+  ## Kill the process `p`.
+  ##
+  ## On Posix OSes the procedure sends ``SIGKILL`` to the process.
+  ## On Windows ``kill`` is simply an alias for `terminate() <#terminate,Process>`_.
+  ##
+  ## See also:
+  ## * `suspend proc <#suspend,Process>`_
+  ## * `resume proc <#resume,Process>`_
+  ## * `terminate proc <#terminate,Process>`_
+  ## * `posix_utils.sendSignal(pid: Pid, signal: int) <posix_utils.html#sendSignal,Pid,int>`_
 
-proc running*(p: PProcess): bool
-  ## Returns true iff the process `p` is still running. Returns immediately.
+proc running*(p: Process): bool {.rtl, extern: "nosp$1", raises: [OSError], tags: [].}
+  ## Returns true if the process `p` is still running. Returns immediately.
 
-proc processID*(p: PProcess): int =
-  ## returns `p`'s process ID.
+proc processID*(p: Process): int {.rtl, extern: "nosp$1".} =
+  ## Returns `p`'s process ID.
+  ##
+  ## See also:
+  ## * `os.getCurrentProcessId proc <os.html#getCurrentProcessId>`_
   return p.id
 
-proc waitForExit*(p: PProcess): int
-  ## waits for the process to finish and returns `p`'s error code.
+proc waitForExit*(p: Process, timeout: int = -1): int {.rtl,
+    extern: "nosp$1", raises: [OSError, ValueError], tags: [TimeEffect].}
+  ## Waits for the process to finish and returns `p`'s error code.
+  ##
+  ## .. warning:: Be careful when using `waitForExit` for processes created without
+  ##   `poParentStreams` because they may fill output buffers, causing deadlock.
+  ##
+  ## On posix, if the process has exited because of a signal, 128 + signal
+  ## number will be returned.
+  ##
+  ## .. warning:: When working with `timeout` parameters, remember that the value is 
+  ##   typically expressed in milliseconds, and ensure that the correct unit of time
+  ##   is used to avoid unexpected behavior.
 
-proc inputStream*(p: PProcess): PStream
-  ## returns ``p``'s input stream for writing to
+proc peekExitCode*(p: Process): int {.rtl, extern: "nosp$1", raises: [OSError], tags: [].}
+  ## Return `-1` if the process is still running. Otherwise the process' exit code.
+  ##
+  ## On posix, if the process has exited because of a signal, 128 + signal
+  ## number will be returned.
 
-proc outputStream*(p: PProcess): PStream
-  ## returns ``p``'s output stream for reading from
+proc inputStream*(p: Process): Stream {.rtl, extern: "nosp$1", tags: [].}
+  ## Returns ``p``'s input stream for writing to.
+  ##
+  ## .. warning:: The returned `Stream` should not be closed manually as it
+  ##   is closed when closing the Process ``p``.
+  ##
+  ## See also:
+  ## * `outputStream proc <#outputStream,Process>`_
+  ## * `errorStream proc <#errorStream,Process>`_
+
+proc outputStream*(p: Process): Stream {.rtl, extern: "nosp$1", raises: [IOError, OSError], tags: [].}
+  ## Returns ``p``'s output stream for reading from.
+  ##
+  ## You cannot perform peek/write/setOption operations to this stream.
+  ## Use `peekableOutputStream proc <#peekableOutputStream,Process>`_
+  ## if you need to peek stream.
+  ##
+  ## .. warning:: The returned `Stream` should not be closed manually as it
+  ##   is closed when closing the Process ``p``.
+  ##
+  ## See also:
+  ## * `inputStream proc <#inputStream,Process>`_
+  ## * `errorStream proc <#errorStream,Process>`_
 
-proc errorStream*(p: PProcess): PStream
-  ## returns ``p``'s output stream for reading from
+proc errorStream*(p: Process): Stream {.rtl, extern: "nosp$1", tags: [].}
+  ## Returns ``p``'s error stream for reading from.
+  ##
+  ## You cannot perform peek/write/setOption operations to this stream.
+  ## Use `peekableErrorStream proc <#peekableErrorStream,Process>`_
+  ## if you need to peek stream.
+  ##
+  ## .. warning:: The returned `Stream` should not be closed manually as it
+  ##   is closed when closing the Process ``p``.
+  ##
+  ## See also:
+  ## * `inputStream proc <#inputStream,Process>`_
+  ## * `outputStream proc <#outputStream,Process>`_
 
-when defined(macosx) or defined(bsd):
-  const
-    CTL_HW = 6
-    HW_AVAILCPU = 25
-    HW_NCPU = 3
-  proc sysctl(x: ptr array[0..3, cint], y: cint, z: pointer,
-              a: var int, b: pointer, c: int): cint {.
-             importc: "sysctl", header: "<sys/sysctl.h>".}
-
-proc countProcessors*(): int = 
-  ## returns the numer of the processors/cores the machine has.
-  ## Returns 0 if it cannot be determined.
-  when defined(windows):
-    var x = getenv("NUMBER_OF_PROCESSORS")
-    if x.len > 0: result = parseInt(x)
-  elif defined(macosx) or defined(bsd):
-    var
-      mib: array[0..3, cint]
-      len, numCPU: int
-    mib[0] = CTL_HW
-    mib[1] = HW_AVAILCPU
-    len = sizeof(numCPU)
-    discard sysctl(addr(mib), 2, addr(numCPU), len, nil, 0)
-    if numCPU < 1:
-      mib[1] = HW_NCPU
-      discard sysctl(addr(mib), 2, addr(numCPU), len, nil, 0)
-    result = numCPU
-  elif defined(hpux):
-    result = mpctl(MPC_GETNUMSPUS, nil, nil)
-  elif defined(irix):
-    var SC_NPROC_ONLN {.importc: "_SC_NPROC_ONLN", header: "<unistd.h>".}: cint
-    result = sysconf(SC_NPROC_ONLN)
-  else:
-    result = sysconf(SC_NPROCESSORS_ONLN)
-  if result <= 0: result = 1
+proc peekableOutputStream*(p: Process): Stream {.rtl, extern: "nosp$1", tags: [], since: (1, 3).}
+  ## Returns ``p``'s output stream for reading from.
+  ##
+  ## You can peek returned stream.
+  ##
+  ## .. warning:: The returned `Stream` should not be closed manually as it
+  ##   is closed when closing the Process ``p``.
+  ##
+  ## See also:
+  ## * `outputStream proc <#outputStream,Process>`_
+  ## * `peekableErrorStream proc <#peekableErrorStream,Process>`_
 
-proc startProcessAux(cmd: string, options: set[TProcessOption]): PProcess =
-  var c = parseCmdLine(cmd)
-  var a: seq[string] = @[] # slicing is not yet implemented :-(
-  for i in 1 .. c.len-1: add(a, c[i])
-  result = startProcess(command=c[0], args=a, options=options)
+proc peekableErrorStream*(p: Process): Stream {.rtl, extern: "nosp$1", tags: [], since: (1, 3).}
+  ## Returns ``p``'s error stream for reading from.
+  ##
+  ## You can run peek operation to returned stream.
+  ##
+  ## .. warning:: The returned `Stream` should not be closed manually as it
+  ##   is closed when closing the Process ``p``.
+  ##
+  ## See also:
+  ## * `errorStream proc <#errorStream,Process>`_
+  ## * `peekableOutputStream proc <#peekableOutputStream,Process>`_
+
+proc inputHandle*(p: Process): FileHandle {.rtl, raises: [], extern: "nosp$1",
+  tags: [].} =
+  ## Returns ``p``'s input file handle for writing to.
+  ##
+  ## .. warning:: The returned `FileHandle` should not be closed manually as
+  ##   it is closed when closing the Process ``p``.
+  ##
+  ## See also:
+  ## * `outputHandle proc <#outputHandle,Process>`_
+  ## * `errorHandle proc <#errorHandle,Process>`_
+  result = p.inHandle
+
+proc outputHandle*(p: Process): FileHandle {.rtl, extern: "nosp$1",
+    raises: [], tags: [].} =
+  ## Returns ``p``'s output file handle for reading from.
+  ##
+  ## .. warning:: The returned `FileHandle` should not be closed manually as
+  ##   it is closed when closing the Process ``p``.
+  ##
+  ## See also:
+  ## * `inputHandle proc <#inputHandle,Process>`_
+  ## * `errorHandle proc <#errorHandle,Process>`_
+  result = p.outHandle
+
+proc errorHandle*(p: Process): FileHandle {.rtl, extern: "nosp$1",
+    raises: [], tags: [].} =
+  ## Returns ``p``'s error file handle for reading from.
+  ##
+  ## .. warning:: The returned `FileHandle` should not be closed manually as
+  ##   it is closed when closing the Process ``p``.
+  ##
+  ## See also:
+  ## * `inputHandle proc <#inputHandle,Process>`_
+  ## * `outputHandle proc <#outputHandle,Process>`_
+  result = p.errHandle
+
+proc countProcessors*(): int {.rtl, extern: "nosp$1", raises: [].} =
+  ## Returns the number of the processors/cores the machine has.
+  ## Returns 0 if it cannot be detected.
+  ## It is implemented just calling `cpuinfo.countProcessors`.
+  result = cpuinfo.countProcessors()
+
+when not defined(nimHasEffectsOf):
+  {.pragma: effectsOf.}
 
 proc execProcesses*(cmds: openArray[string],
-                    options = {poStdErrToStdOut, poParentStreams},
-                    n = countProcessors()): int =
-  ## executes the commands `cmds` in parallel. Creates `n` processes
-  ## that execute in parallel. The highest return value of all processes
-  ## is returned.
+    options = {poStdErrToStdOut, poParentStreams}, n = countProcessors(),
+    beforeRunEvent: proc(idx: int) = nil,
+    afterRunEvent: proc(idx: int, p: Process) = nil):
+  int {.rtl, extern: "nosp$1",
+        raises: [ValueError, OSError, IOError],
+        tags: [ExecIOEffect, TimeEffect, ReadEnvEffect, RootEffect],
+        effectsOf: [beforeRunEvent, afterRunEvent].} =
+  ## Executes the commands `cmds` in parallel.
+  ## Creates `n` processes that execute in parallel.
+  ##
+  ## The highest (absolute) return value of all processes is returned.
+  ## Runs `beforeRunEvent` before running each command.
+
   assert n > 0
   if n > 1:
-    var q: seq[PProcess]
-    newSeq(q, n)
-    var m = min(n, cmds.len)
-    for i in 0..m-1:
-      q[i] = startProcessAux(cmds[i], options=options)
-    when defined(noBusyWaiting):
-      var r = 0
-      for i in m..high(cmds):
-        when defined(debugExecProcesses):
-          var err = ""
-          var outp = outputStream(q[r])
-          while running(q[r]) or not outp.atEnd(outp):
-            err.add(outp.readLine())
-            err.add("\n")
-          echo(err)
-        result = max(waitForExit(q[r]), result)
-        q[r] = startProcessAux(cmds[i], options=options)
-        r = (r + 1) mod n
-    else:
-      var i = m
-      while i <= high(cmds):
-        sleep(50)
-        for r in 0..n-1:
-          if not running(q[r]):
-            #echo(outputStream(q[r]).readLine())
-            result = max(waitForExit(q[r]), result)
-            q[r] = startProcessAux(cmds[i], options=options)
-            inc(i)
-            if i > high(cmds): break
-    for i in 0..m-1:
-      result = max(waitForExit(q[i]), result)  
-  else:
-    for i in 0..high(cmds):
-      var p = startProcessAux(cmds[i], options=options)
-      result = max(waitForExit(p), result)
-
-when true:
-  nil
-else:
-  proc startGUIProcess*(command: string,
-                     workingDir: string = "",
-                     args: openarray[string] = [],
-                     env: PStringTable = nil,
-                     x = -1,
-                     y = -1,
-                     width = -1,
-                     height = -1): PProcess
-
-proc execProcess(command: string,
-                 options: set[TProcessOption] = {poStdErrToStdOut,
-                                                 poUseShell}): string =
-  var p = startProcessAux(command, options=options)
-  var outp = outputStream(p)
-  result = ""
-  while running(p) or not outp.atEnd(outp):
-    result.add(outp.readLine())
-    result.add("\n")
-
-when false:
-  proc deallocCStringArray(a: cstringArray) =
     var i = 0
-    while a[i] != nil:
-      dealloc(a[i])
+    var q = newSeq[Process](n)
+    var idxs = newSeq[int](n) # map process index to cmds index
+
+    when defined(windows):
+      var w: WOHandleArray
+      var m = min(min(n, MAXIMUM_WAIT_OBJECTS), cmds.len)
+      var wcount = m
+    else:
+      var m = min(n, cmds.len)
+
+    while i < m:
+      if beforeRunEvent != nil:
+        beforeRunEvent(i)
+      q[i] = startProcess(cmds[i], options = options + {poEvalCommand})
+      idxs[i] = i
+      when defined(windows):
+        w[i] = q[i].fProcessHandle
       inc(i)
-    dealloc(a)
 
-when defined(Windows):
+    var ecount = len(cmds)
+    while ecount > 0:
+      var rexit = -1
+      when defined(windows):
+        # waiting for all children, get result if any child exits
+        var ret = waitForMultipleObjects(int32(wcount), addr(w), 0'i32,
+                                         INFINITE)
+        if ret == WAIT_TIMEOUT:
+          # must not be happen
+          discard
+        elif ret == WAIT_FAILED:
+          raiseOSError(osLastError())
+        else:
+          var status: int32
+          for r in 0..m-1:
+            if not isNil(q[r]) and q[r].fProcessHandle == w[ret]:
+              discard getExitCodeProcess(q[r].fProcessHandle, status)
+              q[r].exitFlag = true
+              q[r].exitStatus = status
+              rexit = r
+              break
+      else:
+        var status: cint = 1
+        # waiting for all children, get result if any child exits
+        let res = waitpid(-1, status, 0)
+        if res > 0:
+          for r in 0..m-1:
+            if not isNil(q[r]) and q[r].id == res:
+              if WIFEXITED(status) or WIFSIGNALED(status):
+                q[r].exitFlag = true
+                q[r].exitStatus = status
+                rexit = r
+                break
+        else:
+          let err = osLastError()
+          if err == OSErrorCode(ECHILD):
+            # some child exits, we need to check our childs exit codes
+            for r in 0..m-1:
+              if (not isNil(q[r])) and (not running(q[r])):
+                q[r].exitFlag = true
+                q[r].exitStatus = status
+                rexit = r
+                break
+          elif err == OSErrorCode(EINTR):
+            # signal interrupted our syscall, lets repeat it
+            continue
+          else:
+            # all other errors are exceptions
+            raiseOSError(err)
+
+      if rexit >= 0:
+        when defined(windows):
+          let processHandle = q[rexit].fProcessHandle
+        result = max(result, abs(q[rexit].peekExitCode()))
+        if afterRunEvent != nil: afterRunEvent(idxs[rexit], q[rexit])
+        close(q[rexit])
+        if i < len(cmds):
+          if beforeRunEvent != nil: beforeRunEvent(i)
+          q[rexit] = startProcess(cmds[i],
+                                  options = options + {poEvalCommand})
+          idxs[rexit] = i
+          when defined(windows):
+            w[rexit] = q[rexit].fProcessHandle
+          inc(i)
+        else:
+          when defined(windows):
+            for k in 0..wcount - 1:
+              if w[k] == processHandle:
+                w[k] = w[wcount - 1]
+                w[wcount - 1] = 0
+                dec(wcount)
+                break
+          q[rexit] = nil
+        dec(ecount)
+  else:
+    for i in 0..high(cmds):
+      if beforeRunEvent != nil:
+        beforeRunEvent(i)
+      var p = startProcess(cmds[i], options = options + {poEvalCommand})
+      result = max(abs(waitForExit(p)), result)
+      if afterRunEvent != nil: afterRunEvent(i, p)
+      close(p)
+
+iterator lines*(p: Process, keepNewLines = false): string {.since: (1, 3), raises: [OSError, IOError, ValueError], tags: [ReadIOEffect, TimeEffect].} =
+  ## Convenience iterator for working with `startProcess` to read data from a
+  ## background process.
+  ##
+  ## See also:
+  ## * `readLines proc <#readLines,Process>`_
+  ##
+  ## Example:
+  ##   ```Nim
+  ##   const opts = {poUsePath, poDaemon, poStdErrToStdOut}
+  ##   var ps: seq[Process]
+  ##   for prog in ["a", "b"]: # run 2 progs in parallel
+  ##     ps.add startProcess("nim", "", ["r", prog], nil, opts)
+  ##   for p in ps:
+  ##     var i = 0
+  ##     for line in p.lines:
+  ##       echo line
+  ##       i.inc
+  ##       if i > 100: break
+  ##     p.close
+  ##   ```
+  var outp = p.outputStream
+  var line = newStringOfCap(120)
+  while outp.readLine(line):
+    if keepNewLines:
+      line.add("\n")
+    yield line
+  discard waitForExit(p)
+
+proc readLines*(p: Process): (seq[string], int) {.since: (1, 3),
+    raises: [OSError, IOError, ValueError], tags: [ReadIOEffect, TimeEffect].} =
+  ## Convenience function for working with `startProcess` to read data from a
+  ## background process.
+  ##
+  ## See also:
+  ## * `lines iterator <#lines.i,Process>`_
+  ##
+  ## Example:
+  ##   ```Nim
+  ##   const opts = {poUsePath, poDaemon, poStdErrToStdOut}
+  ##   var ps: seq[Process]
+  ##   for prog in ["a", "b"]: # run 2 progs in parallel
+  ##     ps.add startProcess("nim", "", ["r", prog], nil, opts)
+  ##   for p in ps:
+  ##     let (lines, exCode) = p.readLines
+  ##     if exCode != 0:
+  ##       for line in lines: echo line
+  ##     p.close
+  ##   ```
+  for line in p.lines: result[0].add(line)
+  result[1] = p.peekExitCode
+
+when not defined(useNimRtl):
+  proc execProcess(command: string, workingDir: string = "",
+      args: openArray[string] = [], env: StringTableRef = nil,
+      options: set[ProcessOption] = {poStdErrToStdOut, poUsePath,
+          poEvalCommand}):
+    string =
+
+    var p = startProcess(command, workingDir = workingDir, args = args,
+        env = env, options = options)
+    var outp = outputStream(p)
+    result = ""
+    var line = newStringOfCap(120)
+    # consider `p.lines(keepNewLines=true)` to circumvent `running` busy-wait
+    while true:
+      # FIXME: converts CR-LF to LF.
+      if outp.readLine(line):
+        result.add(line)
+        result.add("\n")
+      elif not running(p): break
+    close(p)
+
+template streamAccess(p) =
+  assert poParentStreams notin p.options, "API usage error: stream access not allowed when you use poParentStreams"
+
+when defined(windows) and not defined(useNimRtl):
   # We need to implement a handle stream for Windows:
   type
-    PFileHandleStream = ref TFileHandleStream
-    TFileHandleStream = object of TStream
-      handle: THandle
+    FileHandleStream = ref object of StreamObj
+      handle: Handle
       atTheEnd: bool
 
-  proc hsClose(s: PFileHandleStream) = nil # nothing to do here
-  proc hsAtEnd(s: PFileHandleStream): bool = return s.atTheEnd
+  proc closeHandleCheck(handle: Handle) {.inline.} =
+    if handle.closeHandle() == 0:
+      raiseOSError(osLastError())
 
-  proc hsReadData(s: PFileHandleStream, buffer: pointer, bufLen: int): int =
+  proc fileClose[T: Handle | FileHandle](h: var T) {.inline.} =
+    if h > 4:
+      closeHandleCheck(h)
+      h = INVALID_HANDLE_VALUE.T
+
+  proc hsClose(s: Stream) =
+    FileHandleStream(s).handle.fileClose()
+
+  proc hsAtEnd(s: Stream): bool = return FileHandleStream(s).atTheEnd
+
+  proc hsReadData(s: Stream, buffer: pointer, bufLen: int): int =
+    var s = FileHandleStream(s)
     if s.atTheEnd: return 0
     var br: int32
-    var a = winlean.ReadFile(s.handle, buffer, bufLen, br, nil)
+    var a = winlean.readFile(s.handle, buffer, bufLen.cint, addr br, nil)
     # TRUE and zero bytes returned (EOF).
     # TRUE and n (>0) bytes returned (good data).
     # FALSE and bytes returned undefined (system error).
-    if a == 0 and br != 0: OSError()
-    s.atTheEnd = br < bufLen
+    if a == 0 and br != 0: raiseOSError(osLastError())
+    s.atTheEnd = br == 0 #< bufLen
     result = br
-  
-  proc hsWriteData(s: PFileHandleStream, buffer: pointer, bufLen: int) =
+
+  proc hsWriteData(s: Stream, buffer: pointer, bufLen: int) =
+    var s = FileHandleStream(s)
     var bytesWritten: int32
-    var a = winlean.writeFile(s.handle, buffer, bufLen, bytesWritten, nil)
-    if a == 0: OSError()
+    var a = winlean.writeFile(s.handle, buffer, bufLen.cint,
+                              addr bytesWritten, nil)
+    if a == 0: raiseOSError(osLastError())
 
-  proc newFileHandleStream(handle: THandle): PFileHandleStream =
-    new(result)
-    result.handle = handle
-    result.close = hsClose
-    result.atEnd = hsAtEnd
-    result.readData = hsReadData
-    result.writeData = hsWriteData
-    
-  proc buildCommandLine(a: string, args: openarray[string]): cstring =
-    var res = quoteIfContainsWhite(a)
-    for i in 0..high(args): 
-      res.add(' ')
-      res.add(quoteIfContainsWhite(args[i]))
-    result = cast[cstring](alloc0(res.len+1))
-    copyMem(result, cstring(res), res.len)
-
-  proc buildEnv(env: PStringTable): cstring =
+  proc newFileHandleStream(handle: Handle): owned FileHandleStream =
+    result = FileHandleStream(handle: handle, closeImpl: hsClose, atEndImpl: hsAtEnd,
+      readDataImpl: hsReadData, writeDataImpl: hsWriteData)
+
+  proc buildCommandLine(a: string, args: openArray[string]): string =
+    result = quoteShell(a)
+    for i in 0..high(args):
+      result.add(' ')
+      result.add(quoteShell(args[i]))
+
+  proc buildEnv(env: StringTableRef): tuple[str: cstring, len: int] =
     var L = 0
     for key, val in pairs(env): inc(L, key.len + val.len + 2)
-    result = cast[cstring](alloc0(L+2))
+    var str = cast[cstring](alloc0(L+2))
     L = 0
     for key, val in pairs(env):
       var x = key & "=" & val
-      copyMem(addr(result[L]), cstring(x), x.len+1) # copy \0
+      copyMem(addr(str[L]), cstring(x), x.len+1) # copy \0
       inc(L, x.len+1)
+    (str, L)
 
-  #proc open_osfhandle(osh: THandle, mode: int): int {.
+  #proc open_osfhandle(osh: Handle, mode: int): int {.
   #  importc: "_open_osfhandle", header: "<fcntl.h>".}
 
   #var
   #  O_WRONLY {.importc: "_O_WRONLY", header: "<fcntl.h>".}: int
   #  O_RDONLY {.importc: "_O_RDONLY", header: "<fcntl.h>".}: int
-
-  proc CreatePipeHandles(Rdhandle, WrHandle: var THandle) =
-    var piInheritablePipe: TSecurityAttributes
-    piInheritablePipe.nlength = SizeOF(TSecurityAttributes)
-    piInheritablePipe.lpSecurityDescriptor = nil
-    piInheritablePipe.Binherithandle = 1
-    if CreatePipe(Rdhandle, Wrhandle, piInheritablePipe, 1024) == 0'i32:
-      OSError()
-
-  proc fileClose(h: THandle) {.inline.} =
-    if h > 4: discard CloseHandle(h)
-
-  proc startProcess(command: string,
-                 workingDir: string = "",
-                 args: openarray[string] = [],
-                 env: PStringTable = nil,
-                 options: set[TProcessOption] = {poStdErrToStdOut}): PProcess =
+  proc myDup(h: Handle; inherit: WINBOOL = 1): Handle =
+    let thisProc = getCurrentProcess()
+    if duplicateHandle(thisProc, h, thisProc, addr result, 0, inherit,
+                       DUPLICATE_SAME_ACCESS) == 0:
+      raiseOSError(osLastError())
+
+  proc createAllPipeHandles(si: var STARTUPINFO;
+                            stdin, stdout, stderr: var Handle; hash: int) =
+    var sa: SECURITY_ATTRIBUTES
+    sa.nLength = sizeof(SECURITY_ATTRIBUTES).cint
+    sa.lpSecurityDescriptor = nil
+    sa.bInheritHandle = 1
+    let pipeOutName = newWideCString(r"\\.\pipe\stdout" & $hash)
+    let pipeInName = newWideCString(r"\\.\pipe\stdin" & $hash)
+    let pipeOut = createNamedPipe(pipeOutName,
+      dwOpenMode = PIPE_ACCESS_INBOUND or FILE_FLAG_WRITE_THROUGH,
+      dwPipeMode = PIPE_NOWAIT,
+      nMaxInstances = 1,
+      nOutBufferSize = 1024, nInBufferSize = 1024,
+      nDefaultTimeOut = 0, addr sa)
+    if pipeOut == INVALID_HANDLE_VALUE:
+      raiseOSError(osLastError())
+    let pipeIn = createNamedPipe(pipeInName,
+      dwOpenMode = PIPE_ACCESS_OUTBOUND or FILE_FLAG_WRITE_THROUGH,
+      dwPipeMode = PIPE_NOWAIT,
+      nMaxInstances = 1,
+      nOutBufferSize = 1024, nInBufferSize = 1024,
+      nDefaultTimeOut = 0, addr sa)
+    if pipeIn == INVALID_HANDLE_VALUE:
+      raiseOSError(osLastError())
+
+    si.hStdOutput = createFileW(pipeOutName,
+        FILE_WRITE_DATA or SYNCHRONIZE, 0, addr sa,
+        OPEN_EXISTING, # 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: TStartupInfo
-      ProcInfo: TProcessInformation
+      si: STARTUPINFO
+      procInfo: PROCESS_INFORMATION
       success: int
-      hi, ho, he: THandle
+      hi, ho, he: Handle
     new(result)
-    SI.cb = SizeOf(SI)
+    result.options = options
+    result.exitFlag = true
+    si.cb = sizeof(si).cint
     if poParentStreams notin options:
-      SI.dwFlags = STARTF_USESTDHANDLES # STARTF_USESHOWWINDOW or
-      CreatePipeHandles(SI.hStdInput, HI)
-      CreatePipeHandles(HO, Si.hStdOutput)
-      if poStdErrToStdOut in options:
-        SI.hStdError = SI.hStdOutput
-        HE = HO
+      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:
-        CreatePipeHandles(HE, Si.hStdError)
-      result.inputHandle = hi
-      result.outputHandle = ho
-      result.errorHandle = he
+        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.inputHandle = si.hStdInput
-      result.outputHandle = si.hStdOutput
-      result.errorHandle = si.hStdError
-    
+      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
-    if false: # poUseShell in options:
-      cmdl = buildCommandLine(getEnv("COMSPEC"), @["/c", command] & args)
+    var cmdRoot: string
+    if poEvalCommand in options:
+      cmdl = command
+      assert args.len == 0
     else:
-      cmdl = buildCommandLine(command, args)
+      cmdRoot = buildCommandLine(command, args)
+      cmdl = cstring(cmdRoot)
     var wd: cstring = nil
-    var e: 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)
-    success = winlean.CreateProcess(nil,
-      cmdl, nil, nil, 1, NORMAL_PRIORITY_CLASS, e, wd, SI, ProcInfo)
-    
+    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)
+    let lastError = osLastError()
+
     if poParentStreams notin options:
-      FileClose(si.hStdInput)
-      FileClose(si.hStdOutput)
+      fileClose(si.hStdInput)
+      fileClose(si.hStdOutput)
       if poStdErrToStdOut notin options:
-        FileClose(si.hStdError)
-      
-    if e != nil: dealloc(e)
-    dealloc(cmdl)
-    if success == 0: OSError()
-    # Close the handle now so anyone waiting is woken:
-    discard closeHandle(procInfo.hThread)
-    result.FProcessHandle = procInfo.hProcess
-    result.id = procInfo.dwProcessID
-
-  proc suspend(p: PProcess) =
-    discard SuspendThread(p.FProcessHandle)
-
-  proc resume(p: PProcess) =
-    discard ResumeThread(p.FProcessHandle)
-
-  proc running(p: PProcess): bool =
-    var x = waitForSingleObject(p.FProcessHandle, 50)
-    return x == WAIT_TIMEOUT
-
-  proc terminate(p: PProcess) =
-    if running(p):
-      discard TerminateProcess(p.FProcessHandle, 0)
+        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: '" & command & "'. OS error:")
+      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
 
-  proc waitForExit(p: PProcess): int =
-    discard WaitForSingleObject(p.FProcessHandle, Infinite)
-    var res: int32
-    discard GetExitCodeProcess(p.FProcessHandle, res)
-    result = res
-    discard CloseHandle(p.FProcessHandle)
+      if p.outHandle != p.errHandle:
+        p.errHandle.fileClose()
+      p.outHandle.fileClose()
+    p.closeThreadAndProcessHandle()
 
-  proc inputStream(p: PProcess): PStream =
-    result = newFileHandleStream(p.inputHandle)
+  proc suspend(p: Process) =
+    discard suspendThread(p.fThreadHandle)
 
-  proc outputStream(p: PProcess): PStream =
-    result = newFileHandleStream(p.outputHandle)
+  proc resume(p: Process) =
+    discard resumeThread(p.fThreadHandle)
 
-  proc errorStream(p: PProcess): PStream =
-    result = newFileHandleStream(p.errorHandle)
+  proc running(p: Process): bool =
+    if p.exitFlag:
+      return false
+    else:
+      var x = waitForSingleObject(p.fProcessHandle, 0)
+      return x == WAIT_TIMEOUT
 
-  proc execCmd(command: string): int = 
+  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: TStartupInfo
-      ProcInfo: TProcessInformation
-      process: THandle
+      si: STARTUPINFO
+      procInfo: PROCESS_INFORMATION
+      process: Handle
       L: int32
-    SI.cb = SizeOf(SI)
-    SI.hStdError = GetStdHandle(STD_ERROR_HANDLE)
-    SI.hStdInput = GetStdHandle(STD_INPUT_HANDLE)
-    SI.hStdOutput = GetStdHandle(STD_OUTPUT_HANDLE)
-    if winlean.CreateProcess(nil, command, nil, nil, 0,
-        NORMAL_PRIORITY_CLASS, nil, nil, SI, ProcInfo) == 0:
-      OSError()
+    si.cb = sizeof(si).cint
+    si.hStdError = getStdHandle(STD_ERROR_HANDLE)
+    si.hStdInput = getStdHandle(STD_INPUT_HANDLE)
+    si.hStdOutput = getStdHandle(STD_OUTPUT_HANDLE)
+    var c = newWideCString(command)
+    var res = winlean.createProcessW(nil, c, 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)
+      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)
+      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
 
-else:
+  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 addCmdArgs(command: string, args: openarray[string]): string =
-    result = quoteIfContainsWhite(command)
-    for i in 0 .. high(args):
-      add(result, " ")
-      add(result, quoteIfContainsWhite(args[i]))
-
-  proc toCStringArray(b, a: openarray[string]): cstringArray =
-    result = cast[cstringArray](alloc0((a.len + b.len + 1) * sizeof(cstring)))
-    for i in 0..high(b):
-      result[i] = cast[cstring](alloc(b[i].len+1))
-      copyMem(result[i], cstring(b[i]), b[i].len+1)
-    for i in 0..high(a):
-      result[i+b.len] = cast[cstring](alloc(a[i].len+1))
-      copyMem(result[i+b.len], cstring(a[i]), a[i].len+1)
-
-  proc ToCStringArray(t: PStringTable): cstringArray =
+  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):
@@ -434,110 +922,660 @@ else:
       copyMem(result[i], addr(x[0]), x.len+1)
       inc(i)
 
-  proc startProcess(command: string,
-                 workingDir: string = "",
-                 args: openarray[string] = [],
-                 env: PStringTable = nil,
-                 options: set[TProcessOption] = {poStdErrToStdOut}): PProcess =
+  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 {.
+      raises: [OSError], tags: [ExecIOEffect, ReadEnvEffect, ReadDirEffect, RootEffect], gcsafe.}
+  else:
+    proc startProcessAuxFork(data: StartProcessData): Pid {.
+      raises: [OSError], tags: [ExecIOEffect, ReadEnvEffect, ReadDirEffect, RootEffect], gcsafe.}
+    {.push stacktrace: off, profiler: off.}
+    proc startProcessAfterFork(data: ptr StartProcessData) {.
+      raises: [OSError], 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
-      p_stdin, p_stdout, p_stderr: array [0..1, cint]
+      pStdin, pStdout, pStderr: array[0..1, cint]
     new(result)
-    result.exitCode = 3 # for ``waitForExit``
-    if pipe(p_stdin) != 0'i32 or pipe(p_stdout) != 0'i32:
-      OSError("failed to create a pipe")
-    var Pid = fork()
-    if Pid < 0:
-      OSError("failed to fork process")
-
-    if pid == 0:
-      ## child process:
-      discard close(p_stdin[writeIdx])
-      if dup2(p_stdin[readIdx], readIdx) < 0: OSError()
-      discard close(p_stdout[readIdx])
-      if dup2(p_stdout[writeIdx], writeIdx) < 0: OSError()
-      if poStdErrToStdOut in options:
-        if dup2(p_stdout[writeIdx], 2) < 0: OSError()
+    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:
-        if pipe(p_stderr) != 0'i32: OSError("failed to create a pipe")
-        discard close(p_stderr[readIdx])
-        if dup2(p_stderr[writeIdx], 2) < 0: OSError()
+        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:
-        os.setCurrentDir(workingDir)
-      if poUseShell notin options:
-        var a = toCStringArray([extractFilename(command)], args)
-        if env == nil:
-          discard execv(command, a)
-        else:
-          discard execve(command, a, ToCStringArray(env))
-      else:
-        var x = addCmdArgs(command, args)
-        var a = toCStringArray(["sh", "-c"], [x])
-        if env == nil:
-          discard execv("/bin/sh", a)
-        else:
-          discard execve("/bin/sh", a, ToCStringArray(env))
-      # too risky to raise an exception here:
-      quit("execve call failed: " & $strerror(errno))
+        setCurrentDir(currentDir)
+    else:
+      pid = startProcessAuxFork(data)
+
     # Parent process. Copy process information.
     if poEchoCmd in options:
-      echo(command & " " & join(args, " "))
+      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)
+      when not defined(nuttx):
+        if poDaemon in data.options:
+          chck posix_spawnattr_setpgroup(attr, 0'i32)
+
+      var flags = POSIX_SPAWN_USEVFORK or
+                  POSIX_SPAWN_SETSIGMASK
+      when not defined(nuttx):
+        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(OSErrorCode(error),
+                      "Could not find command: '" & $data.sysCommand & "'. OS error: " & $strerror(error))
+
+      return pid
+
+    {.push stacktrace: off, profiler: off.}
+    proc startProcessFail(data: ptr StartProcessData, 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
+          var exe: string
+          try:
+            exe = findExe(data.sysCommand)
+          except OSError as e:
+            startProcessFail(data, e.errorCode)
+          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 std/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:
+            raiseAssert "unreachable!"
+
+      result = exitStatusLikeShell(p.exitStatus)
+
+  else:
+    import std/times except getTime
+    import std/monotimes
+
+    proc waitForExit(p: Process, timeout: int = -1): int =
+      if p.exitFlag:
+        return exitStatusLikeShell(p.exitStatus)
+
+      if timeout < 0:
+        # Backwards compatibility with previous verison to
+        # handle cases where timeout == -1, but extend
+        # to handle cases where timeout < 0
+        var status: cint
+        if waitpid(p.id, status, 0) < 0:
+          raiseOSError(osLastError())
+        p.exitFlag = true
+        p.exitStatus = status
+      else:
+        # Max 50ms delay
+        const maxWait = initDuration(milliseconds = 50)
+        let wait = initDuration(milliseconds = timeout)
+        let deadline = getMonoTime() + wait
+        # starting 50μs delay
+        var delay = initDuration(microseconds = 50)
+        
+        while true:
+          var status: cint
+          let pid = waitpid(p.id, status, WNOHANG)
+          if p.id == pid :
+            p.exitFlag = true
+            p.exitStatus = status
+            break
+          elif pid.int == -1:
+            raiseOsError(osLastError())
+          else:
+            # Continue waiting if needed
+            if getMonoTime() >= deadline:
+              # Previous version of `waitForExit`
+              # foricibly killed the process.
+              # We keep this so we don't break programs
+              # that depend on this behavior
+              if posix.kill(p.id, SIGKILL) < 0:
+                raiseOSError(osLastError())
+            else:
+              const max = 1_000_000_000
+              let 
+                newWait = getMonoTime() + delay
+                ticks = newWait.ticks()
+                ns = ticks mod max
+                secs = ticks div max
+              var 
+                waitSpec: TimeSpec
+                unused: Timespec
+              waitSpec.tv_sec = posix.Time(secs)
+              waitSpec.tv_nsec = clong ns 
+              discard posix.clock_nanosleep(CLOCK_MONOTONIC, TIMER_ABSTIME, waitSpec, unused)
+              let remaining = deadline - getMonoTime()
+              delay = min([delay * 2, remaining, maxWait])
+
+      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: "<stdlib.h>".}
+
+  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] {.raises: [OSError, IOError], 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:
+  ##   ```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)
 
-    result.inputHandle = p_stdin[writeIdx]
-    result.outputHandle = p_stdout[readIdx]
-    if poStdErrToStdOut in options:
-      result.errorHandle = result.outputHandle
+  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.errorHandle = p_stderr[readIdx]
-      discard close(p_stderr[writeIdx])
-    discard close(p_stdin[readIdx])
-    discard close(p_stdout[writeIdx])
-
-  proc suspend(p: PProcess) =
-    discard kill(p.id, SIGSTOP)
-
-  proc resume(p: PProcess) =
-    discard kill(p.id, SIGCONT)
-
-  proc running(p: PProcess): bool =
-    result = waitPid(p.id, p.exitCode, WNOHANG) == int(p.id)
-
-  proc terminate(p: PProcess) =
-    if kill(p.id, SIGTERM) == 0'i32:
-      if running(p): discard kill(p.id, SIGKILL)
-
-  proc waitForExit(p: PProcess): int =
-    #if waitPid(p.id, p.exitCode, 0) == int(p.id):
-    # ``waitPid`` fails if the process is not running anymore. But then
-    # ``running`` probably set ``p.exitCode`` for us. Since ``p.exitCode`` is
-    # initialized with 3, wrong success exit codes are prevented.
-    var oldExitCode = p.exitCode
-    if waitPid(p.id, p.exitCode, 0) < 0:
-      # failed, so restore old exitCode
-      p.exitCode = oldExitCode
-    result = int(p.exitCode)
-
-  proc inputStream(p: PProcess): PStream =
-    var f: TFile
-    if not open(f, p.inputHandle, fmWrite): OSError()
-    result = newFileStream(f)
-
-  proc outputStream(p: PProcess): PStream =
-    var f: TFile
-    if not open(f, p.outputHandle, fmRead): OSError()
-    result = newFileStream(f)
-
-  proc errorStream(p: PProcess): PStream =
-    var f: TFile
-    if not open(f, p.errorHandle, fmRead): OSError()
-    result = newFileStream(f)
-
-  proc csystem(cmd: cstring): cint {.nodecl, importc: "system".}
-
-  proc execCmd(command: string): int = 
-    result = csystem(command)
-
-when isMainModule:
-  var x = execProcess("gcc -v")
-  echo "ECHO ", x
+      result[1] = peekExitCode(p)
+      if result[1] != -1: break
+  close(p)