xref: /minix3/minix/commands/term/term.c (revision 433d6423c39e34ec4b79c950597bb2d236f886be)
1 /* term - terminal simulator		Author: Andy Tanenbaum */
2 
3 /* This program allows the user to turn a MINIX system into a dumb
4  * terminal to communicate with a remote computer through one of the ttys.
5  * It forks into two processes.  The parent sits in a tight loop copying
6  * from stdin to the tty.  The child sits in a tight loop copying from
7  * the tty to stdout.
8  *
9  * 2 Sept 88 BDE (Bruce D. Evans): Massive changes to make current settings the
10  * default, allow any file as the "tty", support fancy baud rates and remove
11  * references to and dependencies on modems and keyboards, so (e.g.)
12  * a local login on /dev/tty1 can do an external login on /dev/tty2.
13  *
14  * 3 Sept 88 BDE: Split parent again to main process copies from stdin to a
15  * pipe which is copied to the tty.  This stops a blocked write to the
16  * tty from hanging the program.
17  *
18  * 11 Oct 88 BDE: Cleaned up baud rates and parity stripping.
19  *
20  * 09 Oct 90 MAT (Michael A. Temari): Fixed bug where terminal isn't reset
21  * if an error occurs.
22  *
23  * Nov 90 BDE: Don't broadcast kill(0, SIGINT) since two or more of these
24  * in a row will kill the parent shell.
25  *
26  * 19 Oct 89 RW (Ralf Wenk): Adapted to MINIX ST 1.1 + RS232 driver. Split
27  * error into error_n and error. Added resetting of the terminal settings
28  * in error.
29  *
30  * 24 Nov 90 RW: Adapted to MINIX ST 1.5.10.2. Forked processes are now
31  * doing an exec to get a better performance. This idea is stolen from
32  * a terminal program written by Felix Croes.
33  *
34  * 01 May 91 RW: Merged the MINIX ST patches with Andys current version.
35  * Most of the 19 Oct 89 patches are deleted because they are already there.
36  *
37  * 10 Mar 96 KJB: Termios adaption, cleanup, command key interface.
38  *
39  * 27 Nov 96 KJB: Add -c flag that binds commands to keys.
40  *
41  * Example usage:
42  *	term			: baud, bits/char, parity from /dev/tty1
43  *	term 9600 7 even	: 9600 baud, 7 bits/char, even parity
44  *	term odd 300 7		:  300 baud, 7 bits/char, odd parity
45  *	term /dev/tty2		: use /dev/tty2 rather than /dev/tty1
46  *				: Any argument starting with "/" is
47  *				: taken as the communication device.
48  *	term 8 57600 /dev/tty2 -atdt4441234	: if an argument begins with
49  *						: - , the rest of that arg is
50  *						: sent to the modem as a
51  *						: dial string
52  */
53 
54 #include <sys/types.h>
55 #include <fcntl.h>
56 #include <termios.h>
57 #include <signal.h>
58 #include <stdlib.h>
59 #include <string.h>
60 #include <unistd.h>
61 #include <stdarg.h>
62 #include <errno.h>
63 #include <sys/wait.h>
64 #include <sys/stat.h>
65 
66 #define CHUNK 1024		/* how much to read at once */
67 
68 char TERM_LINE[] = "/dev/modem";/* default serial port to use */
69 
70 				/* device lock file */
71 char lockfile[] = "/usr/spool/locks/LK.iii.jjj.kkk";
72 
73 char *commdev;			/* communications device a.k.a. "modem". */
74 int commfd;			/* open file no. for comm device */
75 struct termios tccomm;		/* terminal parameters for commfd */
76 struct termios tcstdin;		/* terminal parameters for stdin */
77 struct termios tcsavestdin;	/* saved terminal parameters for stdin */
78 
79 /* Special key to get term's attention. */
80 #define HOTKEY	'\035'		/* CTRL-] */
81 
82 struct param_s {
83   char *pattern;
84   unsigned value;
85   enum { BAD, BITS, PARITY, SPEED } type;
86 } params[] = {
87   { "5",	CS5,		BITS	},
88   { "6",	CS6,		BITS	},
89   { "7",	CS7,		BITS	},
90   { "8",	CS8,		BITS	},
91 
92   { "even",	PARENB,		PARITY	},
93   { "odd",	PARENB|PARODD,	PARITY	},
94 
95   { "50",	B50,		SPEED	},
96   { "75",	B75,		SPEED	},
97   { "110",	B110,		SPEED	},
98   { "134",	B134,		SPEED	},
99   { "200",	B200,		SPEED	},
100   { "300",	B300,		SPEED	},
101   { "600",	B600,		SPEED	},
102   { "1200",	B1200,		SPEED	},
103   { "1800",	B1800,		SPEED	},
104   { "2400",	B2400,		SPEED	},
105   { "4800",	B4800,		SPEED	},
106   { "9600",	B9600,		SPEED	},
107   { "19200",	B19200,		SPEED	},
108   { "38400",	B38400,		SPEED	},
109   { "57600",	B57600,		SPEED	},
110   { "115200",	B115200,	SPEED	},
111   { "",		0,		BAD	},	/* BAD type to end list */
112 };
113 
114 #define NIL ((char *) NULL)		/* tell(fd, ..., NIL) */
115 
116 int main(int argc, char *argv[]);
117 int isdialstr(char *arg);
118 void tell(int fd, ...);
119 void reader(int on);
120 void shell(char *cmd);
121 void lock_device(char *device);
122 void fatal(char *label);
123 void setnum(char *s, int n);
124 void set_uart(int argc, char *argv[], struct termios *tcp);
125 void set_raw(struct termios *tcp);
126 void quit(int code);
127 
main(argc,argv)128 int main(argc, argv)
129 int argc;
130 char *argv[];
131 {
132   int i;
133   unsigned char key;
134   int candial;
135 
136   for (i = 1; i < argc; ++i) {
137 	if (argv[i][0] == '/') {
138 		if (commdev != NULL) {
139 			tell(2, "term: too many communication devices\n", NIL);
140 			exit(1);
141 		}
142 		commdev = argv[i];
143 	}
144   }
145   if (commdev == NULL) commdev = TERM_LINE;
146 
147   /* Save tty attributes of the terminal. */
148   if (tcgetattr(0, &tcsavestdin) < 0) {
149 	tell(2, "term: standard input is not a terminal\n", NIL);
150 	exit(1);
151   }
152 
153   lock_device(commdev);
154 
155   commfd = open(commdev, O_RDWR);
156   if (commfd < 0) {
157 	tell(2, "term: can't open ", commdev, ": ", strerror(errno), "\n", NIL);
158 	quit(1);
159   }
160 
161   /* Compute RAW modes of terminal and modem. */
162   if (tcgetattr(commfd, &tccomm) < 0) {
163 	tell(2, "term: ", commdev, " is not a terminal\n", NIL);
164 	quit(1);
165   }
166   signal(SIGINT, quit);
167   signal(SIGTERM, quit);
168   tcstdin = tcsavestdin;
169   set_raw(&tcstdin);
170   set_raw(&tccomm);
171   set_uart(argc, argv, &tccomm);
172   tcsetattr(0, TCSANOW, &tcstdin);
173   tcsetattr(commfd, TCSANOW, &tccomm);
174 
175   /* Start a reader process to copy modem output to the screen. */
176   reader(1);
177 
178   /* Welcome message. */
179   tell(1, "Connected to ", commdev,
180 			", command key is CTRL-], type ^]? for help\r\n", NIL);
181 
182   /* Dial. */
183   candial = 0;
184   for (i = 1; i < argc; ++i) {
185 	if (!isdialstr(argv[i])) continue;
186 	tell(commfd, argv[i] + 1, "\r", NIL);
187 	candial = 1;
188   }
189 
190   /* Main loop of the terminal simulator. */
191   while (read(0, &key, 1) == 1) {
192 	if (key == HOTKEY) {
193 		/* Command key typed. */
194 		if (read(0, &key, 1) != 1) continue;
195 
196 		switch (key) {
197 		default:
198 			/* Added command? */
199 			for (i = 1; i < argc; ++i) {
200 				char *arg = argv[i];
201 
202 				if (arg[0] == '-' && arg[1] == 'c'
203 							&& arg[2] == key) {
204 					reader(0);
205 					tcsetattr(0, TCSANOW, &tcsavestdin);
206 					shell(arg+3);
207 					tcsetattr(0, TCSANOW, &tcstdin);
208 					reader(1);
209 					break;
210 				}
211 			}
212 			if (i < argc) break;
213 
214 			/* Unrecognized command, print list. */
215 			tell(1, "\r\nTerm commands:\r\n",
216 				" ? - this help\r\n",
217 				candial ? " d - redial\r\n" : "",
218 				" s - subshell (e.g. for file transfer)\r\n",
219 				" h - hangup (+++ ATH)\r\n",
220 				" b - send a break\r\n",
221 				" q - exit term\r\n",
222 				NIL);
223 			for (i = 1; i < argc; ++i) {
224 				char *arg = argv[i];
225 				static char cmd[] = " x - ";
226 
227 				if (arg[0] == '-' && arg[1] == 'c'
228 							&& arg[2] != 0) {
229 					cmd[1] = arg[2];
230 					tell(1, cmd, arg+3, "\r\n", NIL);
231 				}
232 			}
233 			tell(1, "^] - send a CTRL-]\r\n\n",
234 				NIL);
235 			break;
236 		case 'd':
237 			/* Redial by sending the dial commands again. */
238 			for (i = 1; i < argc; ++i) {
239 				if (!isdialstr(argv[i])) continue;
240 				tell(commfd, argv[i] + 1, "\r", NIL);
241 			}
242 			break;
243 		case 's':
244 			/* Subshell. */
245 			reader(0);
246 			tcsetattr(0, TCSANOW, &tcsavestdin);
247 			shell(NULL);
248 			tcsetattr(0, TCSANOW, &tcstdin);
249 			reader(1);
250 			break;
251 		case 'h':
252 			/* Hangup by using the +++ escape and ATH command. */
253 			sleep(2);
254 			tell(commfd, "+++", NIL);
255 			sleep(2);
256 			tell(commfd, "ATH\r", NIL);
257 			break;
258 		case 'b':
259 			/* Send a break. */
260 			tcsendbreak(commfd, 0);
261 			break;
262 		case 'q':
263 			/* Exit term. */
264 			quit(0);
265 		case HOTKEY:
266 			(void) write(commfd, &key, 1);
267 			break;
268 		}
269 	} else {
270 		/* Send keyboard input down the serial line. */
271 		if (write(commfd, &key, 1) != 1) break;
272 	}
273   }
274   tell(2, "term: nothing to copy from input to ", commdev, "?\r\n", NIL);
275   quit(1);
276 }
277 
278 
isdialstr(char * arg)279 int isdialstr(char *arg)
280 {
281 /* True iff arg is the start of a dial string, i.e. "-at...". */
282 
283   return (arg[0] == '-'
284   	&& (arg[1] == 'a' || arg[1] == 'A')
285   	&& (arg[2] == 't' || arg[2] == 'T'));
286 }
287 
288 
tell(int fd,...)289 void tell(int fd, ...)
290 {
291 /* Write strings to file descriptor 'fd'. */
292   va_list ap;
293   char *s;
294 
295   va_start(ap, fd);
296   while ((s = va_arg(ap, char *)) != NIL) write(fd, s, strlen(s));
297   va_end(ap);
298 }
299 
300 
reader(on)301 void reader(on)
302 int on;
303 {
304 /* Start or end a process that copies from the modem to the screen. */
305 
306   static pid_t pid;
307   char buf[CHUNK];
308   ssize_t n, m, r;
309 
310   if (!on) {
311 	/* End the reader process (if any). */
312 	if (pid == 0) return;
313 	kill(pid, SIGKILL);
314 	(void) waitpid(pid, (int *) NULL, 0);
315 	pid = 0;
316 	return;
317   }
318 
319   /* Start a reader */
320   pid = fork();
321   if (pid < 0) {
322 	tell(2, "term: fork() failed: ", strerror(errno), "\r\n", NIL);
323 	quit(1);
324   }
325   if (pid == 0) {
326 	/* Child: Copy from the modem to the screen. */
327 
328 	while ((n = read(commfd, buf, sizeof(buf))) > 0) {
329 		m = 0;
330 		while (m < n && (r = write(1, buf + m, n - m)) > 0) m += r;
331 	}
332 	tell(2, "term: nothing to copy from ", commdev, " to output?\r\n", NIL);
333 	kill(getppid(), SIGTERM);
334 	_exit(1);
335   }
336   /* One reader on the loose. */
337 }
338 
339 
shell(char * cmd)340 void shell(char *cmd)
341 {
342 /* Invoke a subshell to allow one to run zmodem for instance.  Run sh -c 'cmd'
343  * instead if 'cmd' non-null.
344  */
345 
346   pid_t pid;
347   char *shell, *sh0;
348   void(*isav) (int);
349   void(*qsav) (int);
350   void(*tsav) (int);
351 
352   if (cmd == NULL) {
353 	tell(1, "\nExit the shell to return to term, ",
354 		commdev, " is open on file descriptor 9.\n", NIL);
355   }
356 
357   if (cmd != NULL || (shell = getenv("SHELL")) == NULL) shell = "/bin/sh";
358   if ((sh0 = strrchr(shell, '/')) == NULL) sh0 = shell; else sh0++;
359 
360   /* Start a shell */
361   pid = fork();
362   if (pid < 0) {
363 	tell(2, "term: fork() failed: ", strerror(errno), "\n", NIL);
364 	return;
365   }
366   if (pid == 0) {
367 	/* Child: Exec the shell. */
368 	setgid(getgid());
369 	setuid(getuid());
370 
371 	if (commfd != 9) { dup2(commfd, 9); close(commfd); }
372 
373 	if (cmd == NULL) {
374 		execl(shell, sh0, (char *) NULL);
375 	} else {
376 		execl(shell, sh0, "-c", cmd, (char *) NULL);
377 	}
378 	tell(2, "term: can't execute ", shell, ": ", strerror(errno), "\n",NIL);
379 	_exit(1);
380   }
381   /* Wait for the shell to exit. */
382   isav = signal(SIGINT, SIG_IGN);
383   qsav = signal(SIGQUIT, SIG_IGN);
384   tsav = signal(SIGTERM, SIG_IGN);
385   (void) waitpid(pid, (int *) 0, 0);
386   (void) signal(SIGINT, isav);
387   (void) signal(SIGQUIT, qsav);
388   (void) signal(SIGTERM, tsav);
389   tell(1, "\n[back to term]\n", NIL);
390 }
391 
392 
lock_device(device)393 void lock_device(device)
394 char *device;
395 {
396 /* Lock a device by creating a lock file using SYSV style locking. */
397 
398   struct stat stbuf;
399   unsigned int pid;
400   int fd;
401   int n;
402   int u;
403 
404   if (stat(device, &stbuf) < 0) fatal(device);
405 
406   if (!S_ISCHR(stbuf.st_mode)) {
407 	tell(2, "term: ", device, " is not a character device\n", NIL);
408 	exit(1);
409   }
410 
411   /* Compute the lock file name. */
412   setnum(lockfile + 23, (stbuf.st_dev >> 8) & 0xFF);	/* FS major (why?) */
413   setnum(lockfile + 27, (stbuf.st_rdev >> 8) & 0xFF);	/* device major */
414   setnum(lockfile + 31, (stbuf.st_rdev >> 0) & 0xFF);	/* device minor */
415 
416   /* Try to make a lock file and put my pid in it. */
417   u = umask(0);
418   for (;;) {
419 	if ((fd = open(lockfile, O_RDONLY)) < 0) {
420 		/* No lock file, try to lock it myself. */
421 		if (errno != ENOENT) fatal(device);
422 		if ((fd = open(lockfile, O_WRONLY|O_CREAT|O_EXCL, 0444)) < 0) {
423 			if (errno == EEXIST) continue;
424 			fatal(lockfile);
425 		}
426 		pid = getpid();
427 		n = write(fd, &pid, sizeof(pid));
428 		if (n < 0) {
429 			n = errno;
430 			(void) unlink(lockfile);
431 			errno = n;
432 			fatal(lockfile);
433 		}
434 		close(fd);
435 		break;
436 	} else {
437 		/* Already there, but who owns it? */
438 		n = read(fd, &pid, sizeof(pid));
439 		if (n < 0) fatal(device);
440 		close(fd);
441 		if (n == sizeof(pid) && !(kill(pid, 0) < 0 && errno == ESRCH)) {
442 			/* It is locked by a running process. */
443 			tell(2, "term: ", device,
444 				" is in use by another program\n", NIL);
445 			if (getpgrp() == getpid()) sleep(3);
446 			exit(1);
447 		}
448 		/* Stale lock. */
449 		tell(1, "Removing stale lock ", lockfile, "\n", NIL);
450 		if (unlink(lockfile) < 0 && errno != ENOENT) fatal(lockfile);
451 	}
452   }
453   /* Lock achieved, but what if two terms encounters a stale lock at the same
454    * time?
455    */
456   umask(u);
457 }
458 
459 
fatal(char * label)460 void fatal(char *label)
461 {
462   tell(2, "term: ", label, ": ", strerror(errno), "\n", NIL);
463   exit(1);
464 }
465 
466 
setnum(char * s,int n)467 void setnum(char *s, int n)
468 {
469 /* Poke 'n' into string 's' backwards as three decimal digits. */
470   int i;
471 
472   for (i = 0; i < 3; i++) { *--s = '0' + (n % 10); n /= 10; }
473 }
474 
475 
set_uart(argc,argv,tcp)476 void set_uart(argc, argv, tcp)
477 int argc;
478 char *argv[];
479 struct termios *tcp;
480 {
481 /* Set up the UART parameters. */
482 
483   int i;
484   char *arg;
485   struct param_s *param;
486 
487   /* Examine all the parameters and check for validity. */
488   for (i = 1; i < argc; ++i) {
489 	arg = argv[i];
490 	if (arg[0] == '/' || arg[0] == '-') continue;
491 
492 	/* Check parameter for legality. */
493 	for (param = &params[0];
494 	     param->type != BAD && strcmp(arg, param->pattern) != 0;
495 	     ++param);
496 	switch (param->type) {
497 	    case BAD:
498 		tell(2, "Invalid parameter: ", arg, "\n", NIL);
499 		quit(1);
500 		break;
501 	    case BITS:
502 		tcp->c_cflag &= ~CSIZE;
503 		tcp->c_cflag |= param->value;
504 		break;
505 	    case PARITY:
506 		tcp->c_cflag &= PARENB | PARODD;
507 		tcp->c_cflag |= param->value;
508 		break;
509 	    case SPEED:
510 		cfsetispeed(tcp, (speed_t) param->value);
511 		cfsetospeed(tcp, (speed_t) param->value);
512 		break;
513 	}
514   }
515 }
516 
517 
set_raw(tcp)518 void set_raw(tcp)
519 struct termios *tcp;
520 {
521   /* Set termios attributes for RAW mode. */
522 
523   tcp->c_iflag &= ~(ICRNL|IGNCR|INLCR|IXON|IXOFF);
524   tcp->c_lflag &= ~(ICANON|IEXTEN|ISIG|ECHO|ECHONL);
525   tcp->c_oflag &= ~(OPOST);
526   tcp->c_cc[VMIN] = 1;
527   tcp->c_cc[VTIME] = 0;
528 }
529 
530 
quit(code)531 void quit(code)
532 int code;
533 {
534 /* Stop the reader process, reset the terminal, and exit. */
535   reader(0);
536   tcsetattr(0, TCSANOW, &tcsavestdin);
537   (void) unlink(lockfile);
538   exit(code);
539 }
540