• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

JuliaLang / julia / #38002

06 Feb 2025 06:14AM UTC coverage: 20.322% (-2.4%) from 22.722%
#38002

push

local

web-flow
bpart: Fully switch to partitioned semantics (#57253)

This is the final PR in the binding partitions series (modulo bugs and
tweaks), i.e. it closes #54654 and thus closes #40399, which was the
original design sketch.

This thus activates the full designed semantics for binding partitions,
in particular allowing safe replacement of const bindings. It in
particular allows struct redefinitions. This thus closes
timholy/Revise.jl#18 and also closes #38584.

The biggest semantic change here is probably that this gets rid of the
notion of "resolvedness" of a binding. Previously, a lot of the behavior
of our implementation depended on when bindings were "resolved", which
could happen at basically an arbitrary point (in the compiler, in REPL
completion, in a different thread), making a lot of the semantics around
bindings ill- or at least implementation-defined. There are several
related issues in the bugtracker, so this closes #14055 closes #44604
closes #46354 closes #30277

It is also the last step to close #24569.
It also supports bindings for undef->defined transitions and thus closes
#53958 closes #54733 - however, this is not activated yet for
performance reasons and may need some further optimization.

Since resolvedness no longer exists, we need to replace it with some
hopefully more well-defined semantics. I will describe the semantics
below, but before I do I will make two notes:

1. There are a number of cases where these semantics will behave
slightly differently than the old semantics absent some other task going
around resolving random bindings.
2. The new behavior (except for the replacement stuff) was generally
permissible under the old semantics if the bindings happened to be
resolved at the right time.

With all that said, there are essentially three "strengths" of bindings:

1. Implicit Bindings: Anything implicitly obtained from `using Mod`, "no
binding", plus slightly more exotic corner cases around conflicts

2. Weakly declared bindin... (continued)

11 of 111 new or added lines in 7 files covered. (9.91%)

1273 existing lines in 68 files now uncovered.

9908 of 48755 relevant lines covered (20.32%)

105126.48 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

28.57
/base/process.jl
1
# This file is a part of Julia. License is MIT: https://julialang.org/license
2

3
mutable struct Process <: AbstractPipe
4
    cmd::Cmd
5
    handle::Ptr{Cvoid}
6
    in::IO
7
    out::IO
8
    err::IO
9
    syncd::Vector{Task}
10
    exitcode::Int64
11
    termsignal::Int32
12
    exitnotify::ThreadSynchronizer
UNCOV
13
    function Process(cmd::Cmd, handle::Ptr{Cvoid}, syncd::Vector{Task})
×
UNCOV
14
        this = new(cmd, handle, devnull, devnull, devnull, syncd,
×
15
                   typemin(fieldtype(Process, :exitcode)),
16
                   typemin(fieldtype(Process, :termsignal)),
17
                   ThreadSynchronizer())
UNCOV
18
        finalizer(uvfinalize, this)
×
19
        return this
×
20
    end
21
end
22
pipe_reader(p::Process) = p.out
352,737✔
23
pipe_writer(p::Process) = p.in
2✔
24

25
# Represents a whole pipeline of any number of related processes
26
# so the entire pipeline can be treated as one entity
27
mutable struct ProcessChain <: AbstractPipe
28
    processes::Vector{Process}
29
    in::IO
30
    out::IO
31
    err::IO
32
    function ProcessChain()
33
        return new(Process[], devnull, devnull, devnull)
×
34
    end
35
end
36
pipe_reader(p::ProcessChain) = p.out
×
37
pipe_writer(p::ProcessChain) = p.in
×
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

48
# release ownership of the libuv handle
UNCOV
49
function uvfinalize(proc::Process)
×
UNCOV
50
    if proc.handle != C_NULL
×
51
        iolock_begin()
×
52
        if proc.handle != C_NULL
×
53
            disassociate_julia_struct(proc.handle)
×
54
            ccall(:jl_close_uv, Cvoid, (Ptr{Cvoid},), proc.handle)
×
55
            proc.handle = C_NULL
×
56
        end
57
        iolock_end()
×
58
    end
59
    nothing
×
60
end
61

62
# called when the process dies
UNCOV
63
function uv_return_spawn(p::Ptr{Cvoid}, exit_status::Int64, termsignal::Int32)
×
UNCOV
64
    data = ccall(:jl_uv_process_data, Ptr{Cvoid}, (Ptr{Cvoid},), p)
×
UNCOV
65
    data == C_NULL && return
×
UNCOV
66
    proc = unsafe_pointer_to_objref(data)::Process
×
UNCOV
67
    proc.exitcode = exit_status
×
UNCOV
68
    proc.termsignal = termsignal
×
UNCOV
69
    disassociate_julia_struct(proc.handle) # ensure that data field is set to C_NULL
×
UNCOV
70
    ccall(:jl_close_uv, Cvoid, (Ptr{Cvoid},), proc.handle)
×
UNCOV
71
    proc.handle = C_NULL
×
UNCOV
72
    lock(proc.exitnotify)
×
UNCOV
73
    try
×
UNCOV
74
        notify(proc.exitnotify)
×
75
    finally
UNCOV
76
        unlock(proc.exitnotify)
×
77
    end
78
    nothing
×
79
end
80

81
# called when the libuv handle is destroyed
82
function _uv_hook_close(proc::Process)
×
83
    Libc.free(@atomicswap :not_atomic proc.handle = C_NULL)
×
84
    nothing
×
85
end
86

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})
89

