From: wkt@csadfa.cs.adfa.oz.au (Warren Toomey)
Subject: Hack: Job Control under 7th Edition Unix
Keywords: hack fun but not very proud of it
Organization: Australian Defence Force Academy, Canberra, Australia
Date: Mon, 31 Aug 1992 04:27:39 GMT

[sorry for the size, but if you're gonna explain a good hack, you might
 as well do it properly :-)    Warren]


			7th Edition Job Control

Job control can be achieved under early versions of Unix, such as 7th Edition,
by using the ptrace(2) system call in a manner not intended by its
designers.

Ptrace() was designed to allow a parent process to trace the excution
of a child process, stopping the child under certain conditions, examining
or modifying the contents of the child's memory, and restarting the child.
We use the stopping/restarting abilities of ptrace() to provide job
control.


To permit a child process to be stopped, it must inform the parent that it
wants its execution to be traced, which it does by ptrace(0,0,0,0).
Fortunately, this can be done after the fork() and before the
exec() in the invoke() routine, so that none of our programs
need to be modified to add the ptrace().

When a traced process is executing, it is stopped under the following
conditions:

	+ the process receives a signal, or
	+ the process exec()s

If the shell is wait()ing on the child, it will be informed that the
child has stopped, and can determine the signal that caused the process
to stop (SIGTRAP in the case that the process exec()d). It is then
able, using ptrace() with various arguments, to terminate or restart
the process. At the same time, the shell can also deliver the signal to
the restarted process, or not deliver the signal -- see the manual for
ptrace(2).

7th Edition job control, thus, is not so much a matter of stopping a process
when requested to by the user, as ensuring that the process is always
restarted, except when the user wants it to stop.


Restarting stopped processes is straightforward. Stopping a running foreground
process, however, is difficult, as there is no terminal key that, when pressed,
will inform the shell to stop the process; indeed, the shell is most likely
blocked wait()ing for the process to terminate.

Two keys that do affect the execution of a foreground process are Int
(usually ^C or DEL), which sends a SIGINT to the process, and
Quit (usually ^\), which sends a SIGQUIT to the process. The
latter cannot be caught or ignored by the process, and the delivery of
SIGQUIT causes the process to terminate, usually with a core dump. However,
when a process is being traced, pending signals are not delivered;
instead, the process is stopped, and the parent informed about the pending
signal. The parent can choose to terminate or restart the process, delivering
or ignoring the signal as described above.


Therefore, with ^C being frequently used, and ^\ rarely used,
we decided to reinterpret the meaning of ^\ and SIGQUIT to mean
``stop the process''.  The SIGQUIT from ^\ is never delivered by the
shell, but all other signals (including the SIGINT from ^C) are
delivered. Users can then re-bind the Quit key with stty(1) to
be the more traditional Stop key, ^Z.

The waitfor() routine must be modified to restart a process stopped
on any signal but SIGQUIT, and deliver any signal except SIGTRAP, which is
caused when the process exec()s. The code is shown below:

void waitfor(pid)
  int pid;
{
  struct job *thisjob;
  int wpid;
  bool subshell=FALSE;
  int stopsig;
  int status;

  if (pid<0) { pid= -pid; subshell= TRUE; }
  while (1)
  {
    if (pid == 0) return;
    wpid = wait(&status);               /* Get a process' status */
    if (wpid == -1 || wpid == 0) break;
    thisjob = findjob(wpid);
    if (thisjob == NULL) continue;
    if (WIFSTOPPED(status))             /* If it was stopped, */
     { stopsig= WSTOPSIG(status);
       if (stopsig!=SIGQUIT)            /* ignore any SIGQUITs, */
        { if (stopsig== SIGTRAP)
            ptrace(7,wpid,1,0);         /* keep it going after exec */
          else
            ptrace(7,wpid,1,stopsig);   /* or deliver the signal */
          continue;
        }
     }     /* SIGQUIT falls out to below where job is marked stopped */
    thisjob->status = status;
    thisjob->changed = TRUE;
    if (pid == wpid) return;
  }
}


WIth a method of stopping foreground jobs now available, we can introduce
the builtins used to stop a background job, and to move a stopped job to
the foreground or background.

The bg builtin is implemented by bg(), which converts its
arguments to a process-id, and then restarts that process by doing
ptrace(7,pid,1,0); the job's status is marked RUNBG, and the current
job pointer is updated.

The fg builtin is implemented by fg(). Fg can bring to the
foreground either a background or a stopped job. The former is already running;
the latter must be restarted with ptrace(7,pid,1,0). Once the job is
running, the current job pointer is updated, and the pid returned so that
the shell can waitfor() the new foreground job.


Finally, two more modifications must be made to the shell before job control
can work. The kill builtin cannot deliver a signal to a stopped job.
Instead, if the specified job is stopped, Kill() must restart the job
and deliver the signal with ptrace(7,pid,1,signal).

Secondly, when a background job is invoke()d, its exec() will
cause it to be stopped on a SIGTRAP. However, the shell will never
waitfor() the job, and so it will remain stopped until waitfor()
is called when a foreground job is exec()d. Therefore, invoke()
must be modified to restart an exec()d background job with
ptrace(7,pid,1,0).


Newsgroups: alt.hackers