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