90
function as_cpumask(cpus::Vector{UInt16})
×
91
    n = max(Int(maximum(cpus)), Int(ccall(:uv_cpumask_size, Cint, ())))
×
92
    cpumask = zeros(Bool, n)
×
93
    for i in cpus
×
94
        cpumask[i] = true
×
95
    end
×
96
    return cpumask
×
97
end
98

99
# handle marshalling of `Cmd` arguments from Julia to C
UNCOV
100
@noinline function _spawn_primitive(file, cmd::Cmd, stdio::SpawnIOs)
×
UNCOV
101
    loop = eventloop()
×
UNCOV
102
    cpumask = cmd.cpus
×
UNCOV
103
    cpumask === nothing || (cpumask = as_cpumask(cpumask))
×
UNCOV
104
    GC.@preserve stdio begin
×
UNCOV
105
        iohandles = Tuple{Cint, UInt}[ # assuming little-endian layout
×
106
            let h = rawhandle(io)
UNCOV
107
                h === C_NULL     ? (0x00, UInt(0)) :
×
UNCOV
108
                h isa OS_HANDLE  ? (0x02, UInt(cconvert(@static(Sys.iswindows() ? Ptr{Cvoid} : Cint), h))) :
×
109
                h isa Ptr{Cvoid} ? (0x04, UInt(h)) :
110
                error("invalid spawn handle $h from $io")
111
            end
112
            for io in stdio]
UNCOV
113
        syncd = Task[io.t for io in stdio if io isa SyncCloseFD]
×
UNCOV
114
        handle = Libc.malloc(_sizeof_uv_process)
×
UNCOV
115
        disassociate_julia_struct(handle)
×
UNCOV
116
        (; exec, flags, env, dir) = cmd
×
UNCOV
117
        flags ⊻= UV_PROCESS_WINDOWS_DISABLE_EXACT_NAME # libuv inverts the default for this, so flip this bit now
×
UNCOV
118
        iolock_begin()
×
UNCOV
119
        err = ccall(:jl_spawn, Int32,
×
120
                  (Cstring, Ptr{Cstring}, Ptr{Cvoid}, Ptr{Cvoid},
121
                   Ptr{Tuple{Cint, UInt}}, Int,
122
                   UInt32, Ptr{Cstring}, Cstring, Ptr{Bool}, Csize_t, Ptr{Cvoid}),
123
            file, exec, loop, handle,
124
            iohandles, length(iohandles),
125
            flags,
126
            env === nothing ? C_NULL : env,
127
            isempty(dir) ? C_NULL : dir,
128
            cpumask === nothing ? C_NULL : cpumask,
129
            cpumask === nothing ? 0 : length(cpumask),
UNCOV
130
            @cfunction(uv_return_spawn, Cvoid, (Ptr{Cvoid}, Int64, Int32)))
×
UNCOV
131
        if err == 0
×
UNCOV
132
            pp = Process(cmd, handle, syncd)
×
UNCOV
133
            associate_julia_struct(handle, pp)
×
134
        else
