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--lib/pure/osproc.nim130
1 files changed, 105 insertions, 25 deletions
diff --git a/lib/pure/osproc.nim b/lib/pure/osproc.nim
index 7431be702..454f9eda0 100644
--- a/lib/pure/osproc.nim
+++ b/lib/pure/osproc.nim
@@ -24,6 +24,20 @@ when defined(linux):
   import linux
 
 type
+  ProcessOption* = enum ## options that can be passed `startProcess`
+    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 trused 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.
+
   ProcessObj = object of RootObj
     when defined(windows):
       fProcessHandle: Handle
@@ -34,18 +48,10 @@ type
       inStream, outStream, errStream: Stream
       id: Pid
     exitCode: cint
+    options: set[ProcessOption]
 
   Process* = ref ProcessObj ## represents an operating system process
 
-  ProcessOption* = enum ## options that can be passed `startProcess`
-    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 trused source.
-    poStdErrToStdOut,    ## merge stdout and stderr to the stdout stream
-    poParentStreams      ## use the parent's streams
 
 {.deprecated: [TProcess: ProcessObj, PProcess: Process,
   TProcessOption: ProcessOption].}
@@ -302,7 +308,7 @@ proc execProcesses*(cmds: openArray[string],
       result = max(waitForExit(p), result)
       close(p)
 
-proc select*(readfds: var seq[Process], timeout = 500): int
+proc select*(readfds: var seq[Process], timeout = 500): int {.benign.}
   ## `select` with a sensible Nim interface. `timeout` is in milliseconds.
   ## Specify -1 for no timeout. Returns the number of processes that are
   ## ready to read from. The processes that are ready to be read from are
@@ -394,13 +400,68 @@ when defined(Windows) and not defined(useNimRtl):
   #var
   #  O_WRONLY {.importc: "_O_WRONLY", header: "<fcntl.h>".}: int
   #  O_RDONLY {.importc: "_O_RDONLY", header: "<fcntl.h>".}: int
+  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) =
+    var sa: SECURITY_ATTRIBUTES
+    sa.nLength = sizeof(SECURITY_ATTRIBUTES).cint
+    sa.lpSecurityDescriptor = nil
+    sa.bInheritHandle = 1
+    let pipeOutName = newWideCString(r"\\.\pipe\stdout")
+    let pipeInName = newWideCString(r"\\.\pipe\stdin")
+    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.hStdOutput == INVALID_HANDLE_VALUE:
+      raiseOSError(osLastError())
+
+    stdin = myDup(pipeIn, 0)
+    stdout = myDup(pipeOut, 0)
+    discard closeHandle(pipeIn)
+    discard closeHandle(pipeOut)
+    stderr = stdout
 
   proc createPipeHandles(rdHandle, wrHandle: var Handle) =
-    var piInheritablePipe: SECURITY_ATTRIBUTES
-    piInheritablePipe.nLength = sizeof(SECURITY_ATTRIBUTES).cint
-    piInheritablePipe.lpSecurityDescriptor = nil
-    piInheritablePipe.bInheritHandle = 1
-    if createPipe(rdHandle, wrHandle, piInheritablePipe, 1024) == 0'i32:
+    var sa: SECURITY_ATTRIBUTES
+    sa.nLength = sizeof(SECURITY_ATTRIBUTES).cint
+    sa.lpSecurityDescriptor = nil
+    sa.bInheritHandle = 1
+    if createPipe(rdHandle, wrHandle, sa, 1024) == 0'i32:
       raiseOSError(osLastError())
 
   proc fileClose(h: Handle) {.inline.} =
@@ -417,16 +478,20 @@ when defined(Windows) and not defined(useNimRtl):
       success: int
       hi, ho, he: Handle
     new(result)
+    result.options = options
     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
+      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)
       else:
-        createPipeHandles(he, si.hStdError)
+        createAllPipeHandles(si, hi, ho, he)
       result.inHandle = FileHandle(hi)
       result.outHandle = FileHandle(ho)
       result.errHandle = FileHandle(he)
@@ -482,12 +547,12 @@ when defined(Windows) and not defined(useNimRtl):
     result.id = procInfo.dwProcessId
 
   proc close(p: Process) =
-    when false:
-      # somehow this does not work on Windows:
+    if poInteractive in p.options:
+      # somehow this is not always required on Windows:
       discard closeHandle(p.inHandle)
       discard closeHandle(p.outHandle)
       discard closeHandle(p.errHandle)
-      discard closeHandle(p.FProcessHandle)
+      #discard closeHandle(p.FProcessHandle)
 
   proc suspend(p: Process) =
     discard suspendThread(p.fProcessHandle)
@@ -564,7 +629,7 @@ when defined(Windows) and not defined(useNimRtl):
     assert readfds.len <= MAXIMUM_WAIT_OBJECTS
     var rfds: WOHandleArray
     for i in 0..readfds.len()-1:
-      rfds[i] = readfds[i].fProcessHandle
+      rfds[i] = readfds[i].outHandle #fProcessHandle
 
     var ret = waitForMultipleObjects(readfds.len.int32,
                                      addr(rfds), 0'i32, timeout.int32)
@@ -578,6 +643,11 @@ when defined(Windows) and not defined(useNimRtl):
       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
@@ -635,6 +705,7 @@ elif not defined(useNimRtl):
     var
       pStdin, pStdout, pStderr: array [0..1, cint]
     new(result)
+    result.options = options
     result.exitCode = -3 # for ``waitForExit``
     if poParentStreams notin options:
       if pipe(pStdin) != 0'i32 or pipe(pStdout) != 0'i32 or
@@ -960,6 +1031,15 @@ elif not defined(useNimRtl):
 
     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}): tuple[