@@ -6,11 +6,12 @@ mutable struct Process <: AbstractPipe
6
6
in:: IO
7
7
out:: IO
8
8
err:: IO
9
+ syncd:: Vector{Task}
9
10
exitcode:: Int64
10
11
termsignal:: Int32
11
12
exitnotify:: ThreadSynchronizer
12
- function Process (cmd:: Cmd , handle:: Ptr{Cvoid} )
13
- this = new (cmd, handle, devnull , devnull , devnull ,
13
+ function Process (cmd:: Cmd , handle:: Ptr{Cvoid} , syncd :: Vector{Task} )
14
+ this = new (cmd, handle, devnull , devnull , devnull , syncd,
14
15
typemin (fieldtype (Process, :exitcode )),
15
16
typemin (fieldtype (Process, :termsignal )),
16
17
ThreadSynchronizer ())
35
36
pipe_reader (p:: ProcessChain ) = p. out
36
37
pipe_writer (p:: ProcessChain ) = p. in
37
38
39
+ # a lightweight pair of a child OS_HANDLE and associated Task that will
40
+ # complete only after all content has been read from it for synchronizing
41
+ # state without the kernel to aide
42
+ struct SyncCloseFD
43
+ fd
44
+ t:: Task
45
+ end
46
+ rawhandle (io:: SyncCloseFD ) = rawhandle (io. fd)
47
+
38
48
# release ownership of the libuv handle
39
49
function uvfinalize (proc:: Process )
40
50
if proc. handle != C_NULL
@@ -74,8 +84,8 @@ function _uv_hook_close(proc::Process)
74
84
nothing
75
85
end
76
86
77
- const SpawnIO = Union{IO, RawFD, OS_HANDLE}
78
- const SpawnIOs = Vector {SpawnIO} # convenience name for readability
87
+ const SpawnIO = Union{IO, RawFD, OS_HANDLE, SyncCloseFD} # internal copy of Redirectable, removing FileRedirect and adding SyncCloseFD
88
+ const SpawnIOs = Memory {SpawnIO} # convenience name for readability (used for dispatch also to clearly distinguish from Vector{Redirectable})
79
89
80
90
function as_cpumask (cpus:: Vector{UInt16} )
81
91
n = max (Int (maximum (cpus)), Int (ccall (:uv_cpumask_size , Cint, ())))
100
110
error (" invalid spawn handle $h from $io " )
101
111
end
102
112
for io in stdio]
113
+ syncd = Task[io. t for io in stdio if io isa SyncCloseFD]
103
114
handle = Libc. malloc (_sizeof_uv_process)
104
115
disassociate_julia_struct (handle)
105
116
(; exec, flags, env, dir) = cmd
117
128
cpumask === nothing ? 0 : length (cpumask),
118
129
@cfunction (uv_return_spawn, Cvoid, (Ptr{Cvoid}, Int64, Int32)))
119
130
if err == 0
120
- pp = Process (cmd, handle)
131
+ pp = Process (cmd, handle, syncd )
121
132
associate_julia_struct (handle, pp)
122
133
else
123
134
ccall (:jl_forceclose_uv , Cvoid, (Ptr{Cvoid},), handle) # will call free on handle eventually
@@ -130,23 +141,24 @@ end
130
141
return pp
131
142
end
132
143
133
- _spawn (cmds:: AbstractCmd ) = _spawn (cmds, SpawnIO[] )
144
+ _spawn (cmds:: AbstractCmd ) = _spawn (cmds, SpawnIOs () )
134
145
135
- # optimization: we can spawn `Cmd` directly without allocating the ProcessChain
136
- function _spawn (cmd:: Cmd , stdios:: SpawnIOs )
137
- isempty (cmd. exec) && throw (ArgumentError (" cannot spawn empty command" ))
146
+ function _spawn (cmd:: AbstractCmd , stdios:: Vector{Redirectable} )
138
147
pp = setup_stdios (stdios) do stdios
139
- return _spawn_primitive (cmd . exec[ 1 ], cmd, stdios)
148
+ return _spawn ( cmd, stdios)
140
149
end
141
150
return pp
142
151
end
143
152
153
+ # optimization: we can spawn `Cmd` directly without allocating the ProcessChain
154
+ function _spawn (cmd:: Cmd , stdios:: SpawnIOs )
155
+ isempty (cmd. exec) && throw (ArgumentError (" cannot spawn empty command" ))
156
+ return _spawn_primitive (cmd. exec[1 ], cmd, stdios)
157
+ end
158
+
144
159
# assume that having a ProcessChain means that the stdio are setup
145
160
function _spawn (cmds:: AbstractCmd , stdios:: SpawnIOs )
146
- pp = setup_stdios (stdios) do stdios
147
- return _spawn (cmds, stdios, ProcessChain ())
148
- end
149
- return pp
161
+ return _spawn (cmds, stdios, ProcessChain ())
150
162
end
151
163
152
164
# helper function for making a copy of a SpawnIOs, with replacement
212
224
213
225
214
226
# open the child end of each element of `stdios`, and initialize the parent end
215
- function setup_stdios (f, stdios:: SpawnIOs )
227
+ function setup_stdios (f, stdios:: Vector{Redirectable} )
216
228
nstdio = length (stdios)
217
229
open_io = SpawnIOs (undef, nstdio)
218
230
close_io = falses (nstdio)
@@ -295,25 +307,26 @@ function setup_stdio(stdio::IO, child_readable::Bool)
295
307
child = child_readable ? rd : wr
296
308
try
297
309
let in = (child_readable ? parent : stdio),
298
- out = (child_readable ? stdio : parent)
299
- @async try
310
+ out = (child_readable ? stdio : parent),
311
+ t = @async try
300
312
write (in, out)
301
313
catch ex
302
314
@warn " Process I/O error" exception= (ex, catch_backtrace ())
315
+ rethrow ()
303
316
finally
304
317
close (parent)
305
- child_readable || closewrite (stdio)
306
318
end
319
+ return (SyncCloseFD (child, t), true )
307
320
end
308
321
catch
309
322
close_pipe_sync (child)
310
323
rethrow ()
311
324
end
312
- return (child, true )
313
325
end
314
326
315
- close_stdio (stdio:: OS_HANDLE ) = close_pipe_sync (stdio)
316
327
close_stdio (stdio) = close (stdio)
328
+ close_stdio (stdio:: OS_HANDLE ) = close_pipe_sync (stdio)
329
+ close_stdio (stdio:: SyncCloseFD ) = close_stdio (stdio. fd)
317
330
318
331
# INTERNAL
319
332
# pad out stdio to have at least three elements,
@@ -325,19 +338,19 @@ close_stdio(stdio) = close(stdio)
325
338
# - An Filesystem.File or IOStream object to redirect the output to
326
339
# - A FileRedirect, containing a string specifying a filename to be opened for the child
327
340
328
- spawn_opts_swallow (stdios:: StdIOSet ) = SpawnIO [stdios... ]
329
- spawn_opts_inherit (stdios:: StdIOSet ) = SpawnIO [stdios... ]
341
+ spawn_opts_swallow (stdios:: StdIOSet ) = Redirectable [stdios... ]
342
+ spawn_opts_inherit (stdios:: StdIOSet ) = Redirectable [stdios... ]
330
343
spawn_opts_swallow (in:: Redirectable = devnull , out:: Redirectable = devnull , err:: Redirectable = devnull ) =
331
- SpawnIO [in, out, err]
344
+ Redirectable [in, out, err]
332
345
# pass original descriptors to child processes by default, because we might
333
346
# have already exhausted and closed the libuv object for our standard streams.
334
347
# ref issue #8529
335
348
spawn_opts_inherit (in:: Redirectable = RawFD (0 ), out:: Redirectable = RawFD (1 ), err:: Redirectable = RawFD (2 )) =
336
- SpawnIO [in, out, err]
349
+ Redirectable [in, out, err]
337
350
338
351
function eachline (cmd:: AbstractCmd ; keep:: Bool = false )
339
352
out = PipeEndpoint ()
340
- processes = _spawn (cmd, SpawnIO [devnull , out, stderr ])
353
+ processes = _spawn (cmd, Redirectable [devnull , out, stderr ])
341
354
# if the user consumes all the data, also check process exit status for success
342
355
ondone = () -> (success (processes) || pipeline_error (processes); nothing )
343
356
return EachLine (out, keep= keep, ondone= ondone):: EachLine
@@ -385,20 +398,20 @@ function open(cmds::AbstractCmd, stdio::Redirectable=devnull; write::Bool=false,
385
398
stdio === devnull || throw (ArgumentError (" no stream can be specified for `stdio` in read-write mode" ))
386
399
in = PipeEndpoint ()
387
400
out = PipeEndpoint ()
388
- processes = _spawn (cmds, SpawnIO [in, out, stderr ])
401
+ processes = _spawn (cmds, Redirectable [in, out, stderr ])
389
402
processes. in = in
390
403
processes. out = out
391
404
elseif read
392
405
out = PipeEndpoint ()
393
- processes = _spawn (cmds, SpawnIO [stdio, out, stderr ])
406
+ processes = _spawn (cmds, Redirectable [stdio, out, stderr ])
394
407
processes. out = out
395
408
elseif write
396
409
in = PipeEndpoint ()
397
- processes = _spawn (cmds, SpawnIO [in, stdio, stderr ])
410
+ processes = _spawn (cmds, Redirectable [in, stdio, stderr ])
398
411
processes. in = in
399
412
else
400
413
stdio === devnull || throw (ArgumentError (" no stream can be specified for `stdio` in no-access mode" ))
401
- processes = _spawn (cmds, SpawnIO [devnull , devnull , stderr ])
414
+ processes = _spawn (cmds, Redirectable [devnull , devnull , stderr ])
402
415
end
403
416
return processes
404
417
end
@@ -415,12 +428,18 @@ function open(f::Function, cmds::AbstractCmd, args...; kwargs...)
415
428
P = open (cmds, args... ; kwargs... )
416
429
function waitkill (P:: Union{Process,ProcessChain} )
417
430
close (P)
418
- # 0.1 seconds after we hope it dies (from closing stdio),
419
- # we kill the process with SIGTERM (15)
420
- local t = Timer (0.1 ) do t
431
+ # shortly after we hope it starts cleanup and dies (from closing
432
+ # stdio), we kill the process with SIGTERM (15) so that we can proceed
433
+ # with throwing the error and hope it will exit soon from that
434
+ local t = Timer (2 ) do t
421
435
process_running (P) && kill (P)
422
436
end
423
- wait (P)
437
+ # pass false to indicate that we do not care about data-races on the
438
+ # Julia stdio objects after this point, since we already know this is
439
+ # an error path and the state of them is fairly unpredictable anyways
440
+ # in that case. Since we closed P some of those should come crumbling
441
+ # down already, and we don't want to throw that error here either.
442
+ wait (P, false )
424
443
close (t)
425
444
end
426
445
ret = try
@@ -430,10 +449,23 @@ function open(f::Function, cmds::AbstractCmd, args...; kwargs...)
430
449
rethrow ()
431
450
end
432
451
close (P. in)
452
+ closestdio = @async begin
453
+ # wait for P to complete (including sync'd), then mark the output streams for EOF (if applicable to that stream type)
454
+ wait (P)
455
+ err = P. err
456
+ applicable (closewrite, err) && closewrite (err)
457
+ out = P. out
458
+ applicable (closewrite, out) && closewrite (out)
459
+ nothing
460
+ end
461
+ # now verify that the output stream is at EOF, and the user didn't fail to consume it successfully
462
+ # (we do not currently verify the user dealt with the stderr stream)
433
463
if ! (eof (P. out):: Bool )
434
464
waitkill (P)
435
465
throw (_UVError (" open(do)" , UV_EPIPE))
436
466
end
467
+ # make sure to closestdio is completely done to avoid data-races later
468
+ wait (closestdio)
437
469
success (P) || pipeline_error (P)
438
470
return ret
439
471
end
@@ -650,26 +682,31 @@ function process_status(s::Process)
650
682
error (" process status error" )
651
683
end
652
684
653
- function wait (x:: Process )
654
- process_exited (x) && return
655
- iolock_begin ()
685
+ function wait (x:: Process , syncd:: Bool = true )
656
686
if ! process_exited (x)
657
- preserve_handle (x)
658
- lock (x. exitnotify)
659
- iolock_end ()
660
- try
661
- wait (x. exitnotify)
662
- finally
663
- unlock (x. exitnotify)
664
- unpreserve_handle (x)
687
+ iolock_begin ()
688
+ if ! process_exited (x)
689
+ preserve_handle (x)
690
+ lock (x. exitnotify)
691
+ iolock_end ()
692
+ try
693
+ wait (x. exitnotify)
694
+ finally
695
+ unlock (x. exitnotify)
696
+ unpreserve_handle (x)
697
+ end
698
+ else
699
+ iolock_end ()
665
700
end
666
- else
667
- iolock_end ()
701
+ end
702
+ # and make sure all sync'd Tasks are complete too
703
+ syncd && for t in x. syncd
704
+ wait (t)
668
705
end
669
706
nothing
670
707
end
671
708
672
- wait (x:: ProcessChain ) = foreach (wait, x. processes)
709
+ wait (x:: ProcessChain , syncd :: Bool = true ) = foreach (p -> wait (p, syncd) , x. processes)
673
710
674
711
show (io:: IO , p:: Process ) = print (io, " Process(" , p. cmd, " , " , process_status (p), " )" )
675
712
0 commit comments