135
            ccall(:jl_forceclose_uv, Cvoid, (Ptr{Cvoid},), handle) # will call free on handle eventually
×
136
        end
UNCOV
137
        iolock_end()
×
138
    end
UNCOV
139
    if err != 0
×
140
        throw(_UVError("could not spawn " * repr(cmd), err))
×
141
    end
UNCOV
142
    return pp
×
143
end
144

145
_spawn(cmds::AbstractCmd) = _spawn(cmds, SpawnIOs())
12✔
146

147
function _spawn(cmd::AbstractCmd, stdios::Vector{Redirectable})
148
    pp = setup_stdios(stdios) do stdios
277✔
UNCOV
149
        return _spawn(cmd, stdios)
×
150
    end
151
    return pp
×
152
end
153

154
# optimization: we can spawn `Cmd` directly without allocating the ProcessChain
155
function _spawn(cmd::Cmd, stdios::SpawnIOs)
156
    isempty(cmd.exec) && throw(ArgumentError("cannot spawn empty command"))
12✔
157
    return _spawn_primitive(cmd.exec[1], cmd, stdios)
12✔
158
end
159

160
# assume that having a ProcessChain means that the stdio are setup
161
function _spawn(cmds::AbstractCmd, stdios::SpawnIOs)
162
    return _spawn(cmds, stdios, ProcessChain())
×
163
end
164

165
# helper function for making a copy of a SpawnIOs, with replacement
166
function _stdio_copy(stdios::SpawnIOs, fd::Int, @nospecialize replace)
×
167
    nio = max(fd, length(stdios))
×
168
    new = SpawnIOs(undef, nio)
×
169
    copyto!(fill!(new, devnull), stdios)
×
170
    new[fd] = replace
×
171
    return new
×
172
end
173

174
function _spawn(redirect::CmdRedirect, stdios::SpawnIOs, args...)
×
175
    fdnum = redirect.stream_no + 1
×
176
    io, close_io = setup_stdio(redirect.handle, redirect.readable)
×
177
    try
×
178
        stdios = _stdio_copy(stdios, fdnum, io)
×
179
        return _spawn(redirect.cmd, stdios, args...)
×
180
    finally
181
        close_io && close_stdio(io)
×
182
    end
183
end
184

185
function _spawn(cmds::OrCmds, stdios::SpawnIOs, chain::ProcessChain)
×
186
    in_pipe, out_pipe = link_pipe(false, false)
×
187
    try
×
188
        stdios_left = _stdio_copy(stdios, 2, out_pipe)
×
189
        _spawn(cmds.a, stdios_left, chain)
×
190
        stdios_right = _stdio_copy(stdios, 1, in_pipe)
×
191
        _spawn(cmds.b, stdios_right, chain)
×
192
    finally
193
        close_pipe_sync(out_pipe)
×
194
        close_pipe_sync(in_pipe)
×
195
    end
196
    return chain
×
197
end
198

199
function _spawn(cmds::ErrOrCmds, stdios::SpawnIOs, chain::ProcessChain)
×
200
    in_pipe, out_pipe = link_pipe(false, false)
×
201
    try
×
202
        stdios_left = _stdio_copy(stdios, 3, out_pipe)
×
203
        _spawn(cmds.a, stdios_left, chain)
×
204
        stdios_right = _stdio_copy(stdios, 1, in_pipe)
×
205
        _spawn(cmds.b, stdios_right, chain)
×
206
    finally
207
        close_pipe_sync(out_pipe)
×
208
        close_pipe_sync(in_pipe)
×
209
    end
210
    return chain
×
211
end
212

213
function _spawn(cmds::AndCmds, stdios::SpawnIOs, chain::ProcessChain)
×
214
    _spawn(cmds.a, stdios, chain)
×
215
    _spawn(cmds.b, stdios, chain)
×
216
    return chain
×
217
end
218

219
function _spawn(cmd::Cmd, stdios::SpawnIOs, chain::ProcessChain)
×
220
    isempty(cmd.exec) && throw(ArgumentError("cannot spawn empty command"))
×
221
    pp = _spawn_primitive(cmd.exec[1], cmd, stdios)
×
222
    push!(chain.processes, pp)
×
223
    return chain
×
224
end
225

226

