From 2359389570a5ba761fceb5ba1db295a1df8cbc64 Mon Sep 17 00:00:00 2001 From: cinap_lenrek Date: Sat, 30 Nov 2019 20:10:08 +0100 Subject: [PATCH] os(1): add c implementation of inferno os command and cmd(3) device manpages this is a reimplementation of infernos os(1) command, which allows running commands in the underhying host operating system when inferno runs in hosted mode (emu). but unlike inferno, we want to use it to run commands on the client side of a inferno or drawterm session from the plan9 cpu server, so it defaults to /mnt/term/cmd for the mountpoint. --- sys/man/1/os | 97 ++++++++++++++++++ sys/man/3/cmd | 223 ++++++++++++++++++++++++++++++++++++++++ sys/src/cmd/os.c | 261 +++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 581 insertions(+) create mode 100644 sys/man/1/os create mode 100644 sys/man/3/cmd create mode 100644 sys/src/cmd/os.c diff --git a/sys/man/1/os b/sys/man/1/os new file mode 100644 index 000000000..708db1c67 --- /dev/null +++ b/sys/man/1/os @@ -0,0 +1,97 @@ +.TH OS 1 +.SH NAME +os \- interface to host OS commands (drawterm only) +.SH SYNOPSIS +.B os +[ +.B -b +] [ +.B -m +.I mountpoint +] [ +.BI -d " dir" +] [ +.B -n +] [ +.BI -N " level" +] +.I cmd +[ +.IR arg ... +] +.SH DESCRIPTION +.I Os +uses a +.IR cmd (3) +device to execute a command, +.IR cmd , +on a host system. +If the +.B -m +option is given, +.I os +uses the device at +.IR mountpoint , +otherwise it is asssumed to be at +.BR /mnt/term/cmd . +.PP +The +.B -d +option causes the command to run in directory +.IR dir ; +an error results and the command will not run if +.I dir +does not exist or is inaccessible. +The standard output and standard error of the command appear on the standard output +and standard error streams of the +.I os +command itself. +.I Os +copies the standard input to the remote command's standard input; redirect +.IR os 's +input to +.B /dev/null +if there is no input to the command. +.I Os +terminates when +.I cmd +does, and its exit status reflects the status of +.I cmd +(if available). +.PP +If the +.I os +command is killed or exits (eg, for lack of input and output), +the host's own process control operations are used to (attempt to) kill +.IR cmd , +if it is still running. +The +.B -b +(background) option suppresses that behaviour. +.PP +The +.B -n +option causes +.I cmd +to run with less than normal priority (`nice'). +The +.B -N +option sets low priority to a particular +.I level +from 1 to 3. +.SH FILES +.B /mnt/term/cmd/clone +.SH SOURCE +.B /sys/src/cmd/os.c +.SH "SEE ALSO" +.IR rcpu (1), +.IR cmd (3) +.SH DIAGNOSTICS +The exit status of +.I os +reflects any error that occurs when starting +.I cmd +and, if it starts successfully, the status of +.I os +is the exit status of +.IR cmd . diff --git a/sys/man/3/cmd b/sys/man/3/cmd new file mode 100644 index 000000000..c3a0c745d --- /dev/null +++ b/sys/man/3/cmd @@ -0,0 +1,223 @@ +.TH CMD 3 +.SH NAME +cmd \- interface to host operating system commands +.SH SYNOPSIS +.B bind -a '#C' / +.PP +.B /cmd/clone +.br +.BI /cmd/ n /ctl +.br +.BI /cmd/ n /data +.br +.BI /cmd/ n /stderr +.br +.BI /cmd/ n /status +.br +.BI /cmd/ n /wait +.SH DESCRIPTION +.I Cmd +provides a way to run commands in the underlying operating system's +command interpreter of drawterm or when Inferno is running hosted. +It serves a three-level directory that is conventionally bound +behind the root directory. +The top of the hierarchy is a directory +.BR cmd , +that contains a +.B clone +file and zero or more numbered directories. +Each directory represents a distinct connection to the host's command interpreter. +The directory contains five files: +.BR ctl , +.BR data , +.BR stderr , +.B status +and +.BR wait , +used as described below. +Opening the +.B clone +file reserves a connection: it is equivalent to opening the +.B ctl +file of an unused connection directory, creating a new one if necessary. +.PP +The file +.B ctl +controls a connection. +When read, it returns the decimal number +.I n +of its connection directory. +Thus, opening and reading +.B clone +allocates a connection directory and reveals the number of the allocated directory, +allowing the other files to be named (eg, +.BI /cmd/ n /data\fR). +.PP +.B Ctl +accepts the following textual commands, allowing quoting as interpreted by +.IR parsecmd (2): +.TP +.BI "dir " wdir +Run the host command in directory +.IR wdir , +which is a directory +.I "on the host system" . +Issue this request before starting the command. +By default, commands are run in the Inferno root directory on the host system. +.TP +.BI "exec " "command args ..." +Spawn a host process to run the +.I command +with arguments as given. +The write returns with an error, setting the error string, if anything prevents +starting the command. +If write returns successfully, the command has started, and its standard input and +output may be accessed through +.BR data , +and its error output accessed through +.B stderr +(see below). +If arguments containing white space are quoted (following the conventions of +.IR rc (1) +or +.IR parsecmd (2)), +they are requoted by +.I cmd +using the host command interpreter's conventions so that +.I command +sees exactly the same arguments as were written to +.BR ctl . +.TP +.B kill +Kill the host command immediately. +.TP +.B killonclose +Set the device to kill the host command when the +.B ctl +file is closed (normally all files must be closed, see below). +.TP +.BI nice " \fR[\fPn\fR]\fP" +Run the host command at less than normal scheduling priority. +Issue this request before starting the command. +The optional value +.IR n , +in the range 1 to 3, +indicates the degree of `niceness' (default: 1). +.PP +The +.B data +file provides a connection to the input and output of a previously-started +host command. +It must be opened separately for reading and for writing. +When opened for reading, it returns data that the command writes to its standard output; when closed, further writes by the command will receive the host +equivalent of `write to closed pipe'. +When opened for writing, data written to the file +can be read by the command on its standard input; when closed, further reads by +the command will see the host equivalent of `end of file'. +(Unfortunately there is no way to know when the command needs input.) +.PP +The +.B stderr +file provides a similar read-only connection to the error output from the command. +If the +.B stderr +file is not opened, the error output will be discarded. +.PP +Once started, a host command runs until it terminates or until it is killed, +by using the +.B kill +or +.B killonclose +requests above, or by closing all +.BR ctl , +.B data +and +.B wait +files for a connection. +.PP +The read-only +.B status +file provides a single line giving the status of the connection (not the command), of the form: +.IP +.BI cmd/ "n opens state wdir arg0" +.PP +where the fields are separated by white space. The meaning of each field is: +.TP +.I n +The +.B cmd +directory number. +.TP +.I opens +The decimal number of open file descriptors for +.BR ctl , +.B data +and +.BR wait . +.TP +.I state +The status of the interface in directory +.IR n : +.RS +.TF Execute +.TP +.B Open +Allocated for use but not yet running a command. +.TP +.B Execute +Running a command. +.TP +.B Done +Command terminated: status available in the +.B status +file (or via +.BR wait ). +.TP +.B Close +Command completed. Available for reallocation via +.BR clone . +.RE +.PD +.TP +.I wdir +The command's initial working directory on the host. +.TP +.I arg0 +The host command name (without arguments). +.PP +The read-only +.B wait +file must be opened before starting a command via +.BR ctl . +When read, it blocks until the command terminates. +The read then returns with a single status line, to be +parsed using +.B tokenize +(see +.IR getfields (2)). +There are five fields: +host process ID (or 0 if unknown); +time the command spent in user code in milliseconds (or 0); +time spent in system code in milliseconds (or 0); +real time in milliseconds (or 0); +and a string giving the exit status of the command. +The exit status is host-dependent, except that an empty string +means success, and a non-empty string contains a diagnostic. +.PP +.SS "Command execution" +In all cases, the command runs in the host operating system's +own file name space. +All file names will be interpreted in that space, not Plan9's. +For example, on Unix +.B / +refers to the host's file system root, not Plan9's; +the effects of mounts and binds will not be visible. +.SH "SEE ALSO" +.IR os (1) +.SH DIAGNOSTICS +A +.B write +to +.B ctl +returns with an error and sets the error string if +a command cannot be started or killed successfully. diff --git a/sys/src/cmd/os.c b/sys/src/cmd/os.c new file mode 100644 index 000000000..54b873897 --- /dev/null +++ b/sys/src/cmd/os.c @@ -0,0 +1,261 @@ +#include +#include + +enum { + Fstdin, + Fstdout, + Fstderr, + Fwait, + Fctl, + Nfd, +}; + +enum { + Pcopyin, + Pcopyout, + Pcopyerr, + Preadwait, + Npid, +}; + +char *mnt = "/mnt/term/cmd"; +char *dir = nil; +char buf[8192]; +int fd[Nfd] = {-1}; +int pid[Npid]; +int nice, foreground = 1; + +void +killstdin(void) +{ + if(pid[Pcopyin] != 0) + postnote(PNPROC, pid[Pcopyin], "kill"); +} + +void +killproc(void) +{ + if(fd[Fctl] >= 0) + write(fd[Fctl], "kill", 4); +} + +int +catch(void*, char *msg) +{ + if(strcmp(msg, "interrupt") == 0 + || strcmp(msg, "hangup") == 0 + || strcmp(msg, "kill") == 0){ + killproc(); + return 1; + } + return 0; +} + +void +fd01(int fd0, int fd1) +{ + int i; + + if(fd0 >= 0 && fd0 != 0){ + if(dup(fd0, 0) < 0) + sysfatal("dup: %r"); + } + if(fd1 >= 0 && fd1 != 1){ + if(dup(fd1, 1) < 0) + sysfatal("dup: %r"); + } + for(i = 0; i 2) + close(fd[i]); + if(fd[i] == fd0) + fd[i] = 0; + else if(fd[i] == fd1) + fd[i] = 1; + else + fd[i] = -1; + } +} + +void +copy(void) +{ + int n; + + while((n = read(0, buf, sizeof(buf))) > 0) + if(write(1, buf, n) != n) + break; +} + +void +usage(void) +{ + fprint(2, "%s: [ -b ] [ -m mountpoint ] [ -d dir ] [ -n ] [ -N level ] cmd [ arg... ]\n", argv0); + exits("usage"); +} + +void +main(int argc, char **argv) +{ + Waitmsg *w; + char *s; + int n; + + quotefmtinstall(); + + ARGBEGIN { + case 'b': + foreground = 0; + break; + case 'm': + mnt = cleanname(EARGF(usage())); + break; + case 'd': + dir = EARGF(usage()); + break; + case 'n': + nice = 1; + break; + case 'N': + nice = atoi(EARGF(usage())); + break; + default: + usage(); + } ARGEND; + + if(argc < 1) + usage(); + + seprint(buf, &buf[sizeof(buf)], "%s/clone", mnt); + if((fd[Fctl] = open(buf, ORDWR)) < 0) + sysfatal("open: %r"); + s = &buf[strlen(mnt)+1]; + if((n = read(fd[Fctl], s, &buf[sizeof(buf)-1]-s)) < 0) + sysfatal("read clone: %r"); + while(n > 0 && buf[n-1] == '\n') + n--; + s += n; + + seprint(s, &buf[sizeof(buf)], "/wait"); + if((fd[Fwait] = open(buf, OREAD)) < 0) + sysfatal("open: %r"); + + if(foreground){ + seprint(s, &buf[sizeof(buf)], "/data"); + if((fd[Fstdin] = open(buf, OWRITE)) < 0) + sysfatal("open: %r"); + + seprint(s, &buf[sizeof(buf)], "/data"); + if((fd[Fstdout] = open(buf, OREAD)) < 0) + sysfatal("open: %r"); + + seprint(s, &buf[sizeof(buf)], "/stderr"); + if((fd[Fstderr] = open(buf, OREAD)) < 0) + sysfatal("open: %r"); + } + + if(dir != nil){ + if(fprint(fd[Fctl], "dir %q", dir) < 0) + sysfatal("cannot change directory: %r"); + } else { + /* + * try to automatically change directory if we are in + * /mnt/term or /mnt/term/root, but unlike -d flag, + * do not error when the dir ctl command fails. + */ + if((s = strrchr(mnt, '/')) != nil){ + n = s - mnt; + dir = getwd(buf, sizeof(buf)); + if(strncmp(dir, mnt, n) == 0 && (dir[n] == 0 || dir[n] == '/')){ + dir += n; + if(strncmp(dir, "/root", 5) == 0 && (dir[5] == 0 || dir[5] == '/')) + dir += 5; + if(*dir == 0) + dir = "/"; + /* hack for win32: /C:... -> C:/... */ + if(dir[1] >= 'A' && dir[1] <= 'Z' && dir[2] == ':') + dir[0] = dir[1], dir[1] = ':', dir[2] = '/'; + fprint(fd[Fctl], "dir %q", dir); + } + } + } + + if(nice != 0) + fprint(fd[Fctl], "nice %d", nice); + + if(foreground) + fprint(fd[Fctl], "killonclose"); + + s = seprint(buf, &buf[sizeof(buf)], "exec"); + while(*argv != nil){ + s = seprint(s, &buf[sizeof(buf)], " %q", *argv++); + if(s >= &buf[sizeof(buf)-1]) + sysfatal("too many arguments"); + } + + if(write(fd[Fctl], buf, s - buf) < 0) + sysfatal("write: %r"); + + if((pid[Preadwait] = fork()) == -1) + sysfatal("fork: %r"); + if(pid[Preadwait] == 0){ + fd01(fd[Fwait], 2); + if((n = read(0, buf, sizeof(buf)-1)) < 0) + rerrstr(buf, sizeof(buf)); + else { + char *f[5]; + + while(n > 0 && buf[n-1] == '\n') + n--; + buf[n] = 0; + if(tokenize(buf, f, 5) == 5) + exits(f[4]); + } + exits(buf); + } + + if(foreground){ + if((pid[Pcopyerr] = fork()) == -1) + sysfatal("fork: %r"); + if(pid[Pcopyerr] == 0){ + fd01(fd[Fstderr], 2); + copy(); + rerrstr(buf, sizeof(buf)); + exits(buf); + } + if((pid[Pcopyout] = fork()) == -1) + sysfatal("fork: %r"); + if(pid[Pcopyout] == 0){ + fd01(fd[Fstdout], 1); + copy(); + rerrstr(buf, sizeof(buf)); + exits(buf); + } + if((pid[Pcopyin] = fork()) == -1) + sysfatal("fork: %r"); + if(pid[Pcopyin] == 0){ + fd01(0, fd[Fstdin]); + copy(); + rerrstr(buf, sizeof(buf)); + exits(buf); + } + } + fd01(fd[Fctl], 2); + atexit(killstdin); + atnotify(catch, 1); + + while((w = wait()) != nil){ + if((s = strstr(w->msg, ": ")) == nil) + s = w->msg; + else + s += 2; + for(n = 0; n < Npid; n++){ + if(pid[n] == w->pid){ + pid[n] = 0; + break; + } + } + if(n == Preadwait) + exits(s); + free(w); + } +} -- 2.44.0