# This file is a part of Julia. License is MIT: https://julialang.org/license ################################### # Cross Platform tests for spawn. # ################################### valgrind_off = ccall(:jl_running_on_valgrind, Cint, ()) == 0 yescmd = `yes` echocmd = `echo` sortcmd = `sort` printfcmd = `printf` truecmd = `true` falsecmd = `false` catcmd = `cat` shcmd = `sh` sleepcmd = `sleep` lscmd = `ls` if is_windows() try # use busybox-w32 on windows success(`busybox`) yescmd = `busybox yes` echocmd = `busybox echo` sortcmd = `busybox sort` printfcmd = `busybox printf` truecmd = `busybox true` falsecmd = `busybox false` catcmd = `busybox cat` shcmd = `busybox sh` sleepcmd = `busybox sleep` lscmd = `busybox ls` end end #### Examples used in the manual #### @test readstring(`$echocmd hello \| sort`) == "hello | sort\n" @test readstring(pipeline(`$echocmd hello`, sortcmd)) == "hello\n" @test length(spawn(pipeline(`$echocmd hello`, sortcmd)).processes) == 2 out = readstring(`$echocmd hello` & `$echocmd world`) @test search(out,"world") != 0:-1 @test search(out,"hello") != 0:-1 @test readstring(pipeline(`$echocmd hello` & `$echocmd world`, sortcmd)) == "hello\nworld\n" @test (run(`$printfcmd " \033[34m[stdio passthrough ok]\033[0m\n"`); true) # Test for SIGPIPE being treated as normal termination (throws an error if broken) is_unix() && run(pipeline(yescmd, `head`, DevNull)) begin a = Base.Condition() @schedule begin p = spawn(pipeline(yescmd,DevNull)) Base.notify(a,p) @test !success(p) end p = wait(a) kill(p) end if valgrind_off # If --trace-children=yes is passed to valgrind, valgrind will # exit here with an error code, and no UVError will be raised. @test_throws Base.UVError run(`foo_is_not_a_valid_command`) end if is_unix() prefixer(prefix, sleep) = `sh -c "while IFS= read REPLY; do echo '$prefix ' \$REPLY; sleep $sleep; done"` @test success(pipeline(`sh -c "for i in 1 2 3 4 5 6 7 8 9 10; do echo \$i; sleep 0.1; done"`, prefixer("A", 0.2) & prefixer("B", 0.2))) @test success(pipeline(`sh -c "for i in 1 2 3 4 5 6 7 8 9 10; do echo \$i; sleep 0.1; done"`, prefixer("X", 0.3) & prefixer("Y", 0.3) & prefixer("Z", 0.3), prefixer("A", 0.2) & prefixer("B", 0.2))) end @test success(truecmd) @test !success(falsecmd) @test success(pipeline(truecmd, truecmd)) @test_broken success(ignorestatus(falsecmd)) @test_broken success(pipeline(ignorestatus(falsecmd), truecmd)) @test !success(pipeline(ignorestatus(falsecmd), falsecmd)) @test !success(ignorestatus(falsecmd) & falsecmd) @test_broken success(ignorestatus(pipeline(falsecmd, falsecmd))) @test_broken success(ignorestatus(falsecmd & falsecmd)) # STDIN Redirection let file = tempname() run(pipeline(`$echocmd hello world`, file)) @test readstring(pipeline(file, catcmd)) == "hello world\n" @test open(readstring, pipeline(file, catcmd), "r") == "hello world\n" rm(file) end # Stream Redirection if !is_windows() # WINNT reports operation not supported on socket (ENOTSUP) for this test local r = Channel(1) local port, server, sock, client, t1, t2 t1 = @async begin port, server = listenany(2326) put!(r, port) client = accept(server) @test readstring(pipeline(client, catcmd)) == "hello world\n" close(server) return true end t2 = @async begin sock = connect(fetch(r)) run(pipeline(`$echocmd hello world`, sock)) close(sock) return true end @test wait(t1) @test wait(t2) end @test readstring(setenv(`$shcmd -c "echo \$TEST"`,["TEST=Hello World"])) == "Hello World\n" @test readstring(setenv(`$shcmd -c "echo \$TEST"`,Dict("TEST"=>"Hello World"))) == "Hello World\n" @test readstring(setenv(`$shcmd -c "echo \$TEST"`,"TEST"=>"Hello World")) == "Hello World\n" @test (withenv("TEST"=>"Hello World") do readstring(`$shcmd -c "echo \$TEST"`); end) == "Hello World\n" let pathA = readchomp(setenv(`$shcmd -c "pwd -P"`;dir="..")), pathB = readchomp(setenv(`$shcmd -c "cd .. && pwd -P"`)) if is_windows() # on windows, sh returns posix-style paths that are not valid according to ispath @test pathA == pathB else @test Base.samefile(pathA, pathB) end end let str = "", stdin, stdout, proc, str2, file for i = 1:1000 str = "$str\n $(randstring(10))" end # Here we test that if we close a stream with pending writes, we don't lose the writes. stdout, stdin, proc = readandwrite(`$catcmd -`) write(stdin, str) close(stdin) str2 = readstring(stdout) @test str2 == str # This test hangs if the end-of-run-walk-across-uv-streams calls shutdown on a stream that is shutting down. file = tempname() open(pipeline(`$catcmd -`, file), "w") do io write(io, str) end rm(file) end # issue #3373 # fixing up Conditions after interruptions let r, t r = Channel(1) t = @async begin try wait(r) end p = spawn(`$sleepcmd 1`); wait(p) @test p.exitcode == 0 return true end yield() schedule(t, InterruptException(), error=true) yield() put!(r,11) yield() @test wait(t) end # Test marking of IO let r, t, sock r = Channel(1) t = @async begin port, server = listenany(2327) put!(r, port) client = accept(server) write(client, "Hello, world!\n") write(client, "Goodbye, world...\n") close(server) return true end sock = connect(fetch(r)) mark(sock) @test ismarked(sock) @test readline(sock) == "Hello, world!" @test readline(sock) == "Goodbye, world..." @test reset(sock) == 0 @test !ismarked(sock) mark(sock) @test ismarked(sock) @test readline(sock) == "Hello, world!" unmark(sock) @test !ismarked(sock) @test_throws ArgumentError reset(sock) @test !unmark(sock) @test readline(sock) == "Goodbye, world..." #@test eof(sock) ## doesn't work close(sock) @test wait(t) end # issue #4535 exename = Base.julia_cmd() if valgrind_off # If --trace-children=yes is passed to valgrind, we will get a # valgrind banner here, not "Hello World\n". @test readstring(pipeline(`$exename --startup-file=no -e 'println(STDERR,"Hello World")'`, stderr=catcmd)) == "Hello World\n" out = Pipe() proc = spawn(pipeline(`$exename --startup-file=no -e 'println(STDERR,"Hello World")'`, stderr = out)) close(out.in) @test readstring(out) == "Hello World\n" @test success(proc) end # issue #6310 @test readstring(pipeline(`$echocmd "2+2"`, `$exename --startup-file=no`)) == "4\n" # issue #5904 @test run(pipeline(ignorestatus(falsecmd), truecmd)) === nothing @testset "redirect_*" begin let OLD_STDOUT = STDOUT, fname = tempname(), f = open(fname,"w") redirect_stdout(f) println("Hello World") redirect_stdout(OLD_STDOUT) close(f) @test "Hello World\n" == readstring(fname) @test OLD_STDOUT === STDOUT rm(fname) end end # Test that redirecting an IOStream does not crash the process let fname = tempname() cmd = """ # Overwrite libuv memory before freeing it, to make sure that a use after free # triggers an assertion. function thrash(handle::Ptr{Void}) # Kill the memory, but write a nice low value in the libuv type field to # trigger the right code path ccall(:memset,Ptr{Void},(Ptr{Void},Cint,Csize_t),handle,0xee,3*sizeof(Ptr{Void})) unsafe_store!(convert(Ptr{Cint},handle+2*sizeof(Ptr{Void})),15) nothing end OLD_STDERR = STDERR redirect_stderr(open("$(escape_string(fname))","w")) # Usually this would be done by GC. Do it manually, to make the failure # case more reliable. oldhandle = OLD_STDERR.handle OLD_STDERR.status = Base.StatusClosing OLD_STDERR.handle = C_NULL ccall(:uv_close,Void,(Ptr{Void},Ptr{Void}),oldhandle,cfunction(thrash,Void,(Ptr{Void},))) sleep(1) import Base.zzzInvalidIdentifier """ try (in,p) = open(pipeline(`$exename --startup-file=no`, stderr=STDERR), "w") write(in,cmd) close(in) wait(p) catch error("IOStream redirect failed. Child stderr was \n$(readstring(fname))\n") finally rm(fname) end end # issue #10994: libuv can't handle strings containing NUL let bad = "bad\0name" @test_throws ArgumentError run(`$bad`) @test_throws ArgumentError run(`$echocmd $bad`) @test_throws ArgumentError run(setenv(`$echocmd hello`, bad=>"good")) @test_throws ArgumentError run(setenv(`$echocmd hello`, "good"=>bad)) end # issue #12829 let out = Pipe(), echo = `$exename --startup-file=no -e 'print(STDOUT, " 1\t", readstring(STDIN))'`, ready = Condition(), t, infd, outfd @test_throws ArgumentError write(out, "not open error") t = @async begin # spawn writer task open(echo, "w", out) do in1 open(echo, "w", out) do in2 notify(ready) write(in1, 'h') write(in2, UInt8['w']) println(in1, "ello") write(in2, "orld\n") end end infd = Base._fd(out.in) outfd = Base._fd(out.out) show(out, out) notify(ready) @test isreadable(out) @test iswritable(out) close(out.in) @test !isopen(out.in) is_windows() || @test !isopen(out.out) # it takes longer to propagate EOF through the Windows event system @test_throws ArgumentError write(out, "now closed error") @test isreadable(out) @test !iswritable(out) if is_windows() # WINNT kernel does not provide a fast mechanism for async propagation # of EOF for a blocking stream, so just wait for it to catch up. # This shouldn't take much more than 32ms. Base.wait_close(out) end @test !isopen(out) end wait(ready) # wait for writer task to be ready before using `out` @test nb_available(out) == 0 @test endswith(readuntil(out, '1'), '1') @test Char(read(out, UInt8)) == '\t' c = UInt8[0] @test c == read!(out, c) Base.wait_readnb(out, 1) @test nb_available(out) > 0 ln1 = readline(out) ln2 = readline(out) desc = readstring(out) @test !isreadable(out) @test !iswritable(out) @test !isopen(out) @test infd != Base._fd(out.in) == Base.INVALID_OS_HANDLE @test outfd != Base._fd(out.out) == Base.INVALID_OS_HANDLE @test nb_available(out) == 0 @test c == UInt8['w'] @test lstrip(ln2) == "1\thello" @test ln1 == "orld" @test isempty(read(out)) @test eof(out) @test desc == "Pipe($infd open => $outfd active, 0 bytes waiting)" wait(t) end # issue #8529 let fname = tempname() write(fname, "test\n") code = """ cmd = pipeline(`echo asdf`,`cat`) if is_windows() try success(`busybox`) cmd = pipeline(`busybox echo asdf`,`busybox cat`) end end for line in eachline(STDIN) run(cmd) end """ @test success(pipeline(`$catcmd $fname`, `$exename --startup-file=no -e $code`)) rm(fname) end # Ensure that quoting works @test Base.shell_split("foo bar baz") == ["foo", "bar", "baz"] @test Base.shell_split("foo\\ bar baz") == ["foo bar", "baz"] @test Base.shell_split("'foo bar' baz") == ["foo bar", "baz"] @test Base.shell_split("\"foo bar\" baz") == ["foo bar", "baz"] # "Over quoted" @test Base.shell_split("'foo\\ bar' baz") == ["foo\\ bar", "baz"] @test Base.shell_split("\"foo\\ bar\" baz") == ["foo\\ bar", "baz"] # Ensure that shell_split handles quoted spaces let cmd = ["/Volumes/External HD/program", "-a"] @test Base.shell_split("/Volumes/External\\ HD/program -a") == cmd @test Base.shell_split("'/Volumes/External HD/program' -a") == cmd @test Base.shell_split("\"/Volumes/External HD/program\" -a") == cmd end # Backticks should automatically quote where necessary let cmd = ["foo bar", "baz"] @test string(`$cmd`) == "`'foo bar' baz`" end @test Base.shell_split("\"\\\\\"") == ["\\"] # issue #13616 @test_throws ErrorException collect(eachline(pipeline(`$catcmd _doesnt_exist__111_`, stderr=DevNull))) # make sure windows_verbatim strips quotes if is_windows() readstring(`cmd.exe /c dir /b spawn.jl`) == readstring(Cmd(`cmd.exe /c dir /b "\"spawn.jl\""`, windows_verbatim=true)) end # make sure Cmd is nestable @test string(Cmd(Cmd(`ls`, detach=true))) == "`ls`" # equality tests for Cmd @test Base.Cmd(``) == Base.Cmd(``) @test Base.Cmd(`lsof -i :9090`) == Base.Cmd(`lsof -i :9090`) @test Base.Cmd(`$echocmd test`) == Base.Cmd(`$echocmd test`) @test Base.Cmd(``) != Base.Cmd(`$echocmd test`) @test Base.Cmd(``, ignorestatus=true) != Base.Cmd(``, ignorestatus=false) @test Base.Cmd(``, dir="TESTS") != Base.Cmd(``, dir="TEST") @test Base.Set([``, ``]) == Base.Set([``]) @test Set([``, echocmd]) != Set([``, ``]) @test Set([echocmd, ``, ``, echocmd]) == Set([echocmd, ``]) # equality tests for AndCmds @test Base.AndCmds(`$echocmd abc`, `$echocmd def`) == Base.AndCmds(`$echocmd abc`, `$echocmd def`) @test Base.AndCmds(`$echocmd abc`, `$echocmd def`) != Base.AndCmds(`$echocmd abc`, `$echocmd xyz`) # test for correct error when an empty command is spawned (Issue 19094) @test_throws ArgumentError run(Base.Cmd(``)) @test_throws ArgumentError run(Base.AndCmds(``, ``)) @test_throws ArgumentError run(Base.AndCmds(``, `$truecmd`)) @test_throws ArgumentError run(Base.AndCmds(`$truecmd`, ``)) @test_throws ArgumentError spawn(Base.Cmd(``)) @test_throws ArgumentError spawn(Base.AndCmds(``, ``)) @test_throws ArgumentError spawn(Base.AndCmds(``, `$echocmd test`)) @test_throws ArgumentError spawn(Base.AndCmds(`$echocmd test`, ``)) # tests for reducing over collection of Cmd @test_throws ArgumentError reduce(&, Base.AbstractCmd[]) @test_throws ArgumentError reduce(&, Base.Cmd[]) @test reduce(&, [`$echocmd abc`, `$echocmd def`, `$echocmd hij`]) == `$echocmd abc` & `$echocmd def` & `$echocmd hij` # test for proper handling of FD exhaustion if is_unix() let ps = Pipe[] ulimit_n = tryparse(Int, readchomp(`sh -c 'ulimit -n'`)) try for i = 1 : 100 * get(ulimit_n, 1000) p = Pipe() Base.link_pipe(p) push!(ps, p) end if isnull(ulimit_n) warn("`ulimit -n` is set to unlimited, fd exhaustion cannot be tested") @test_broken false else @test false end catch ex isa(ex, Base.UVError) || rethrow(ex) @test ex.code == Base.UV_EMFILE finally for p in ps close(p) end end end end # Test for PR 17803 let p=Pipe() Base.link_pipe(p; julia_only_read=true, julia_only_write=true) ccall(:jl_static_show, Void, (Ptr{Void}, Any), p.in, Int128(-1)) @async close(p.in) @test readstring(p.out) == "Int128(0xffffffffffffffffffffffffffffffff)" end # readlines(::Cmd), accidentally broken in #20203 @test sort(readlines(`$lscmd -A`)) == sort(readdir()) # issue #19864 (PR #20497) @test readchomp(pipeline(ignorestatus( `$exename --startup-file=no -e ' struct Error19864 <: Exception; end Base.showerror(io::IO, e::Error19864) = print(io, "correct19864") throw(Error19864())'`), stderr=catcmd)) == "ERROR: correct19864"