227
# open the child end of each element of `stdios`, and initialize the parent end
UNCOV
228
function setup_stdios(f, stdios::Vector{Redirectable})
×
UNCOV
229
    nstdio = length(stdios)
×
UNCOV
230
    open_io = SpawnIOs(undef, nstdio)
×
UNCOV
231
    close_io = falses(nstdio)
×
UNCOV
232
    try
×
UNCOV
233
        for i in 1:nstdio
×
UNCOV
234
            open_io[i], close_io[i] = setup_stdio(stdios[i], i == 1)
×
UNCOV
235
        end
×
UNCOV
236
        pp = f(open_io)
×
UNCOV
237
        return pp
×
238
    finally
UNCOV
239
        for i in 1:nstdio
×
UNCOV
240
            close_io[i] && close_stdio(open_io[i])
×
UNCOV
241
        end
×
242
    end
243
end
244

245
function setup_stdio(stdio::PipeEndpoint, child_readable::Bool)
×
246
    if stdio.status == StatusInit
×
247
        # if the PipeEndpoint isn't open, set it to the parent end
248
        # and pass the other end to the child
249
        rd, wr = link_pipe(!child_readable, child_readable)
×
250
        try
×
251
            open_pipe!(stdio, child_readable ? wr : rd)
×
252
        catch
253
            close_pipe_sync(rd)
×
254
            close_pipe_sync(wr)
×
255
            rethrow()
×
256
        end
257
        child = child_readable ? rd : wr
×
258
        return (child, true)
×
259
    end
260
    # if it's already open, assume that it's already the child end
261
    # (since we can't do anything else)
262
    return (stdio, false)
×
263
end
264

265
function setup_stdio(stdio::Pipe, child_readable::Bool)
×
266
    if stdio.in.status == StatusInit && stdio.out.status == StatusInit
×
267
        link_pipe!(stdio)
×
268
    end
269
    io = child_readable ? stdio.out : stdio.in
×
270
    return (io, false)
×
271
end
272

273
setup_stdio(stdio::AbstractPipe, readable::Bool) =
×
274
    setup_stdio(readable ? pipe_reader(stdio) : pipe_writer(stdio), readable)
275

276
function setup_stdio(stdio::IOStream, child_readable::Bool)
×
277
    io = RawFD(fd(stdio))
×
278
    return (io, false)
×
279
end
280

281
function setup_stdio(stdio::FileRedirect, child_readable::Bool)
×
282
    if child_readable
×
283
        attr = JL_O_RDONLY
×
284
        perm = zero(S_IRUSR)
×
285
    else
286
        attr = JL_O_WRONLY | JL_O_CREAT
×
287
        attr |= stdio.append ? JL_O_APPEND : JL_O_TRUNC
×
288
        perm = S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH
×
289
    end
290
    io = Filesystem.open(stdio.filename, attr, perm)
×
291
    return (io, true)
×
292
end
293

294
# incrementally move data between an arbitrary IO and a system Pipe,
295
# including copying the EOF (shutdown) when finished
296
# TODO: probably more efficient (when valid) to use `stdio` directly as the
297
#       PipeEndpoint buffer field in some cases
298
function setup_stdio(stdio::IO, child_readable::Bool)
299
    parent = PipeEndpoint()
300
    rd, wr = link_pipe(!child_readable, child_readable)
301
    try
302
        open_pipe!(parent, child_readable ? wr : rd)
303
    catch
304
        close_pipe_sync(rd)
305
        close_pipe_sync(wr)
306
        rethrow()
307
    end
308
    child = child_readable ? rd : wr
309
    try
310
        let in = (child_readable ? parent : stdio),
311
            out = (child_readable ? stdio : parent),
312
            t = @async try
88✔
313
                write(in, out)
88✔
314
            catch ex
315
                @warn "Process I/O error" exception=(ex, catch_backtrace())
×
316
                rethrow()
88✔
317
            finally
318
                close(parent)
88✔
319
            end
320
            return (SyncCloseFD(child, t), true)
321
        end
322
    catch
323
        close_pipe_sync(child)
324
        rethrow()
325
    end
326
end
327

328
close_stdio(stdio) = close(stdio)
5✔
329
close_stdio(stdio::OS_HANDLE) = close_pipe_sync(stdio)
×
330
close_stdio(stdio::SyncCloseFD) = close_stdio(stdio.fd)
×
331

