xref: /haiku/src/apps/terminal/TermApp.cpp (revision 9760dcae2038d47442f4658c2575844c6cf92c40)
1 /*
2  * Copyright 2001-2009, Haiku, Inc. All rights reserved.
3  * Copyright (c) 2003-2004 Kian Duffy <myob@users.sourceforge.net>
4  * Copyright (C) 1998,99 Kazuho Okui and Takashi Murai.
5  *
6  * Distributed unter the terms of the MIT license.
7  */
8 
9 
10 #include "TermApp.h"
11 
12 #include <errno.h>
13 #include <pwd.h>
14 #include <signal.h>
15 #include <stdio.h>
16 #include <stdlib.h>
17 #include <unistd.h>
18 
19 #include <Alert.h>
20 #include <Clipboard.h>
21 #include <NodeInfo.h>
22 #include <Path.h>
23 #include <Roster.h>
24 #include <Screen.h>
25 #include <String.h>
26 
27 
28 #include "Arguments.h"
29 #include "Globals.h"
30 #include "PrefHandler.h"
31 #include "TermConst.h"
32 #include "TermView.h"
33 #include "TermWindow.h"
34 
35 
36 static bool sUsageRequested = false;
37 //static bool sGeometryRequested = false;
38 
39 const char *kDefaultShell = "/bin/bash";
40 const ulong MSG_ACTIVATE_TERM = 'msat';
41 const ulong MSG_TERM_WINDOW_INFO = 'mtwi';
42 
43 
44 int
45 main()
46 {
47 	TermApp app;
48 	app.Run();
49 
50 	return 0;
51 }
52 
53 
54 TermApp::TermApp()
55 	: BApplication(TERM_SIGNATURE),
56 	fStartFullscreen(false),
57 	fWindowNumber(-1),
58 	fTermWindow(NULL),
59 	fArgs(NULL)
60 {
61 	const char *defaultArgs[2];
62 	defaultArgs[0] = kDefaultShell;
63 	defaultArgs[1] = "--login";
64 
65 	struct passwd passwdStruct;
66 	struct passwd *passwdResult;
67 	char stringBuffer[256];
68 	if (!getpwuid_r(getuid(), &passwdStruct, stringBuffer,
69 			sizeof(stringBuffer), &passwdResult)) {
70 		defaultArgs[0] = passwdStruct.pw_shell;
71 	}
72 
73 	fArgs = new Arguments(2, defaultArgs);
74 
75 	fWindowTitle = "Terminal";
76 	_RegisterTerminal();
77 
78 	if (fWindowNumber > 0)
79 		fWindowTitle << " " << fWindowNumber;
80 
81 	int i = fWindowNumber / 16;
82 	int j = fWindowNumber % 16;
83 	int k = (j * 16) + (i * 64) + 50;
84 	int l = (j * 16)  + 50;
85 
86 	fTermFrame.Set(k, l, k + 50, k + 50);
87 }
88 
89 
90 TermApp::~TermApp()
91 {
92 	delete fArgs;
93 }
94 
95 
96 void
97 TermApp::ReadyToRun()
98 {
99 	// Prevent opeing window when option -h is given.
100 	if (sUsageRequested)
101 		return;
102 
103 	// Install a SIGCHLD signal handler, so that we will be notified, when
104 	// a shell exits.
105 	struct sigaction action;
106 #ifdef __HAIKU__
107 	action.sa_handler = (sighandler_t)_SigChildHandler;
108 #else
109 	action.sa_handler = (__signal_func_ptr)_SigChildHandler;
110 #endif
111 	sigemptyset(&action.sa_mask);
112 #ifdef SA_NODEFER
113 	action.sa_flags = SA_NODEFER;
114 #endif
115 	action.sa_userdata = this;
116 	if (sigaction(SIGCHLD, &action, NULL) < 0) {
117 		fprintf(stderr, "sigaction() failed: %s\n", strerror(errno));
118 		// continue anyway
119 	}
120 
121 	// init the mouse copy'n'paste clipboard
122 	gMouseClipboard = new BClipboard(MOUSE_CLIPBOARD_NAME, true);
123 
124 	status_t status = _MakeTermWindow(fTermFrame);
125 
126 	// failed spawn, print stdout and open alert panel
127 	// TODO: This alert does never show up.
128 	if (status < B_OK) {
129 		(new BAlert("alert", "Terminal couldn't start the shell. Sorry.",
130 			"OK", NULL, NULL, B_WIDTH_FROM_LABEL,
131 			B_INFO_ALERT))->Go(NULL);
132 		PostMessage(B_QUIT_REQUESTED);
133 		return;
134 	}
135 
136 	// using BScreen::Frame isn't enough
137 	if (fStartFullscreen)
138 		BMessenger(fTermWindow).SendMessage(FULLSCREEN);
139 }
140 
141 
142 void
143 TermApp::Quit()
144 {
145 	if (!sUsageRequested)
146 		_UnregisterTerminal();
147 
148 	BApplication::Quit();
149 }
150 
151 
152 void
153 TermApp::AboutRequested()
154 {
155 	TermView::AboutRequested();
156 }
157 
158 
159 void
160 TermApp::MessageReceived(BMessage* msg)
161 {
162 	switch (msg->what) {
163 		case MENU_SWITCH_TERM:
164 			_SwitchTerm();
165 			break;
166 
167 		case MSG_ACTIVATE_TERM:
168 			fTermWindow->Activate();
169 			break;
170 
171 		case MSG_TERM_WINDOW_INFO:
172 		{
173 			BMessage reply(B_REPLY);
174 			reply.AddBool("minimized", fTermWindow->IsMinimized());
175 			reply.AddInt32("workspaces", fTermWindow->Workspaces());
176 			msg->SendReply(&reply);
177 			break;
178 		}
179 
180 		case MSG_CHECK_CHILDREN:
181 			_HandleChildCleanup();
182 			break;
183 
184 		default:
185 			BApplication::MessageReceived(msg);
186 			break;
187 	}
188 }
189 
190 
191 void
192 TermApp::ArgvReceived(int32 argc, char **argv)
193 {
194 	fArgs->Parse(argc, argv);
195 
196 	if (fArgs->UsageRequested()) {
197 		_Usage(argv[0]);
198 		sUsageRequested = true;
199 		PostMessage(B_QUIT_REQUESTED);
200 		return;
201 	}
202 
203 	if (fArgs->Title() != NULL)
204 		fWindowTitle = fArgs->Title();
205 
206 	fStartFullscreen = fArgs->FullScreen();
207 }
208 
209 
210 void
211 TermApp::RefsReceived(BMessage* message)
212 {
213 	// Works Only Launced by Double-Click file, or Drags file to App.
214 	if (!IsLaunching())
215 		return;
216 
217 	entry_ref ref;
218 	if (message->FindRef("refs", 0, &ref) != B_OK)
219 		return;
220 
221 	BFile file;
222 	if (file.SetTo(&ref, B_READ_WRITE) != B_OK)
223 		return;
224 
225 	BNodeInfo info(&file);
226 	char mimetype[B_MIME_TYPE_LENGTH];
227 	info.GetType(mimetype);
228 
229 	// if App opened by Pref file
230 	if (!strcmp(mimetype, PREFFILE_MIMETYPE)) {
231 
232 		BEntry ent(&ref);
233 		BPath path(&ent);
234 		PrefHandler::Default()->OpenText(path.Path());
235 		return;
236 	}
237 
238 	// if App opened by Shell Script
239 	if (!strcmp(mimetype, "text/x-haiku-shscript")){
240 		// Not implemented.
241 		//    beep();
242 		return;
243 	}
244 }
245 
246 
247 status_t
248 TermApp::_MakeTermWindow(BRect &frame)
249 {
250 	try {
251 		fTermWindow = new TermWindow(frame, fWindowTitle.String(), fArgs);
252 	} catch (int error) {
253 		return (status_t)error;
254 	} catch (...) {
255 		return B_ERROR;
256 	}
257 
258 	fTermWindow->Show();
259 
260 	return B_OK;
261 }
262 
263 
264 void
265 TermApp::_ActivateTermWindow(team_id id)
266 {
267 	BMessenger app(TERM_SIGNATURE, id);
268 	if (app.IsTargetLocal())
269 		fTermWindow->Activate();
270 	else
271 		app.SendMessage(MSG_ACTIVATE_TERM);
272 }
273 
274 
275 void
276 TermApp::_SwitchTerm()
277 {
278 	team_id myId = be_app->Team();
279 	BList teams;
280 	be_roster->GetAppList(TERM_SIGNATURE, &teams);
281 
282 	int32 numTerms = teams.CountItems();
283 	if (numTerms <= 1)
284 		return;
285 
286 	// Find our position in the app teams.
287 	int32 i;
288 
289 	for (i = 0; i < numTerms; i++) {
290 		if (myId == reinterpret_cast<team_id>(teams.ItemAt(i)))
291 			break;
292 	}
293 
294 	do {
295 		if (--i < 0)
296 			i = numTerms - 1;
297 	} while (!_IsSwitchTarget(reinterpret_cast<team_id>(teams.ItemAt(i))));
298 
299 	// Activate switched terminal.
300 	_ActivateTermWindow(reinterpret_cast<team_id>(teams.ItemAt(i)));
301 }
302 
303 
304 bool
305 TermApp::_IsSwitchTarget(team_id id)
306 {
307 	uint32 currentWorkspace = 1L << current_workspace();
308 
309 	BMessenger app(TERM_SIGNATURE, id);
310 	if (app.IsTargetLocal()) {
311 		return !fTermWindow->IsMinimized()
312 			&& (fTermWindow->Workspaces() & currentWorkspace) != 0;
313 	}
314 
315 	BMessage reply;
316 	if (app.SendMessage(MSG_TERM_WINDOW_INFO, &reply) != B_OK)
317 		return false;
318 
319 	bool minimized;
320 	int32 workspaces;
321 	if (reply.FindBool("minimized", &minimized) != B_OK
322 		|| reply.FindInt32("workspaces", &workspaces) != B_OK)
323 		return false;
324 
325 	return !minimized && (workspaces & currentWorkspace) != 0;
326 }
327 
328 
329 /*!	Checks if all teams that have an ID-to-team mapping in the message
330 	are still running.
331 	The IDs for teams that are gone will be made available again, and
332 	their mapping is removed from the message.
333 */
334 void
335 TermApp::_SanitizeIDs(BMessage* data, uint8* windows, ssize_t length)
336 {
337 	BList teams;
338 	be_roster->GetAppList(TERM_SIGNATURE, &teams);
339 
340 	for (int32 i = 0; i < length; i++) {
341 		if (!windows[i])
342 			continue;
343 
344 		BString id("id-");
345 		id << i + 1;
346 
347 		team_id team;
348 		if (data->FindInt32(id.String(), &team) != B_OK)
349 			continue;
350 
351 		if (!teams.HasItem((void*)team)) {
352 			windows[i] = false;
353 			data->RemoveName(id.String());
354 		}
355 	}
356 }
357 
358 
359 /*!
360 	Removes the current fWindowNumber (ID) from the supplied array, or
361 	finds a free ID in it, and sets fWindowNumber accordingly.
362 */
363 bool
364 TermApp::_UpdateIDs(bool set, uint8* windows, ssize_t maxLength,
365 	ssize_t* _length)
366 {
367 	ssize_t length = *_length;
368 
369 	if (set) {
370 		int32 i;
371 		for (i = 0; i < length; i++) {
372 			if (!windows[i]) {
373 				windows[i] = true;
374 				fWindowNumber = i + 1;
375 				break;
376 			}
377 		}
378 
379 		if (i == length) {
380 			if (length >= maxLength)
381 				return false;
382 
383 			windows[length] = true;
384 			length++;
385 			fWindowNumber = length;
386 		}
387 	} else {
388 		// update information and write it back
389 		windows[fWindowNumber - 1] = false;
390 	}
391 
392 	*_length = length;
393 	return true;
394 }
395 
396 
397 void
398 TermApp::_UpdateRegistration(bool set)
399 {
400 	if (set)
401 		fWindowNumber = -1;
402 	else if (fWindowNumber < 0)
403 		return;
404 
405 #ifdef __HAIKU__
406 	// use BClipboard - it supports atomic access in Haiku
407 	BClipboard clipboard(TERM_SIGNATURE);
408 
409 	while (true) {
410 		if (!clipboard.Lock())
411 			return;
412 
413 		BMessage* data = clipboard.Data();
414 
415 		const uint8* windowsData;
416 		uint8 windows[512];
417 		ssize_t length;
418 		if (data->FindData("ids", B_RAW_TYPE,
419 				(const void**)&windowsData, &length) != B_OK)
420 			length = 0;
421 
422 		if (length > (ssize_t)sizeof(windows))
423 			length = sizeof(windows);
424 		if (length > 0)
425 			memcpy(windows, windowsData, length);
426 
427 		_SanitizeIDs(data, windows, length);
428 
429 		status_t status = B_OK;
430 		if (_UpdateIDs(set, windows, sizeof(windows), &length)) {
431 			// add/remove our ID-to-team mapping
432 			BString id("id-");
433 			id << fWindowNumber;
434 
435 			if (set)
436 				data->AddInt32(id.String(), Team());
437 			else
438 				data->RemoveName(id.String());
439 
440 			data->RemoveName("ids");
441 			//if (data->ReplaceData("ids", B_RAW_TYPE, windows, length) != B_OK)
442 			data->AddData("ids", B_RAW_TYPE, windows, length);
443 
444 			status = clipboard.Commit(true);
445 		}
446 
447 		clipboard.Unlock();
448 
449 		if (status == B_OK)
450 			break;
451 	}
452 #else	// !__HAIKU__
453 	// use a file to store the IDs - unfortunately, locking
454 	// doesn't work on BeOS either here
455 	int fd = open("/tmp/terminal_ids", O_RDWR | O_CREAT);
456 	if (fd < 0)
457 		return;
458 
459 	struct flock lock;
460 	lock.l_type = F_WRLCK;
461 	lock.l_whence = SEEK_CUR;
462 	lock.l_start = 0;
463 	lock.l_len = -1;
464 	fcntl(fd, F_SETLKW, &lock);
465 
466 	uint8 windows[512];
467 	ssize_t length = read_pos(fd, 0, windows, sizeof(windows));
468 	if (length < 0) {
469 		close(fd);
470 		return;
471 	}
472 
473 	if (length > (ssize_t)sizeof(windows))
474 		length = sizeof(windows);
475 
476 	if (_UpdateIDs(set, windows, sizeof(windows), &length))
477 		write_pos(fd, 0, windows, length);
478 
479 	close(fd);
480 #endif	// !__HAIKU__
481 }
482 
483 
484 void
485 TermApp::_UnregisterTerminal()
486 {
487 	_UpdateRegistration(false);
488 }
489 
490 
491 void
492 TermApp::_RegisterTerminal()
493 {
494 	_UpdateRegistration(true);
495 }
496 
497 
498 //#ifndef B_NETPOSITIVE_APP_SIGNATURE
499 //#define B_NETPOSITIVE_APP_SIGNATURE "application/x-vnd.Be-NPOS"
500 //#endif
501 //
502 //void
503 //TermApp::ShowHTML(BMessage *msg)
504 //{
505 //  const char *url;
506 //  msg->FindString("Url", &url);
507 //  BMessage message;
508 //
509 //  message.what = B_NETPOSITIVE_OPEN_URL;
510 //  message.AddString("be:url", url);
511 
512 //  be_roster->Launch(B_NETPOSITIVE_APP_SIGNATURE, &message);
513 //  while(!(be_roster->IsRunning(B_NETPOSITIVE_APP_SIGNATURE)))
514 //    snooze(10000);
515 //
516 //  // Activate net+
517 //  be_roster->ActivateApp(be_roster->TeamFor(B_NETPOSITIVE_APP_SIGNATURE));
518 //}
519 
520 
521 void
522 TermApp::_HandleChildCleanup()
523 {
524 }
525 
526 
527 /*static*/ void
528 TermApp::_SigChildHandler(int signal, void* data)
529 {
530 	// Spawing a thread that does the actual signal handling is pretty much
531 	// the only safe thing to do in a multi-threaded application. The
532 	// interrupted thread might have been anywhere, e.g. in a critical section,
533 	// holding locks. If we do anything that does require locking at any point
534 	// (e.g. memory allocation, messaging), we risk a dead-lock or data
535 	// structure corruption. Spawing a thread is safe though, since its only
536 	// a system call.
537 	thread_id thread = spawn_thread(_ChildCleanupThread, "child cleanup",
538 		B_NORMAL_PRIORITY, ((TermApp*)data)->fTermWindow);
539 	if (thread >= 0)
540 		resume_thread(thread);
541 }
542 
543 
544 /*static*/ status_t
545 TermApp::_ChildCleanupThread(void* data)
546 {
547 	// Just drop the windowa message and let it do the actual work. This
548 	// saves us additional synchronization measures.
549 	return ((TermWindow*)data)->PostMessage(MSG_CHECK_CHILDREN);
550 }
551 
552 
553 
554 void
555 TermApp::_Usage(char *name)
556 {
557 	fprintf(stderr, "Haiku Terminal\n"
558 		"Copyright 2001-2009 Haiku, Inc.\n"
559 		"Copyright(C) 1999 Kazuho Okui and Takashi Murai.\n"
560 		"\n"
561 		"Usage: %s [OPTION] [SHELL]\n", name);
562 
563 	fprintf(stderr,
564 		"  -h,     --help               print this help\n"
565 		//"  -p,     --preference         load preference file\n"
566 		"  -t,     --title              set window title\n"
567 		"  -f,     --fullscreen         start fullscreen\n"
568 		//"  -geom,  --geometry           set window geometry\n"
569 		//"                               An example of geometry is \"80x25+100+100\"\n"
570 		);
571 }
572 
573