xref: /haiku/src/apps/terminal/Shell.cpp (revision 495060760727dd782c9f8a90db71e5d727f19748)
1 /*
2  * Copyright 2007-2013, Haiku, Inc. All rights reserved.
3  * Copyright (c) 2003-4 Kian Duffy <myob@users.sourceforge.net>
4  * Copyright (c) 2004 Daniel Furrer <assimil8or@users.sourceforge.net>
5  * Parts Copyright (C) 1998,99 Kazuho Okui and Takashi Murai.
6  *
7  * Distributed under the terms of the MIT license.
8  *
9  * Authors:
10  *		Kian Duffy, myob@users.sourceforge.net
11  *		Daniel Furrer, assimil8or@users.sourceforge.net
12  *		Siarzhuk Zharski, zharik@gmx.li
13  */
14 
15 
16 #include "Shell.h"
17 
18 #include <dirent.h>
19 #include <errno.h>
20 #include <fcntl.h>
21 #include <new>
22 #include <pwd.h>
23 #include <signal.h>
24 #include <string.h>
25 #include <stdlib.h>
26 #include <stddef.h>
27 #include <stdio.h>
28 #include <sys/param.h>
29 #include <sys/stat.h>
30 #include <sys/wait.h>
31 #include <termios.h>
32 #include <time.h>
33 #include <unistd.h>
34 
35 #include <Catalog.h>
36 #include <Entry.h>
37 #include <Locale.h>
38 #include <OS.h>
39 #include <Path.h>
40 
41 #include <util/KMessage.h>
42 
43 #include <extended_system_info.h>
44 #include <extended_system_info_defs.h>
45 
46 #include "ActiveProcessInfo.h"
47 #include "ShellParameters.h"
48 #include "TermConst.h"
49 #include "TermParse.h"
50 #include "TerminalBuffer.h"
51 
52 
53 #ifndef CEOF
54 #define CEOF ('D'&037)
55 #endif
56 #ifndef CSUSP
57 #define CSUSP ('Z'&037)
58 #endif
59 #ifndef CQUIT
60 #define CQUIT ('\\'&037)
61 #endif
62 #ifndef CEOL
63 #define CEOL 0
64 #endif
65 #ifndef CSTOP
66 #define CSTOP ('Q'&037)
67 #endif
68 #ifndef CSTART
69 #define CSTART ('S'&037)
70 #endif
71 #ifndef CSWTCH
72 #define CSWTCH 0
73 #endif
74 
75 // TODO: should extract from /etc/passwd instead???
76 const char *kDefaultShell = "/bin/sh";
77 const char *kTerminalType = "xterm-256color";
78 
79 /*
80  * Set environment variable.
81  */
82 #if defined(HAIKU_TARGET_PLATFORM_BEOS) || \
83 	defined(HAIKU_TARGET_PLATFORM_BONE) || \
84 	defined(HAIKU_TARGET_PLATFORM_LIBBE_TEST)
85 
86 extern char **environ;
87 
88 static int setenv(const char *var, const char *value, bool overwrite);
89 
90 static int
91 setenv(const char *var, const char *value, bool overwrite)
92 {
93 	int envindex = 0;
94 	const int len = strlen(var);
95 	const int val_len = strlen (value);
96 
97 	while (environ[envindex] != NULL) {
98 		if (!strncmp(environ[envindex], var, len)) {
99 			/* found it */
100 			if (overwrite) {
101 				environ[envindex] = (char *)malloc((unsigned)len + val_len + 2);
102 				sprintf(environ[envindex], "%s=%s", var, value);
103 			}
104 			return 0;
105 		}
106 		envindex++;
107 	}
108 
109 	environ[envindex] = (char *)malloc((unsigned)len + val_len + 2);
110 	sprintf(environ[envindex], "%s=%s", var, value);
111 	environ[++envindex] = NULL;
112 	return 0;
113 }
114 #endif
115 
116 
117 /* handshake interface */
118 typedef struct
119 {
120 	int status;		/* status of child */
121 	char msg[128];	/* error message */
122 	unsigned short row;		/* terminal rows */
123 	unsigned short col;		/* Terminal columns */
124 } handshake_t;
125 
126 /* status of handshake */
127 #define PTY_OK	0	/* pty open and set termios OK */
128 #define PTY_NG	1	/* pty open or set termios NG */
129 #define PTY_WS	2	/* pty need WINSIZE (row and col ) */
130 
131 
132 Shell::Shell()
133 	:
134 	fFd(-1),
135 	fProcessID(-1),
136 	fTermParse(NULL),
137 	fAttached(false)
138 {
139 }
140 
141 
142 Shell::~Shell()
143 {
144 	Close();
145 }
146 
147 
148 status_t
149 Shell::Open(int row, int col, const ShellParameters& parameters)
150 {
151 	if (fFd >= 0)
152 		return B_ERROR;
153 
154 	status_t status = _Spawn(row, col, parameters);
155 	if (status < B_OK)
156 		return status;
157 
158 	fTermParse = new (std::nothrow) TermParse(fFd);
159 	if (fTermParse == NULL) {
160 		Close();
161 		return B_NO_MEMORY;
162 	}
163 
164 	return B_OK;
165 }
166 
167 
168 void
169 Shell::Close()
170 {
171 	delete fTermParse;
172 	fTermParse = NULL;
173 
174 	if (fFd >= 0) {
175 		close(fFd);
176 		kill(-fShellInfo.ProcessID(), SIGHUP);
177 		fShellInfo.SetProcessID(-1);
178 		int status;
179 		wait(&status);
180 		fFd = -1;
181 	}
182 }
183 
184 
185 const char *
186 Shell::TTYName() const
187 {
188 	return ttyname(fFd);
189 }
190 
191 
192 ssize_t
193 Shell::Read(void *buffer, size_t numBytes) const
194 {
195 	if (fFd < 0)
196 		return B_NO_INIT;
197 
198 	return read(fFd, buffer, numBytes);
199 }
200 
201 
202 ssize_t
203 Shell::Write(const void *buffer, size_t numBytes)
204 {
205 	if (fFd < 0)
206 		return B_NO_INIT;
207 
208 	return write(fFd, buffer, numBytes);
209 }
210 
211 
212 status_t
213 Shell::UpdateWindowSize(int rows, int columns)
214 {
215 	struct winsize winSize;
216 	winSize.ws_row = rows;
217 	winSize.ws_col = columns;
218 	if (ioctl(fFd, TIOCSWINSZ, &winSize) != 0)
219 		return errno;
220 	return B_OK;
221 }
222 
223 
224 status_t
225 Shell::GetAttr(struct termios &attr) const
226 {
227 	if (tcgetattr(fFd, &attr) < 0)
228 		return errno;
229 	return B_OK;
230 }
231 
232 
233 status_t
234 Shell::SetAttr(const struct termios &attr)
235 {
236 	if (tcsetattr(fFd, TCSANOW, &attr) < 0)
237 		return errno;
238 	return B_OK;
239 }
240 
241 
242 int
243 Shell::FD() const
244 {
245 	return fFd;
246 }
247 
248 
249 bool
250 Shell::HasActiveProcesses() const
251 {
252 	pid_t running = tcgetpgrp(fFd);
253 	if (running == fShellInfo.ProcessID() || running == -1)
254 		return false;
255 
256 	return true;
257 }
258 
259 
260 bool
261 Shell::GetActiveProcessInfo(ActiveProcessInfo& _info) const
262 {
263 	_info.Unset();
264 
265 	// get the foreground process group
266 	pid_t process = tcgetpgrp(fFd);
267 	if (process < 0)
268 		return false;
269 
270 	// get more info on the process group leader
271 	KMessage info;
272 	status_t error = get_extended_team_info(process, B_TEAM_INFO_BASIC, info);
273 	if (error != B_OK)
274 		return false;
275 
276 	// fetch the name and the current directory from the info
277 	const char* name;
278 	int32 cwdDevice;
279 	int64 cwdDirectory = 0;
280 	if (info.FindString("name", &name) != B_OK
281 		|| info.FindInt32("cwd device", &cwdDevice) != B_OK
282 		|| info.FindInt64("cwd directory", &cwdDirectory) != B_OK) {
283 		return false;
284 	}
285 
286 	// convert the node ref into a path
287 	entry_ref cwdRef(cwdDevice, cwdDirectory, ".");
288 	BPath cwdPath;
289 	if (cwdPath.SetTo(&cwdRef) != B_OK)
290 		return false;
291 
292 	// set the result
293 	_info.SetTo(process, name, cwdPath.Path());
294 
295 	return true;
296 }
297 
298 
299 status_t
300 Shell::AttachBuffer(TerminalBuffer *buffer)
301 {
302 	if (fAttached)
303 		return B_ERROR;
304 
305 	fAttached = true;
306 
307 	return fTermParse->StartThreads(buffer);
308 }
309 
310 
311 void
312 Shell::DetachBuffer()
313 {
314 	if (fAttached)
315 		fTermParse->StopThreads();
316 }
317 
318 
319 // private
320 static status_t
321 send_handshake_message(thread_id target, const handshake_t& handshake)
322 {
323 	return send_data(target, 0, &handshake, sizeof(handshake_t));
324 }
325 
326 
327 static void
328 receive_handshake_message(handshake_t& handshake)
329 {
330 	thread_id sender;
331 	receive_data(&sender, &handshake, sizeof(handshake_t));
332 }
333 
334 
335 static void
336 initialize_termios(struct termios &tio)
337 {
338 	/*
339 	 * Set Terminal interface.
340 	 */
341 
342 	tio.c_line = 0;
343 	tio.c_lflag |= ECHOE;
344 
345 	/* input: nl->nl, cr->nl */
346 	tio.c_iflag &= ~(INLCR|IGNCR);
347 	tio.c_iflag |= ICRNL;
348 	tio.c_iflag &= ~ISTRIP;
349 
350 	/* output: cr->cr, nl in not retrun, no delays, ln->cr/ln */
351 	tio.c_oflag &= ~(OCRNL|ONLRET|NLDLY|CRDLY|TABDLY|BSDLY|VTDLY|FFDLY);
352 	tio.c_oflag |= ONLCR;
353 	tio.c_oflag |= OPOST;
354 
355 	/* baud rate is 19200 (equal beterm) */
356 	tio.c_cflag &= ~(CBAUD);
357 	tio.c_cflag |= B19200;
358 
359 	tio.c_cflag &= ~CSIZE;
360 	tio.c_cflag |= CS8;
361 	tio.c_cflag |= CREAD;
362 
363 	tio.c_cflag |= HUPCL;
364 	tio.c_iflag &= ~(IGNBRK|BRKINT);
365 
366 	/*
367 	 * enable signals, canonical processing (erase, kill, etc), echo.
368 	*/
369 	tio.c_lflag |= ISIG|ICANON|ECHO|ECHOE|ECHONL;
370 	tio.c_lflag &= ~(ECHOK | IEXTEN);
371 
372 	/* set control characters. */
373 	tio.c_cc[VINTR]  = 'C' & 0x1f;	/* '^C'	*/
374 	tio.c_cc[VQUIT]  = CQUIT;		/* '^\'	*/
375 	tio.c_cc[VERASE] = 0x7f;		/* '^?'	*/
376 	tio.c_cc[VKILL]  = 'U' & 0x1f;	/* '^U'	*/
377 	tio.c_cc[VEOF]   = CEOF;		/* '^D' */
378 	tio.c_cc[VEOL]   = CEOL;		/* '^@' */
379 	tio.c_cc[VMIN]   = 4;
380 	tio.c_cc[VTIME]  = 0;
381 	tio.c_cc[VEOL2]  = CEOL;		/* '^@' */
382 	tio.c_cc[VSWTCH] = CSWTCH;		/* '^@' */
383 	tio.c_cc[VSTART] = CSTART;		/* '^S' */
384 	tio.c_cc[VSTOP]  = CSTOP;		/* '^Q' */
385 	tio.c_cc[VSUSP]  = CSUSP;		/* '^Z' */
386 }
387 
388 
389 #undef B_TRANSLATION_CONTEXT
390 #define B_TRANSLATION_CONTEXT "Terminal Shell"
391 
392 status_t
393 Shell::_Spawn(int row, int col, const ShellParameters& parameters)
394 {
395 	const char** argv = (const char**)parameters.Arguments();
396 	int argc = parameters.ArgumentCount();
397 	const char* defaultArgs[3] = {kDefaultShell, "-l", NULL};
398 	struct passwd passwdStruct;
399 	struct passwd *passwdResult;
400 	char stringBuffer[256];
401 
402 	if (argv == NULL || argc == 0) {
403 		if (getpwuid_r(getuid(), &passwdStruct, stringBuffer,
404 				sizeof(stringBuffer), &passwdResult) == 0
405 			&& passwdResult != NULL) {
406 			defaultArgs[0] = passwdStruct.pw_shell;
407 		}
408 
409 		argv = defaultArgs;
410 		argc = 2;
411 
412 		fShellInfo.SetDefaultShell(true);
413 	} else
414 		fShellInfo.SetDefaultShell(false);
415 
416 	fShellInfo.SetEncoding(parameters.Encoding());
417 
418 	signal(SIGTTOU, SIG_IGN);
419 
420 	// get a pseudo-tty
421 	int master = posix_openpt(O_RDWR | O_NOCTTY);
422 	const char *ttyName;
423 
424 	if (master < 0) {
425 		fprintf(stderr, "Didn't find any available pseudo ttys.");
426 		return errno;
427 	}
428 
429 	if (grantpt(master) != 0 || unlockpt(master) != 0
430 		|| (ttyName = ptsname(master)) == NULL) {
431 		close(master);
432 		fprintf(stderr, "Failed to init pseudo tty.");
433 		return errno;
434 	}
435 
436 	/*
437 	 * Get the modes of the current terminal. We will duplicates these
438 	 * on the pseudo terminal.
439 	 */
440 
441 	thread_id terminalThread = find_thread(NULL);
442 
443 	/* Fork a child process. */
444 	fShellInfo.SetProcessID(fork());
445 	if (fShellInfo.ProcessID() < 0) {
446 		close(master);
447 		return B_ERROR;
448 	}
449 
450 	handshake_t handshake;
451 
452 	if (fShellInfo.ProcessID() == 0) {
453 		// Now in child process.
454 
455 		// close the PTY master side
456 		close(master);
457 
458 		/*
459 		 * Make our controlling tty the pseudo tty. This hapens because
460 		 * we cleared our original controlling terminal above.
461 		 */
462 
463 		/* Set process session leader */
464 		if (setsid() < 0) {
465 			handshake.status = PTY_NG;
466 			snprintf(handshake.msg, sizeof(handshake.msg),
467 				"could not set session leader.");
468 			send_handshake_message(terminalThread, handshake);
469 			exit(1);
470 		}
471 
472 		/* open slave pty */
473 		int slave = -1;
474 		if ((slave = open(ttyName, O_RDWR)) < 0) {
475 			handshake.status = PTY_NG;
476 			snprintf(handshake.msg, sizeof(handshake.msg),
477 				"can't open tty (%s).", ttyName);
478 			send_handshake_message(terminalThread, handshake);
479 			exit(1);
480 		}
481 
482 		/* set signal default */
483 		signal(SIGCHLD, SIG_DFL);
484 		signal(SIGHUP, SIG_DFL);
485 		signal(SIGQUIT, SIG_DFL);
486 		signal(SIGTERM, SIG_DFL);
487 		signal(SIGINT, SIG_DFL);
488 		signal(SIGTTOU, SIG_DFL);
489 
490 		struct termios tio;
491 		/* get tty termios (not necessary).
492 		 * TODO: so why are we doing it ?
493 		 */
494 		tcgetattr(slave, &tio);
495 
496 		initialize_termios(tio);
497 
498 		/*
499 		 * change control tty.
500 		 */
501 
502 		dup2(slave, 0);
503 		dup2(slave, 1);
504 		dup2(slave, 2);
505 
506 		/* close old slave fd. */
507 		if (slave > 2)
508 			close(slave);
509 
510 		/*
511 		 * set terminal interface.
512 		 */
513 		if (tcsetattr(0, TCSANOW, &tio) == -1) {
514 			handshake.status = PTY_NG;
515 			snprintf(handshake.msg, sizeof(handshake.msg),
516 				"failed set terminal interface (TERMIOS).");
517 			send_handshake_message(terminalThread, handshake);
518 			exit(1);
519 		}
520 
521 		/*
522 		 * set window size.
523 		 */
524 
525 		handshake.status = PTY_WS;
526 		send_handshake_message(terminalThread, handshake);
527 		receive_handshake_message(handshake);
528 
529 		if (handshake.status != PTY_WS) {
530 			handshake.status = PTY_NG;
531 			snprintf(handshake.msg, sizeof(handshake.msg),
532 				"mismatch handshake.");
533 			send_handshake_message(terminalThread, handshake);
534 			exit(1);
535 		}
536 
537 		struct winsize ws = { handshake.row, handshake.col };
538 
539 		ioctl(0, TIOCSWINSZ, &ws, sizeof(ws));
540 
541 		tcsetpgrp(0, getpgrp());
542 			// set this process group ID as the controlling terminal
543 		set_thread_priority(find_thread(NULL), B_NORMAL_PRIORITY);
544 
545 		/* pty open and set termios successful. */
546 		handshake.status = PTY_OK;
547 		send_handshake_message(terminalThread, handshake);
548 
549 		/*
550 		 * setenv TERM and TTY.
551 		 */
552 		setenv("TERM", kTerminalType, true);
553 		setenv("TTY", ttyName, true);
554 		setenv("TTYPE", fShellInfo.EncodingName(), true);
555 
556 		// set the current working directory, if one is given
557 		if (parameters.CurrentDirectory().Length() > 0)
558 			chdir(parameters.CurrentDirectory().String());
559 
560 		execve(argv[0], (char * const *)argv, environ);
561 
562 		// Exec failed.
563 		// TODO: This doesn't belong here.
564 
565 		sleep(1);
566 
567 		BString alertCommand = "alert --stop '";
568 		alertCommand += B_TRANSLATE("Cannot execute \"%command\":\n\t%error");
569 		alertCommand += "' '";
570 		alertCommand += B_TRANSLATE("Use default shell");
571 		alertCommand += "' '";
572 		alertCommand += B_TRANSLATE("Abort");
573 		alertCommand += "'";
574 		alertCommand.ReplaceFirst("%command", argv[0]);
575 		alertCommand.ReplaceFirst("%error", strerror(errno));
576 
577 		int returnValue = system(alertCommand.String());
578 		if (returnValue == 0) {
579 			execl(kDefaultShell, kDefaultShell,
580 				"-l", NULL);
581 		}
582 
583 		exit(1);
584 	}
585 
586 	/*
587 	 * In parent Process, Set up the input and output file pointers so
588 	 * that they can write and read the pseudo terminal.
589 	 */
590 
591 	/*
592 	 * close parent control tty.
593 	 */
594 
595 	int done = 0;
596 	while (!done) {
597 		receive_handshake_message(handshake);
598 
599 		switch (handshake.status) {
600 			case PTY_OK:
601 				done = 1;
602 				break;
603 
604 			case PTY_NG:
605 				fprintf(stderr, "%s\n", handshake.msg);
606 				done = -1;
607 				break;
608 
609 			case PTY_WS:
610 				handshake.row = row;
611 				handshake.col = col;
612 				handshake.status = PTY_WS;
613 				send_handshake_message(fShellInfo.ProcessID(), handshake);
614 				break;
615 		}
616 	}
617 
618 	if (done <= 0) {
619 		close(master);
620 		return B_ERROR;
621 	}
622 
623 	fFd = master;
624 
625 	return B_OK;
626 }
627