332
# INTERNAL
333
# pad out stdio to have at least three elements,
334
# passing either `devnull` or the corresponding `stdio`
335
# A Redirectable can be any of:
336
#   - A system IO handle, to be passed to the child
337
#   - An uninitialized pipe, to be created
338
#   - devnull (to pass /dev/null for 0-2, or to leave undefined for fd > 2)
339
#   - An Filesystem.File or IOStream object to redirect the output to
340
#   - A FileRedirect, containing a string specifying a filename to be opened for the child
341

342
spawn_opts_swallow(stdios::StdIOSet) = Redirectable[stdios...]
×
343
spawn_opts_inherit(stdios::StdIOSet) = Redirectable[stdios...]
×
344
spawn_opts_swallow(in::Redirectable=devnull, out::Redirectable=devnull, err::Redirectable=devnull) =
29✔
345
    Redirectable[in, out, err]
346
# pass original descriptors to child processes by default, because we might
347
# have already exhausted and closed the libuv object for our standard streams.
348
# ref issue #8529
349
spawn_opts_inherit(in::Redirectable=RawFD(0), out::Redirectable=RawFD(1), err::Redirectable=RawFD(2)) =
38✔
350
    Redirectable[in, out, err]
351

352
function eachline(cmd::AbstractCmd; keep::Bool=false)
×
353
    out = PipeEndpoint()
×
354
    processes = _spawn(cmd, Redirectable[devnull, out, stderr])
×
355
    # if the user consumes all the data, also check process exit status for success
356
    ondone = () -> (success(processes) || pipeline_error(processes); nothing)
×
357
    return EachLine(out, keep=keep, ondone=ondone)::EachLine
×
358
end
359

360
"""
361
    open(command, mode::AbstractString, stdio=devnull)
362

363
Run `command` asynchronously. Like `open(command, stdio; read, write)` except specifying
364
the read and write flags via a mode string instead of keyword arguments.
365
Possible mode strings are:
366

367
| Mode | Description | Keywords                         |
368
|:-----|:------------|:---------------------------------|
369
| `r`  | read        | none                             |
370
| `w`  | write       | `write = true`                   |
371
| `r+` | read, write | `read = true, write = true`      |
372
| `w+` | read, write | `read = true, write = true`      |
373
"""
374
function open(cmds::AbstractCmd, mode::AbstractString, stdio::Redirectable=devnull)
211✔
375
    if mode == "r+" || mode == "w+"
440✔
376
        return open(cmds, stdio, read = true, write = true)
17✔
377
    elseif mode == "r"
199✔
378
        return open(cmds, stdio)
161✔
379
    elseif mode == "w"
38✔
380
        return open(cmds, stdio, write = true)
38✔
381
    else
382
        throw(ArgumentError("mode must be \"r\", \"w\", \"r+\", or \"w+\", not $(repr(mode))"))
×
383
    end
384
end
385

386
# return a Process object to read-to/write-from the pipeline
387
"""
388
    open(command, stdio=devnull; write::Bool = false, read::Bool = !write)
389

390
Start running `command` asynchronously, and return a `process::IO` object.  If `read` is
391
true, then reads from the process come from the process's standard output and `stdio` optionally
392
specifies the process's standard input stream.  If `write` is true, then writes go to
393
the process's standard input and `stdio` optionally specifies the process's standard output
394
stream.
395
The process's standard error stream is connected to the current global `stderr`.
396
"""
397
function open(cmds::AbstractCmd, stdio::Redirectable=devnull; write::Bool=false, read::Bool=!write)
610✔
398
    if read && write
276✔
399
        stdio === devnull || throw(ArgumentError("no stream can be specified for `stdio` in read-write mode"))
17✔
400
        in = PipeEndpoint()
17✔
401
        out = PipeEndpoint()
17✔
402
        processes = _spawn(cmds, Redirectable[in, out, stderr])
17✔
403
        processes.in = in
17✔
404
        processes.out = out
17✔
405
    elseif read
259✔
406
        out = PipeEndpoint()
219✔
407
        processes = _spawn(cmds, Redirectable[stdio, out, stderr])
219✔
408
        processes.out = out
219✔
409
    elseif write
40✔
410
        in = PipeEndpoint()
