(* Copyright (C) 2009 Matthew Fluet. * Copyright (C) 2002-2008 Henry Cejtin, Matthew Fluet, Suresh * Jagannathan, and Stephen Weeks. * * MLton is released under a BSD-style license. * See the file MLton-LICENSE for details. *) structure MLtonProcess = struct structure Prim = PrimitiveFFI.MLton.Process structure MLton = Primitive.MLton local open Posix in structure FileSys = FileSys structure IO = IO structure ProcEnv = ProcEnv structure Process = Process structure FileDesc = PrePosix.FileDesc structure PId = PrePosix.PId structure Signal = PrePosix.Signal end structure Mask = MLtonSignal.Mask structure SysCall = PosixError.SysCall type pid = PId.t exception MisuseOfForget exception DoublyRedirected type input = unit type output = unit type none = unit type chain = unit type any = unit val useWindowsProcess = MLton.Platform.OS.useWindowsProcess val readWrite = let open FileSys.S in flags [irusr, iwusr, irgrp, iwgrp, iroth, iwoth] end structure Child = struct datatype 'use childt = FileDesc of FileSys.file_desc | Stream of 'use * ('use -> unit) | Term type ('use, 'dir) t = 'use childt ref (* This is _not_ the identity; by rebuilding it we get type * ('a, 'b) t -> ('c, 'd) t *) fun remember x = case !x of FileDesc f => (x := Stream ((), fn () => ()) ; ref (FileDesc f)) | Stream _ => raise MisuseOfForget (* remember twice = bad *) | Term => ref Term local fun convert (new, close) p = case !p of FileDesc fd => let val str = new (fd, "") val () = p := Stream (str, close) in str end | Stream (str, _) => str | Term => raise MisuseOfForget in val binIn = convert (BinIO.newIn, BinIO.closeIn) val binOut = convert (BinIO.newOut, BinIO.closeOut) val textIn = convert (TextIO.newIn, TextIO.closeIn) val textOut = convert (TextIO.newOut, TextIO.closeOut) end fun fd p = case !p of FileDesc fd => fd | _ => raise MisuseOfForget fun close ch = case ch of FileDesc fd => IO.close fd | Stream (str, close) => close str | Term => () val close = fn (stdin, stdout, stderr) => (close stdin; close stdout; close stderr) end structure Param = struct datatype ('use, 'dir) t = File of string | FileDesc of FileSys.file_desc | Pipe | Self (* This is _not_ the identity; by rebuilding it we get type * ('a, 'b) t -> ('c, 'd) t *) val forget = fn File x => File x | FileDesc f => FileDesc f | Pipe => Pipe | Self => Self val pipe = Pipe local val null = if useWindowsProcess then "nul" else "/dev/null" in val null = File null end val self = Self fun file f = File f fun fd f = FileDesc f fun child c = FileDesc (case !c of Child.FileDesc f => (c := Child.Stream ((), fn () => ()); f) | Child.Stream _ => raise DoublyRedirected | Child.Term => raise MisuseOfForget) fun setCloseExec fd = if useWindowsProcess then () else IO.setfd (fd, IO.FD.flags [IO.FD.cloexec]) local fun openOut std p = case p of File s => (FileSys.creat (s, readWrite), Child.Term) | FileDesc f => (f, Child.Term) | Pipe => let val {infd, outfd} = IO.pipe () val () = setCloseExec infd in (outfd, Child.FileDesc infd) end | Self => (std, Child.Term) in fun openStdout p = openOut FileSys.stdout p fun openStderr p = openOut FileSys.stderr p end fun openStdin p = case p of File s => (FileSys.openf (s, FileSys.O_RDONLY, FileSys.O.flags []), Child.Term) | FileDesc f => (f, Child.Term) | Pipe => let val {infd, outfd} = IO.pipe () val () = setCloseExec outfd in (infd, Child.FileDesc outfd) end | Self => (FileSys.stdin, Child.Term) fun close p fd = case p of File _ => IO.close fd | FileDesc _ => IO.close fd | Pipe => IO.close fd | _ => () end datatype ('stdin, 'stdout, 'stderr) t = T of {pid: Process.pid, (* if useWindowsProcess, * then this is a Windows process handle * and can't be passed to * Posix.Process.* functions. *) status: Posix.Process.exit_status option ref, stderr: ('stderr, input) Child.t, stdin: ('stdin, output) Child.t, stdout: ('stdout, input) Child.t} local fun make f (T r) = f r in val getStderr = fn z => make #stderr z val getStdin = fn z => make #stdin z val getStdout = fn z => make #stdout z end fun ('a, 'b) protect (f: 'a -> 'b, x: 'a): 'b = let val () = Mask.block Mask.all in DynamicWind.wind (fn () => f x, fn () => Mask.unblock Mask.all) end local fun reap reapFn (T {pid, status, stderr, stdin, stdout, ...}) = case !status of NONE => let val _ = Child.close (!stdin, !stdout, !stderr) val st = reapFn pid in status := SOME st ; st end | SOME st => st in fun reapForFork p = reap (fn pid => let (* protect is probably too much; typically, one * would only mask SIGINT, SIGQUIT and SIGHUP. *) val (_, st) = protect (Process.waitpid, (Process.W_CHILD pid, [])) in st end) p fun reapForCreate p = reap (fn pid => let val pid' = PId.toRep pid val status' = ref (C_Status.fromInt 0) val () = SysCall.simple (fn () => PrimitiveFFI.Windows.Process.getexitcode (pid', status')) in Process.fromStatus' (!status') end) p end val reap = fn p => (if useWindowsProcess then reapForCreate else reapForFork) p local fun kill killFn (p as T {pid, status, ...}, signal) = case !status of NONE => let val () = killFn (pid, signal) in ignore (reap p) end | SOME _ => () in fun killForFork p = kill (fn (pid, signal) => Process.kill (Process.K_PROC pid, signal)) p fun killForCreate p = kill (fn (pid, signal) => SysCall.simple (fn () => PrimitiveFFI.Windows.Process.terminate (PId.toRep pid, Signal.toRep signal))) p end val kill = fn (p, signal) => (if useWindowsProcess then killForCreate else killForFork) (p, signal) fun launchWithFork (path, args, env, stdin, stdout, stderr) = case protect (Process.fork, ()) of NONE => (* child *) let fun dup2 (old, new) = if old = new then () else (IO.dup2 {old = old, new = new}; IO.close old) val args = path :: args val execTh = case env of NONE => (fn () => Process.exec (path, args)) | SOME env => (fn () => Process.exece (path, args, env)) in dup2 (stdin, FileSys.stdin) ; dup2 (stdout, FileSys.stdout) ; dup2 (stderr, FileSys.stderr) ; ignore (execTh ()) ; Process.exit 0w127 (* just in case *) end | SOME pid => pid (* parent *) fun strContains seps s = CharVector.exists (Char.contains seps) s (* In MinGW, a string must be escaped if it contains " \t" or is "". * Escaping means adds "s on the front and end. Any quotes inside * must be escaped with \. Any \s already in the string must be * doubled ONLY when they precede a " or the end of string. *) fun mingwEscape (l, 0) = l | mingwEscape (l, i) = mingwEscape (#"\\"::l, i-1) fun mingwFold (#"\\", (l, escapeCount)) = (#"\\"::l, escapeCount+1) | mingwFold (#"\"", (l, escapeCount)) = (#"\"" :: mingwEscape (#"\\"::l, escapeCount), 0) | mingwFold (x, (l, _)) = (x :: l, 0) val mingwQuote = mingwEscape o CharVector.foldl mingwFold ([#"\""], 0) fun mingwEscape y = if not (strContains " \t\"" y) andalso y<>"" then y else String.implode (List.rev (#"\"" :: mingwQuote y)) fun cygwinEscape y = if not (strContains " \t\"\r\n\f'" y) andalso y<>"" then y else concat ["\"", String.translate (fn #"\"" => "\\\"" | #"\\" => "\\\\" | x => String.str x) y, "\""] val cmdEscapeCreate = if MLton.Platform.OS.host = MLton.Platform.OS.MinGW then mingwEscape else cygwinEscape val cmdEscapeSpawn = if MLton.Platform.OS.host = MLton.Platform.OS.MinGW then mingwEscape else (fn s => s) fun launchWithCreate (path, args, env, stdin, stdout, stderr) = let val path' = NullString.nullTerm (let open MLton.Platform.OS in case host of Cygwin => Cygwin.toFullWindowsPath path | MinGW => path | _ => raise Fail "MLton.Process.launchWithCreate: path'" end) val args' = NullString.nullTerm (String.concatWith " " (List.map cmdEscapeCreate (path :: args))) val env' = Option.map (fn env => NullString.nullTerm ((String.concatWith "\000" env) ^ "\000")) env val stdin' = FileDesc.toRep stdin val stdout' = FileDesc.toRep stdout val stderr' = FileDesc.toRep stderr val createTh = case env' of NONE => (fn () => PrimitiveFFI.Windows.Process.createNull (path', args', stdin', stdout', stderr')) | SOME env' => (fn () => PrimitiveFFI.Windows.Process.create (path', args', env', stdin', stdout', stderr')) val pid' = SysCall.simpleResult' ({errVal = C_PId.castFromFixedInt ~1}, fn () => createTh ()) val pid = PId.fromRep pid' in pid end val launch = fn z => (if useWindowsProcess then launchWithCreate else launchWithFork) z fun create {args, env, path, stderr, stdin, stdout} = if not (FileSys.access (path, [FileSys.A_EXEC])) then PosixError.raiseSys PosixError.noent else let val () = TextIO.flushOut TextIO.stdOut val (fstdin, cstdin) = Param.openStdin stdin val (fstdout, cstdout) = Param.openStdout stdout val (fstderr, cstderr) = Param.openStderr stderr val closeStdio = fn () => (Param.close stdin fstdin ; Param.close stdout fstdout ; Param.close stderr fstderr) val pid = launch (path, args, env, fstdin, fstdout, fstderr) handle ex => (closeStdio () ; Child.close (cstdin, cstdout, cstderr) ; raise ex) val () = closeStdio () in T {pid = pid, status = ref NONE, stderr = ref cstderr, stdin = ref cstdin, stdout = ref cstdout} end fun spawne {path, args, env} = if useWindowsProcess then let val args = List.map cmdEscapeSpawn args val path = NullString.nullTerm path val args = CUtil.C_StringArray.fromList args val env = CUtil.C_StringArray.fromList env in (PId.fromRep o SysCall.simpleResult') ({errVal = C_PId.castFromFixedInt ~1}, fn () => Prim.spawne (path, args, env)) end else case Posix.Process.fork () of NONE => (Posix.Process.exece (path, args, env) handle _ => () ; Posix.Process.exit 0w127) | SOME pid => pid fun spawn {args, path}= spawne {args = args, env = ProcEnv.environ (), path = path} fun spawnp {args, file} = if useWindowsProcess then let val file = NullString.nullTerm file val args = List.map cmdEscapeSpawn args val args = CUtil.C_StringArray.fromList args in (PId.fromRep o SysCall.simpleResult') ({errVal = C_PId.castFromFixedInt ~1}, fn () => Prim.spawnp (file, args)) end else case Posix.Process.fork () of NONE => (Posix.Process.execp (file, args) handle _ => () ; Posix.Process.exit 0w127) | SOME pid => pid open Exit end