Пример управления терминалом: программа tscript
Программа tscript устроена следующим образом: при старте она выполняет вызовы fork и ехес для запуска пользовательской оболочки. Далее все данные, записываемые на терминал оболочкой, сохраняются в файле, при этом оболочка ничего об этом не знает и продолжает вести себя так, как будто она полностью управляет дисциплиной линии связи и, следовательно, терминалом. Логическая структура программы tscript показана на рис. 9.6.
Рис. 9.6. Использование псевдотерминала в программе tscript
Основные элементы схемы:
tscript | Первый запускаемый процесс. После инициализации псевдотерминала и дисциплины линии связи этот процесс использует вызовы fork и ехес для создания оболочки shell. Теперь программа tscript играет две роли. Первая состоит в чтении из настоящего терминала и записи всех данных в порт ведущего устройства псевдотерминала. (Все данные, записываемые в ведущее устройство псевдотерминала, непосредственно передаются на ведомое устройство псевдотерминала.) Вторая роль состоит в чтении вывода программы оболочки shell при помощи псевдотерминала и копировании этих данных на настоящий терминал и в выходной файл | ||
shell |
| Пользовательская оболочка. Перед запуском процесса shell модули дисциплины линии связи STREAM вставляются в ведомое устройство. Стандартный ввод, стандартный вывод и стандартный вывод диагностики оболочки перенаправляются в ведомое устройство псевдотерминала |
Первая задача программы tscript состоит в установке обработчика сигнала SIGCHLD и в открытии псевдотерминала. Затем программа создает процесс shell. И, наконец, вызывается процедура script. Эта процедура отслеживает два потока данных: ввод с клавиатуры (стандартный ввод), который она передает ведущему устройству псевдотерминала, и ввод с ведущего устройства псевдотерминала, передаваемый на стандартный вывод и записываемый в выходной файл.
(* Программа tscript управление терминалом *)
(* Хотя в Linux этот пример и не работает... *)
uses linux,stdio;
var
dattr:termios;
var
act:sigactionrec;
mfd, sfd:longint;
err:integer;
buf:array [0..511] of char;
mask:sigset_t;
begin
(* Сохранить текущие установки терминала *)
tcgetattr (0, dattr);
(* Открыть псевдотерминал *)
err := pttyopen (mfd, sfd);
if err <> 1 then
begin
writeln (stderr, 'pttyopen: ', err);
perror ('Ошибка при открытии псевдотерминала');
halt (1);
end;
(* Установить обработчик сигнала SIGCHLD *)
act.handler.sh := @catch_child;
sigfillset (@mask);
act.sa_mask:=mask.__val[0];
sigaction (SIGCHLD, @act, nil);
(* Создать процесс оболочки *)
case fork of
-1: (* ошибка *)
begin
perror ('Ошибка вызова оболочки');
halt (2);
end;
0: (* дочерний процесс *)
begin
fdclose (mfd);
runshell (sfd);
end;
else (* родительский процесс *)
begin
fdclose (sfd);
script (mfd);
end;
end;
end.
Основная программа использует четыре процедуры. Первая из них называется catch_child. Это обработчик сигнала SIGCHLD. При получении сигнала SIGCHLD процедура catch_child восстанавливает атрибуты терминала и завершает работу.
procedure catch_child (signo:integer);cdecl;
begin
tcsetattr (0, TCSAFLUSH, dattr);
halt (0);
end;
Вторая процедура, pttyopen, открывает псевдотерминал.
function pttyopen (var masterfd, slavefd:longint):integer;
var
slavenm:pchar;
begin
(* Открыть псевдотерминал -
* получить дескриптор файла главного устройства *)
masterfd := fdopen ('/dev/ptmx', Open_RDWR);
if masterfd = -1 then
begin
pttyopen:=-1;
exit;
end;
(* Изменить права доступа для ведомого устройства *)
if grantpt (masterfd) = -1 then
begin
fdclose (masterfd);
pttyopen:=-2;
exit;
end;
(* Разблокировать ведомое устройство, связанное с mfd *)
if unlockpt (masterfd) = -1 then
begin
fdclose (masterfd);
pttyopen:=-3;
exit;
end;
(* Получить имя ведомого устройства и затем открыть его *)
slavenm := ptsname (masterfd);
if slavenm = nil then
begin
fdclose (masterfd);
pttyopen:=-4;
exit;
end;
slavefd := fdopen (slavenm, Open_RDWR);
if slavefd = -1 then
begin
fdclose (masterfd);
pttyopen:=-5;
exit;
end;
(* Создать дисциплину линии связи *)
ioctl (slavefd, I_PUSH, pchar('ptem'));
if linuxerror>0 then
begin
fdclose (masterfd);
fdclose (slavefd);
pttyopen:=-6;
exit;
end;
ioctl (slavefd, I_PUSH, pchar('ldterm'));
if linuxerror>0 then
begin
fdclose (masterfd);
fdclose (slavefd);
pttyopen:=-7;
exit;
end;
pttyopen:=1;
end;
Третья процедура – процедура runshell. Она выполняет следующие задачи:
– вызывает setpgrp, чтобы оболочка выполнялась в своей группе процессов. Это позволяет оболочке полностью управлять обработкой сигналов, в особенности в отношении управления заданиями;
– вызывает системный вызов dup2 для перенаправления дескрипторов stdin, stdout и stderr на дескриптор файла ведомого устройства. Это особенно важный шаг;
– запускает оболочку при помощи вызова ехес, которая выполняется до тех пор, пока не будет прервана пользователем.
procedure runshell (sfd:longint);
begin
setpgrp;
dup2 (sfd, 0);
dup2 (sfd, 1);
dup2 (sfd, 2);
execl ('/bin/sh -i');
end;
Теперь рассмотрим саму процедуру script. Первым действием процедуры script является изменение дисциплины линии связи так, чтобы она работала в режиме прямого доступа. Это достигается получением текущих атрибутов терминала и изменением их при помощи вызова tcsetattr. Затем процедура script открывает файл output и использует системный вызов select (обсуждавшийся в главе 7) для обеспечения одновременного ввода со своего стандартного ввода и ведущего устройства псевдотерминала. Если данные поступают со стандартного ввода, то процедура script передает их без изменений ведущему устройству псевдотерминала. При поступлении же данных с ведущего устройства псевдотерминала процедура script записывает эти данные в терминал пользователя и в файл
output.
procedure script(mfd:longint);
var
nread, ofile:longint;
_set, master:fdset;
attr:termios;
buf:array [0..511] of char;
begin
(* Перевести дисциплину линии связи в режим прямого доступа *)
tcgetattr (0, attr);
attr.c_cc[VMIN] := 1;
attr.c_cc[VTIME] := 0;
attr.c_lflag := attr.c_lflag and not (ISIG or ECHO or ICANON);
tcsetattr (0, TCSAFLUSH, attr);
(* Открыть выходной файл *)
ofile := fdopen ('output', Open_CREAT or Open_WRONLY or Open_TRUNC, octal(0666));
(* Задать битовые маски для системного вызова select *)
FD_ZERO (master);
FD_SET (0, master);
FD_SET (mfd, master);
(* Вызов select осуществляется без таймаута,
* и будет заблокирован до наступления события. *)
_set := master;
while select (mfd + 1, @_set, nil, nil, nil) > 0 do
begin
(* Проверить стандартный ввод *)
if FD_ISSET (0, _set) then
begin
nread := fdread (0, buf, 512);
fdwrite (mfd, buf, nread);
end;
(* Проверить главное устройство *)
if FD_ISSET (mfd, _set) then
begin
nread := fdread (mfd, buf, 512);
write (ofile, buf, nread);
write (1, buf, nread);
end;
_set := master;
end;
end;
Следующий сеанс демонстрирует работу программы tscript. Комментарии, обозначенные символом #, показывают, какая из оболочек выполняется в данный момент.
$ ./tscript
$ ls -l tscript # работает новая оболочка
-rwxr-xr-x 1 spate fcf 6984 Jan 22 21:57 tscript
$ head -2 /etc/passwd # выполняется в новой оболочке
root:х:0:1:0000-Admin(0000):/:/bin/ksh
daemon:x:1:1:0000-Admin(0000):/:
$ exit # выход из новой оболочки
$ cat output # работает исходная оболочка
-rwxr-xr-x 1 spate fcf 6984 Jan 22 21:57 tscript
root:х:0:1:0000-Admin(0000):/:/bin/ksh
daemon:x:1:1:0000-Admin(0000):/:
Упражнение 9.5. Добавьте к программе обработку ошибок и возможность задания в качестве параметра имени выходного файла. Если имя не задано, используйте по умолчанию имя output.
Упражнение 9.6. Эквивалентная стандартная программа UNIX script позволяет задать параметр -а, который указывает на необходимость дополнения файла output (содержимое файла не уничтожается). Реализуйте аналогичную возможность в программе tscript.