40✔
411
        processes = _spawn(cmds, Redirectable[in, stdio, stderr])
40✔
412
        processes.in = in
40✔
413
    else
414
        stdio === devnull || throw(ArgumentError("no stream can be specified for `stdio` in no-access mode"))
×
415
        processes = _spawn(cmds, Redirectable[devnull, devnull, stderr])
×
416
    end
417
    return processes
276✔
418
end
419

420
"""
421
    open(f::Function, command, args...; kwargs...)
422

423
Similar to `open(command, args...; kwargs...)`, but calls `f(stream)` on the
424
resulting process stream, then closes the input stream and waits for the process
425
to complete. Return the value returned by `f` on success. Throw an error if the
426
process failed, or if the process attempts to print anything to stdout.
427
"""
428
function open(f::Function, cmds::AbstractCmd, args...; kwargs...)
120✔
429
    P = open(cmds, args...; kwargs...)
60✔
430
    function waitkill(P::Union{Process,ProcessChain})
60✔
431
        close(P)
×
432
        # shortly after we hope it starts cleanup and dies (from closing
433
        # stdio), we kill the process with SIGTERM (15) so that we can proceed
434
        # with throwing the error and hope it will exit soon from that
435
        local t = Timer(2) do t
×
436
            process_running(P) && kill(P)
437
        end
438
        # pass false to indicate that we do not care about data-races on the
439
        # Julia stdio objects after this point, since we already know this is
440
        # an error path and the state of them is fairly unpredictable anyways
441
        # in that case. Since we closed P some of those should come crumbling
442
        # down already, and we don't want to throw that error here either.
443
        wait(P, false)
×
444
        close(t)
×
445
    end
446
    ret = try
60✔
447
        f(P)
60✔
448
    catch
449
        waitkill(P)
×
450
        rethrow()
×
451
    end
452
    close(P.in)
60✔
453
    closestdio = @async begin
120✔
454
        # wait for P to complete (including sync'd), then mark the output streams for EOF (if applicable to that stream type)
455
        wait(P)
60✔
456
        err = P.err
60✔
457
        applicable(closewrite, err) && closewrite(err)
60✔
458
        out = P.out
60✔
459
        applicable(closewrite, out) && closewrite(out)
60✔
460
        nothing
60✔
461
    end
462
    # now verify that the output stream is at EOF, and the user didn't fail to consume it successfully
463
    # (we do not currently verify the user dealt with the stderr stream)
464
    if !(eof(P.out)::Bool)
60✔
465
        waitkill(P)
×
466
        throw(_UVError("open(do)", UV_EPIPE))
×
467
    end
468
    # make sure to closestdio is completely done to avoid data-races later
469
    wait(closestdio)
60✔
470
    success(P) || pipeline_error(P)
60✔
471
    return ret
60✔
472
end
473

474
"""
475
    read(command::Cmd)
476

477
Run `command` and return the resulting output as an array of bytes.
478
"""
479
function read(cmd::AbstractCmd)
161✔
480
    procs = open(cmd, "r", devnull)
161✔
481
    bytes = read(procs.out)
161✔
482
    success(procs) || pipeline_error(procs)
162✔
483
    return bytes::Vector{UInt8}
160✔
484
end
485

486
"""
487
    read(command::Cmd, String)
488

489
Run `command` and return the resulting output as a `String`.
490
"""
491
read(cmd::AbstractCmd, ::Type{String}) = String(read(cmd))::String
273✔
492

493
"""
494
    run(command, args...; wait::Bool = true)
495

496
Run a command object, constructed with backticks (see the [Running External Programs](@ref)
497
section in the manual). Throws an error if anything goes wrong, including the process
498
exiting with a non-zero status (when `wait` is true).
499

500
The `args...` allow you to pass through file descriptors to the command, and are ordered
501
like regular unix file descriptors (eg `stdin, stdout, stderr, FD(3), FD(4)...`).
502

503
If `wait` is false, the process runs asynchronously. You can later wait for it and check
504
its exit status by calling `success` on the returned process object.
505

506
When `wait` is false, the process' I/O streams are directed to `devnull`.
507
When `wait` is true, I/O streams are shared with the parent process.
508
Use [`pipeline`](@ref) to control I/O redirection.
509
"""
510
function run(cmds::AbstractCmd, args...; wait::Bool = true)
123✔
511
    if wait
