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???
76const char *kDefaultShell = "/bin/sh";
77const char *kColorTerminalType = "truecolor";
78const 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
87extern char **environ;
88
89static int setenv(const char *var, const char *value, bool overwrite);
90
91static int
92setenv(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 */
119typedef 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
133Shell::Shell()
134	:
135	fFd(-1),
136	fProcessID(-1),
137	fTermParse(NULL),
138	fAttached(false)
139{
140}
141
142
143Shell::~Shell()
144{
145	Close();
146}
147
148
149status_t
150Shell::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
169void
170Shell::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
186const char *
187Shell::TTYName() const
188{
189	return ttyname(fFd);
190}
191
192
193ssize_t
194Shell::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
203ssize_t
204Shell::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
213status_t
214Shell::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
225status_t
226Shell::GetAttr(struct termios &attr) const
227{
228	if (tcgetattr(fFd, &attr) < 0)
229		return errno;
230	return B_OK;
231}
232
233
234status_t
235Shell::SetAttr(const struct termios &attr)
236{
237	if (tcsetattr(fFd, TCSANOW, &attr) < 0)
238		return errno;
239	return B_OK;
240}
241
242
243int
244Shell::FD() const
245{
246	return fFd;
247}
248
249
250bool
251Shell::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
261bool
262Shell::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
300status_t
301Shell::AttachBuffer(TerminalBuffer *buffer)
302{
303	if (fAttached)
304		return B_ERROR;
305
306	fAttached = true;
307
308	return fTermParse->StartThreads(buffer);
309}
310
311
312void
313Shell::DetachBuffer()
314{
315	if (fAttached)
316		fTermParse->StopThreads();
317}
318
319
320// private
321static status_t
322send_handshake_message(thread_id target, const handshake_t& handshake)
323{
324	return send_data(target, 0, &handshake, sizeof(handshake_t));
325}
326
327
328static void
329receive_handshake_message(handshake_t& handshake)
330{
331	thread_id sender;
332	receive_data(&sender, &handshake, sizeof(handshake_t));
333}
334
335
336static void
337initialize_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
393status_t
394Shell::_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