UnixSpawnImpl.mesa
Copyright Ó 1989, 1990, 1991, 1992 by Xerox Corporation. All rights reserved.
Michael Plass, January 14, 1993 5:12 pm PST
Peter B. Kessler, January 16, 1990 10:32:25 am PST
DIRECTORY Ascii, EditedStream, IO, Process, PseudoTerminal, RefText, Rope, UnixErrno, UnixSpawn, UnixSysCallExtensions, UnixSysCalls, UnixTypes, UXStrings;
This is just like PlumberImpl, except that there are no dependencies on Viewers interfaces.
UnixSpawnImpl: CEDAR MONITOR LOCKS wire USING wire: Wire
IMPORTS EditedStream, IO, Process, PseudoTerminal, RefText, Rope, UnixErrno, UnixSysCallExtensions, UXStrings, UnixSysCalls
EXPORTS UnixSpawn
~ BEGIN OPEN PT: PseudoTerminal;
ROPE: TYPE ~ Rope.ROPE;
Error: PUBLIC ERROR [code: ATOM, msg: ROPE] ~ CODE;
PseudoTerminal: TYPE ~ PT.PseudoTerminal;
AllocatePseudoTerminal: PROC RETURNS [PseudoTerminal] ~ {
ENABLE PT.Error => Error[code, msg];
pseudoTerminal: PseudoTerminal ¬ PT.Allocate[];
RETURN [pseudoTerminal]
};
ClosePseudoTerminal: PROC [pty: PseudoTerminal] ~ {
ENABLE PT.Error => Error[code, msg];
PT.Close[pty];
};
CHARPtrFromRefText: PROC [refText: REF TEXT] RETURNS [UnixTypes.CHARPtr] ~ TRUSTED {
charPtr: UnixTypes.CHARPtr ~
LOOPHOLE[LOOPHOLE[refText, LONG POINTER]+SIZE[TEXT[0]]];
RETURN [charPtr];
};
CopyFDToStream: PROC [in: UnixTypes.FileDescriptor, out: IO.STREAM, wire: Wire] ~ {
bufLength: NAT ~ 1; -- Reading one byte at a time seems to circumvent a PCR/Unix interaction problem that causes output at the end to get lost.
buffer: REF TEXT ~ RefText.New[bufLength];
bufPtr: UnixTypes.CHARPtr ~ CHARPtrFromRefText[refText: buffer];
BEGIN
ENABLE {
IO.Error => {
NoteResult[wire, LIST[$ioerror, NEW[INT ¬ ORD[ec]]]];
CONTINUE;
};
ABORTED => {
NoteResult[wire, $aborted];
CONTINUE;
};
};
WHILE Active[wire] DO TRUSTED {
res: INT ¬ 0;
res ¬ UnixSysCalls.Read[d: in, buf: bufPtr, nBytes: bufLength];
IF UserAborted[wire] THEN EXIT;
IF res <= 0 THEN {
NoteResult[wire, LIST[$uioerror, NEW[INT ¬ ORD[UnixErrno.GetErrno[]]]]];
RETURN;
};
buffer.length ¬ res;
IO.PutBlock[self: out, block: buffer];
Process.CheckForAbort[];
} ENDLOOP;
NoteResult[wire, $terminated];
END;
};
CopyStreamToFD: PROC [in: IO.STREAM, out: UnixTypes.FileDescriptor, wire: Wire] = {
buffer: REF TEXT ~ RefText.New[1];
bufPtr: UnixTypes.CHARPtr ~ CHARPtrFromRefText[refText: buffer];
BEGIN
ENABLE {
IO.Error => {
NoteResult[wire, LIST[$ioerror, NEW[INT ¬ ORD[ec]]]];
CONTINUE;
};
IO.Rubout => {
echo: IO.STREAM ¬ NIL;
echo ¬ EditedStream.GetEcho[stream ! IO.Error => CONTINUE];
IO.Reset[stream];
IF echo # NIL THEN IO.PutRope[echo, "^C"];
buffer[0] ¬ 'C-ORD['@];
[] ¬ UnixSysCalls.Write[d: out, buf: bufPtr, nBytes: 1];
RETRY;
};
ABORTED => {
NoteResult[wire, $aborted];
CONTINUE;
};
};
WHILE Active[wire] DO
res: INT ¬ 0;
nBytes: INT ¬ 0;
IF AbortEnable[wire, TRUE] THEN ERROR ABORTED;
nBytes ¬ IO.GetBlock[in, buffer, 0, 1];
IF AbortEnable[wire, FALSE] THEN {
Process.CancelAbort[Process.GetCurrent[]]; -- An abort was requested after the last time the IO.GetBlock checked; we are about to terminate anyway, so we would rather do it cleanly.
IF nBytes # 0 THEN {IO.Backup[in, buffer[0]]}; -- not meant for us!
EXIT;
};
IF nBytes = 0 THEN {
res: UnixTypes.RES ¬ UnixSysCalls.Close[out];
IF res # $success THEN {
IF UnixErrno.GetErrno[] = $EABORTED THEN ERROR ABORTED;
NoteResult[wire, LIST[$uioerror, NEW[INT ¬ ORD[UnixErrno.GetErrno[]]]]];
RETURN;
};
NoteResult[wire, $eof];
RETURN;
};
IF AbortEnable[wire, TRUE] THEN ERROR ABORTED;
res ¬ UnixSysCalls.Write[d: out, buf: bufPtr, nBytes: nBytes];
[] ¬ AbortEnable[wire, FALSE];
IF res <= 0 THEN {
IF UnixErrno.GetErrno[] = $EABORTED THEN ERROR ABORTED;
NoteResult[wire, LIST[$uioerror, NEW[INT ¬ ORD[UnixErrno.GetErrno[]]]]];
RETURN;
};
ENDLOOP;
NoteResult[wire, $terminated];
END;
};
Wire: TYPE ~ REF WireRep ¬ NIL;
WireRep: TYPE ~ MONITORED RECORD [
active: BOOLEAN ¬ FALSE,
userAborted: BOOLEAN ¬ FALSE,
getBlocked: BOOLEAN ¬ FALSE,
connectedTo: ROPE ¬ NIL,
process: PROCESS ¬ NIL,
result: REF ¬ NIL
];
Active: ENTRY PROC [wire: Wire] RETURNS [BOOL] ~ {
RETURN [wire.active]
};
UserAborted: ENTRY PROC [wire: Wire] RETURNS [BOOL] ~ {
RETURN [wire.userAborted]
};
AbortEnable: ENTRY PROC [wire: Wire, bool: BOOL] RETURNS [BOOL] ~ {
wire.getBlocked ¬ bool;
RETURN [NOT wire.active];
};
NoteResult: ENTRY PROC [wire: Wire, result: REF] ~ {
wire.result ¬ result;
};
BreakWire: ENTRY PROC [wire: Wire, abort: BOOLEAN] RETURNS [process: PROCESS ¬ NIL] ~ {
wire.userAborted ¬ abort;
IF (abort OR wire.getBlocked) AND wire.process # NIL THEN TRUSTED {
Process.Abort[wire.process];
};
process ¬ wire.process;
wire.process ¬ NIL;
wire.active ¬ FALSE;
};
BreakConduit: PROC [conduit: Conduit, abort: BOOLEAN] ~ {
p: ARRAY [0..3) OF PROCESS ¬ ALL[NIL];
BEGIN
ENABLE UNWIND => {
Abort and detach any processed that have not already been joined up.
FOR i: NAT IN [0..3) DO
process: PROCESS ¬ p[i];
IF process # NIL THEN TRUSTED {
ENABLE Process.InvalidProcess => CONTINUE;
Process.Abort[process];
Process.Detach[process];
};
ENDLOOP;
};
p[0] ¬ BreakWire[wire: conduit.stdin, abort: abort];
p[1] ¬ BreakWire[wire: conduit.stdout, abort: abort];
p[2] ¬ BreakWire[wire: conduit.stderr, abort: abort];
FOR i: NAT IN [0..3) DO
process: PROCESS ¬ p[i];
IF process # NIL THEN TRUSTED {
JOIN process;
p[i] ¬ NIL;
};
ENDLOOP;
END;
IF conduit.pty # NIL THEN {
ClosePseudoTerminal[conduit.pty];
};
};
Conduit: TYPE = REF ConduitRep;
ConduitRep: TYPE = RECORD [
pty: PseudoTerminal ¬ NIL,
stdin: Wire,
stdout: Wire,
stderr: Wire,
completeCommand: ROPE ¬ NIL, -- for informational purposes
message: REF
];
MakeConduit: PROC [stdin, stdout, stderr: REF, debug: BOOL] RETURNS [Conduit] ~ {
conduit: Conduit ¬ NEW[ConduitRep ¬ [
stdin: NEW[WireRep ¬ [active: TRUE]],
stdout: NEW[WireRep ¬ [active: TRUE]],
stderr: NIL,
message: IF debug THEN stderr ELSE NIL]];
WITH stdin SELECT FROM
rope: ROPE => conduit.stdin.connectedTo ¬ rope;
refText: REF TEXT => conduit.stdin.connectedTo ¬ Rope.FromRefText[s: refText];
stream: IO.STREAM => {
IF conduit.pty = NIL THEN {
conduit.pty ¬ AllocatePseudoTerminal[];
};
conduit.stdin.connectedTo ¬ conduit.pty.slaveName;
TRUSTED {
conduit.stdin.process ¬ FORK CopyStreamToFD[
in: stream, out: conduit.pty.controllerFD, wire: conduit.stdin]
};
};
ENDCASE => ERROR;
WITH stdout SELECT FROM
rope: ROPE => conduit.stdout.connectedTo ¬ rope;
refText: REF TEXT => conduit.stdout.connectedTo ¬ Rope.FromRefText[s: refText];
stream: IO.STREAM => {
IF conduit.pty = NIL THEN {
conduit.pty ¬ AllocatePseudoTerminal[];
};
conduit.stdout.connectedTo ¬ conduit.pty.slaveName;
TRUSTED {
conduit.stdout.process ¬ FORK CopyFDToStream[
in: conduit.pty.controllerFD, out: stream, wire: conduit.stdout]
};
};
ENDCASE => ERROR;
IF stderr = stdout
THEN { conduit.stderr ¬ conduit.stdout }
ELSE {
conduit.stderr ¬ NEW[WireRep ¬ [active: TRUE]];
WITH stderr SELECT FROM
rope: ROPE => conduit.stderr.connectedTo ¬ rope;
refText: REF TEXT => conduit.stderr.connectedTo ¬ Rope.FromRefText[s: refText];
stream: IO.STREAM => {
IF conduit.pty = NIL THEN {
conduit.pty ¬ AllocatePseudoTerminal[];
};
conduit.stderr.connectedTo ¬ conduit.pty.slaveName;
IF NOT Rope.Equal[conduit.stderr.connectedTo, conduit.stdout.connectedTo] THEN {
If stdout is already reading from the pty, stderr shouldn't.
A pity that stdout and stderr are merged into the output of a pty.
TRUSTED {
conduit.stderr.process ¬ FORK CopyFDToStream[
in: conduit.pty.controllerFD, out: stream, wire: conduit.stderr]
};
}
};
ENDCASE => ERROR;
};
RETURN [conduit];
};
DebugPrint: PROC [conduit: Conduit, rope: ROPE] ~ {
WITH conduit.message SELECT FROM
stream: IO.STREAM => {
IO.PutF[stream, "%l%g%l", [rope["sb"]], [rope[rope]], [rope["SB"]]];
};
ENDCASE => NULL;
};
Cleanup: PROC [conduit: Conduit, abort: BOOLEAN, msg: ROPE] ~ {
in: ROPE ~ conduit.stdin.connectedTo;
out: ROPE ~ conduit.stdout.connectedTo;
err: ROPE ~ conduit.stderr.connectedTo;
BreakConduit[conduit: conduit, abort: abort];
DebugPrint[
conduit: conduit,
rope: IO.PutFLR["%g %g (in=%g, out=%g, err=%g) [[%g]]\n", LIST[
[rope[IF abort THEN "Aborting" ELSE "Terminating"]],
[rope[conduit.completeCommand]],
[rope[in]],
[rope[out]],
[rope[err]],
[rope[msg]]]]];
};
Spawn: PUBLIC PROC [command: ROPE, wd: ROPE, stdin: REF, stdout: REF, stderr: REF, exec: BOOLEAN, tty: BOOLEAN, debug: BOOLEAN] RETURNS [UnixSpawn.SpawnResult] ~ {
conduit: Conduit ~ MakeConduit[stdin: stdin, stdout: stdout, stderr: stderr, debug: debug];
res: INT;
errno: UnixErrno.Errno;
completeCommand: ROPE ¬ command;
FOR i: INT DECREASING IN [0..Rope.Size[completeCommand]) WHILE Rope.Fetch[completeCommand, i] = '\n DO
Hopefully we go through this loop just once!
completeCommand ¬ Rope.Substr[completeCommand, 0, i];
ENDLOOP;
IF exec THEN {
completeCommand ¬ Rope.Concat[base: "exec ", rest: completeCommand];
};
IF tty AND (conduit.pty # NIL) THEN {
completeCommand ¬ Rope.Cat[
r1: completeCommand,
r2: " -t ", r3: conduit.pty.slaveName,
r4: " -p ", r5: conduit.pty.controllerName];
};
conduit.completeCommand ¬ completeCommand;
DebugPrint[
conduit: conduit,
rope: IO.PutFLR["\nSpawning %g (in=%g, out=%g, err=%g, wd=%g, conduit=%g)\n", LIST[
[rope[conduit.completeCommand]],
[rope[conduit.stdin.connectedTo]],
[rope[conduit.stdout.connectedTo]],
[rope[conduit.stderr.connectedTo]],
[rope[wd]],
[cardinal[LOOPHOLE[conduit]]]
]]
];
res ¬ UnixSysCallExtensions.CDSpawn[
cmd: UXStrings.Create[completeCommand],
wd: UXStrings.Create[wd],
stdin: UXStrings.Create[conduit.stdin.connectedTo],
stdout: UXStrings.Create[conduit.stdout.connectedTo],
stderr: UXStrings.Create[conduit.stderr.connectedTo]
! UNWIND => Cleanup[conduit: conduit, abort: TRUE, msg: "Spawn!UNWIND"]];
errno ¬ UnixErrno.GetErrno[];
Cleanup[conduit: conduit, abort: res # 0 AND errno = $EABORTED, msg: "Cleanup"];
RETURN [[
res: res,
errno: errno.ORD,
details: LIST[
NEW[INT ¬ res],
NEW[INT ¬ errno.ORD],
conduit.stdin.result, conduit.stdout.result, conduit.stderr.result
]
]]
};
END.