1✔
UNCOV
512
        ps = _spawn(cmds, spawn_opts_inherit(args...))
×
UNCOV
513
        success(ps) || pipeline_error(ps)
×
514
    else
515
        stdios = spawn_opts_swallow(args...)
1✔
516
        ps = _spawn(cmds, stdios)
1✔
517
        # for each stdio input argument, guess whether the user
518
        # passed a `stdio` placeholder object as input, and thus
519
        # might be able to use the return AbstractProcess as an IO object
520
        # (this really only applies to PipeEndpoint, Pipe, TCPSocket, or an AbstractPipe wrapping one of those)
521
        if length(stdios) > 0
1✔
522
            in = stdios[1]
1✔
523
            isa(in, IO) && (ps.in = in)
1✔
524
            if length(stdios) > 1
1✔
525
                out = stdios[2]
1✔
526
                isa(out, IO) && (ps.out = out)
1✔
527
                if length(stdios) > 2
1✔
528
                    err = stdios[3]
1✔
529
                    isa(err, IO) && (ps.err = err)
1✔
530
                end
531
            end
532
        end
533
    end
534
    return ps
1✔
535
end
536

537
# some common signal numbers that are usually available on all platforms
538
# and might be useful as arguments to `kill` or testing against `Process.termsignal`
539
const SIGHUP  = 1
540
const SIGINT  = 2
541
const SIGQUIT = 3 # !windows
542
const SIGKILL = 9
543
const SIGPIPE = 13 # !windows
544
const SIGTERM = 15
545

UNCOV
546
function test_success(proc::Process)
×
UNCOV
547
    @assert process_exited(proc)
×
UNCOV
548
    if proc.exitcode < 0
×
549
        #TODO: this codepath is not currently tested
550
        throw(_UVError("could not start process " * repr(proc.cmd), proc.exitcode))
×
551
    end
UNCOV
552
    return proc.exitcode == 0 && proc.termsignal == 0
×
553
end
554

UNCOV
555
function success(x::Process)
×
UNCOV
556
    wait(x)
×
UNCOV
557
    return test_success(x)
×
558
end
559
success(procs::Vector{Process}) = mapreduce(success, &, procs)
×
560
success(procs::ProcessChain) = success(procs.processes)
×
561

562
"""
563
    success(command)
564

565
Run a command object, constructed with backticks (see the [Running External Programs](@ref)
566
section in the manual), and tell whether it was successful (exited with a code of 0).
567
An exception is raised if the process cannot be started.
568
"""
569
success(cmd::AbstractCmd) = success(_spawn(cmd))
12✔
570

571

572
"""
573
    ProcessFailedException
574

575
Indicates problematic exit status of a process.
576
When running commands or pipelines, this is thrown to indicate
577
a nonzero exit code was returned (i.e. that the invoked process failed).
578
"""
579
struct ProcessFailedException <: Exception
580
    procs::Vector{Process}
1✔
581
end
582
ProcessFailedException(proc::Process) = ProcessFailedException([proc])
1✔
583

584
function showerror(io::IO, err::ProcessFailedException)
2✔
585
    if length(err.procs) == 1
2✔
586
        proc = err.procs[1]
2✔
587
        println(io, "failed process: ", proc, " [", proc.exitcode, "]")
2✔
588
    else
589
        println(io, "failed processes:")
×
590
        for proc in err.procs
×
591
            println(io, "  ", proc, " [", proc.exitcode, "]")
×
592
        end
×
593
    end
594
end
595

596
function pipeline_error(proc::Process)
597
    if !proc.cmd.ignorestatus
1✔
598
        throw(ProcessFailedException(proc))
1✔
599
    end
600
    nothing
×
601
end
602

603
function pipeline_error(procs::ProcessChain)
×
604
    failed = Process[]
×
605
    for p = procs.processes
×
606
        if !test_success(p) && !p.cmd.ignorestatus
×
607
            push!(failed, p)
×
608
        end
609
    end
×
610
    isempty(failed) && return nothing
×
611
    throw(ProcessFailedException(failed))
×
612
end
613

614
"""
615
    kill(p::Process, signum=Base.SIGTERM)
616

617
Send a signal to a process. The default is to terminate the process.
618
Returns successfully if the process has already exited, but throws an
619
error if killing the process failed for other reasons (e.g. insufficient
620
permissions).
621
"""
622
function kill(p::Process, signum::Integer=SIGTERM)
×
623
    iolock_begin()
×
624
    if process_running(p)
×
625
        @assert p.handle != C_NULL
×
626
        err = ccall(:uv_process_kill, Int32, (Ptr{Cvoid}, Int32), p.handle, signum)
×
627
        if err != 0 && err != UV_ESRCH
×
628
            throw(_UVError("kill", err))
×
629
        end
630
    end
631
    iolock_end()
×
632
    nothing
×
633
end
634
kill(ps::Vector{Process}, signum::Integer=SIGTERM) = for p in ps; kill(p, signum); end
×
635
kill(ps::ProcessChain, signum::Integer=SIGTERM) = kill(ps.processes, signum)
×
636

637
"""
638
    getpid(process) -> Int32
639

640
Get the child process ID, if it still exists.
641

642
!!! compat "Julia 1.1"
643
    This function requires at least Julia 1.1.
644
"""
645
function Libc.getpid(p::Process)
×
646
    # TODO: due to threading, this method is only weakly synchronized with the user application
647
    iolock_begin()
×
648
    ppid = Int32(0)
×
649
    if p.handle != C_NULL # e.g. process_running
×
650
        ppid = ccall(:jl_uv_process_pid, Int32, (Ptr{Cvoid},), p.handle)
×
651
    end
652
    iolock_end()
×
653
    ppid <= 0 && throw(_UVError("getpid", UV_ESRCH))
×
654
    return ppid
×
655
end
656

657
## process status ##
658

659
"""
660
    process_running(p::Process)
661

662
Determine whether a process is currently running.
663
"""
664
process_running(s::Process) = s.handle != C_NULL
51✔
665
process_running(s::Vector{Process}) = any(process_running, s)
×
666
process_running(s::ProcessChain) = process_running(s.processes)
×
667

668
"""
669
    process_exited(p::Process)
670

671
Determine whether a process has exited.
672
"""
673
process_exited(s::Process) = !process_running(s)
46✔
674
process_exited(s::Vector{Process}) = all(process_exited, s)
×
675
process_exited(s::ProcessChain) = process_exited(s.processes)
×
676

677
process_signaled(s::Process) = (s.termsignal > 0)
48✔
678

679
function process_status(s::Process)
680
    return process_running(s) ? "ProcessRunning" :
6✔
681
           process_signaled(s) ? "ProcessSignaled(" * string(s.termsignal) * ")" :
682
           process_exited(s) ? "ProcessExited(" * string(s.exitcode) * ")" :
683
           error("process status error")
684
end
685

686
function wait(x::Process, syncd::Bool=true)
687
    if !process_exited(x)
106✔
688
        iolock_begin()
689
        if !process_exited(x)
690
            preserve_handle(x)
691
            lock(x.exitnotify)
692
            iolock_end()
693
            try
694
                wait(x.exitnotify)
695
            finally
696
                unlock(x.exitnotify)
697
                unpreserve_handle(x)
698
            end
699
        else
700
            iolock_end()
701
        end
702
    end
703
    # and make sure all sync'd Tasks are complete too
704
    syncd && for t in x.syncd
705
        wait(t)
706
    end
707
    nothing
708
end
709

710
wait(x::ProcessChain, syncd::Bool=true) = foreach(p -> wait(p, syncd), x.processes)
×
711

712
show(io::IO, p::Process) = print(io, "Process(", p.cmd, ", ", process_status(p), ")")
3✔
713

714
# allow the elements of the Cmd to be accessed as an array or iterator
715
for f in (:length, :firstindex, :lastindex, :keys, :first, :last, :iterate)
716
    @eval $f(cmd::Cmd) = $f(cmd.exec)
1✔
717
end
718
Iterators.reverse(cmd::Cmd) = Iterators.reverse(cmd.exec)
×
719
eltype(::Type{Cmd}) = eltype(fieldtype(Cmd, :exec))
×
720
for f in (:iterate, :getindex)
721
    @eval $f(cmd::Cmd, i) = $f(cmd.exec, i)
×
722
end
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc