1/*
2 * Copyright 2010, Ingo Weinhold, ingo_weinhold@gmx.de.
3 * Distributed under the terms of the MIT License.
4 */
5
6
7#include "TerminalRoster.h"
8
9#include <stdio.h>
10
11#include <new>
12
13#include <Looper.h>
14#include <Roster.h>
15#include <String.h>
16
17#include <AutoLocker.h>
18
19#include "TermConst.h"
20
21
22static const bigtime_t kAppsRunningCheckInterval = 1000000;
23
24
25// #pragma mark - Info
26
27
28/*!	Creates an Info with the given \a id and \a team ID.
29	\c workspaces is set to 0 and \c minimized to \c true.
30*/
31TerminalRoster::Info::Info(int32 id, team_id team)
32	:
33	id(id),
34	team(team),
35	workspaces(0),
36	minimized(true)
37{
38}
39
40
41/*!	Create an Info and initializes its data from \a archive.
42*/
43TerminalRoster::Info::Info(const BMessage& archive)
44{
45	if (archive.FindInt32("id", &id) != B_OK)
46		id = -1;
47	if (archive.FindInt32("team", &team) != B_OK)
48		team = -1;
49	if (archive.FindUInt32("workspaces", &workspaces) != B_OK)
50		workspaces = 0;
51	if (archive.FindBool("minimized", &minimized) != B_OK)
52		minimized = true;
53}
54
55
56/*!	Writes the Info's data into fields of \a archive.
57	The const BMessage& constructor can restore an identical Info from it.
58*/
59status_t
60TerminalRoster::Info::Archive(BMessage& archive) const
61{
62	status_t error;
63	if ((error = archive.AddInt32("id", id)) != B_OK
64		|| (error = archive.AddInt32("team", team)) != B_OK
65		|| (error = archive.AddUInt32("workspaces", workspaces)) != B_OK
66		|| (error = archive.AddBool("minimized", minimized)) != B_OK) {
67		return error;
68	}
69
70	return B_OK;
71}
72
73
74/*!	Compares two Infos.
75	Infos are considered equal, iff all data members are.
76*/
77bool
78TerminalRoster::Info::operator==(const Info& other) const
79{
80	return id == other.id && team == other.team
81		&& workspaces == other.workspaces && minimized == other.minimized;
82}
83
84
85// #pragma mark - TerminalRoster
86
87
88/*!	Creates a TerminalRoster.
89	Most methods cannot be used until Register() has been invoked.
90*/
91TerminalRoster::TerminalRoster()
92	:
93	BHandler("terminal roster"),
94	fLock("terminal roster"),
95	fClipboard(TERM_SIGNATURE),
96	fInfos(10, true),
97	fOurInfo(NULL),
98	fLastCheckedTime(0),
99	fListener(NULL),
100	fInfosUpdated(false)
101{
102}
103
104
105/*!	Locks the object.
106	Also makes sure the roster list is reasonably up-to-date.
107*/
108bool
109TerminalRoster::Lock()
110{
111	// lock
112	bool locked = fLock.Lock();
113	if (!locked)
114		return false;
115
116	// make sure we're registered
117	if (fOurInfo == NULL) {
118		fLock.Unlock();
119		return false;
120	}
121
122	// If the check interval has passed, make sure all infos still have running
123	// teams.
124	bigtime_t now = system_time();
125	if (fLastCheckedTime + kAppsRunningCheckInterval) {
126		bool needsUpdate = false;
127		for (int32 i = 0; const Info* info = TerminalAt(i); i++) {
128			if (!_TeamIsRunning(info->team)) {
129				needsUpdate = true;
130				break;
131			}
132		}
133
134		if (needsUpdate) {
135			AutoLocker<BClipboard> clipboardLocker(fClipboard);
136			if (clipboardLocker.IsLocked()) {
137				if (_UpdateInfos(true) == B_OK)
138					_UpdateClipboard();
139			}
140		} else
141			fLastCheckedTime = now;
142	}
143
144	return true;
145}
146
147
148/*!	Unlocks the object.
149	As a side effect the listener will be notified, if the terminal list has
150	changed in any way.
151*/
152void
153TerminalRoster::Unlock()
154{
155	if (fOurInfo != NULL && fInfosUpdated) {
156		// the infos have changed -- notify our listener
157		_NotifyListener();
158	}
159
160	fLock.Unlock();
161}
162
163
164/*!	Registers a terminal with the roster and establishes a link.
165
166	The object attaches itself to the supplied \a looper and will receive
167	updates via messaging (obviously the looper must run (not necessarily
168	right now) for this to work).
169
170	\param teamID The team ID of this team.
171	\param looper A looper the object can attach itself to.
172	\return \c B_OK, if successful, another error code otherwise.
173*/
174status_t
175TerminalRoster::Register(team_id teamID, BLooper* looper)
176{
177	AutoLocker<BLocker> locker(fLock);
178
179	if (fOurInfo != NULL) {
180		// already registered
181		return B_BAD_VALUE;
182	}
183
184	// lock the clipboard
185	AutoLocker<BClipboard> clipboardLocker(fClipboard);
186	if (!clipboardLocker.IsLocked())
187		return B_BAD_VALUE;
188
189	// get the current infos from the clipboard
190	status_t error = _UpdateInfos(true);
191	if (error != B_OK)
192		return error;
193
194	// find an unused ID
195	int32 id = 0;
196	for (int32 i = 0; const Info* info = TerminalAt(i); i++) {
197		if (info->id > id)
198			break;
199		id++;
200	}
201
202	// create our own info
203	fOurInfo = new(std::nothrow) Info(id, teamID);
204	if (fOurInfo == NULL)
205		return B_NO_MEMORY;
206
207	// insert it
208	if (!fInfos.BinaryInsert(fOurInfo, &_CompareInfos)) {
209		delete fOurInfo;
210		fOurInfo = NULL;
211		return B_NO_MEMORY;
212	}
213
214	// update the clipboard
215	error = _UpdateClipboard();
216	if (error != B_OK) {
217		fInfos.MakeEmpty(true);
218		fOurInfo = NULL;
219		return error;
220	}
221
222	// add ourselves to the looper and start watching
223	looper->AddHandler(this);
224
225	be_roster->StartWatching(this, B_REQUEST_QUIT);
226	fClipboard.StartWatching(this);
227
228	// Update again in case we've missed a update message sent before we were
229	// listening.
230	_UpdateInfos(false);
231
232	return B_OK;
233}
234
235
236/*!	Unregisters the terminal from the roster and closes the link.
237
238	Basically undoes all effects of Register().
239*/
240void
241TerminalRoster::Unregister()
242{
243	AutoLocker<BLocker> locker(fLock);
244	if (!locker.IsLocked())
245		return;
246
247	// stop watching and remove ourselves from the looper
248	be_roster->StartWatching(this);
249	fClipboard.StartWatching(this);
250
251	Looper()->RemoveHandler(this);
252
253	// lock the clipboard and get the current infos
254	AutoLocker<BClipboard> clipboardLocker(fClipboard);
255	if (!clipboardLocker.IsLocked() || _UpdateInfos(false) != B_OK)
256		return;
257
258	// remove our info and update the clipboard
259	fInfos.RemoveItem(fOurInfo);
260	fOurInfo = NULL;
261
262	_UpdateClipboard();
263}
264
265
266/*!	Returns the ID assigned to this terminal when it was registered.
267*/
268int32
269TerminalRoster::ID() const
270{
271	return fOurInfo != NULL ? fOurInfo->id : -1;
272}
273
274
275/*!	Updates this terminal's window status.
276	All other running terminals will be notified, if the status changed.
277
278	\param minimized \c true, if the window is minimized.
279	\param workspaces The window's workspaces mask.
280*/
281void
282TerminalRoster::SetWindowInfo(bool minimized, uint32 workspaces)
283{
284	AutoLocker<TerminalRoster> locker(this);
285	if (!locker.IsLocked())
286		return;
287
288	if (minimized == fOurInfo->minimized && workspaces == fOurInfo->workspaces)
289		return;
290
291	fOurInfo->minimized = minimized;
292	fOurInfo->workspaces = workspaces;
293	fInfosUpdated = true;
294
295	// lock the clipboard and get the current infos
296	AutoLocker<BClipboard> clipboardLocker(fClipboard);
297	if (!clipboardLocker.IsLocked() || _UpdateInfos(false) != B_OK)
298		return;
299
300	// update the clipboard to make our change known to the others
301	_UpdateClipboard();
302}
303
304
305/*!	Overriden to handle update messages.
306*/
307void
308TerminalRoster::MessageReceived(BMessage* message)
309{
310	switch (message->what) {
311		case B_SOME_APP_QUIT:
312		{
313			BString signature;
314			if (message->FindString("be:signature", &signature) != B_OK
315				|| signature != TERM_SIGNATURE) {
316				break;
317			}
318			// fall through
319		}
320
321		case B_CLIPBOARD_CHANGED:
322		{
323			// lock ourselves and the clipboard and update the infos
324			AutoLocker<TerminalRoster> locker(this);
325			AutoLocker<BClipboard> clipboardLocker(fClipboard);
326			if (clipboardLocker.IsLocked()) {
327				_UpdateInfos(false);
328
329				if (fInfosUpdated)
330					_NotifyListener();
331			}
332
333			break;
334		}
335
336		default:
337			BHandler::MessageReceived(message);
338			break;
339	}
340}
341
342
343/*!	Updates the terminal info list from the clipboard.
344
345	\param checkApps If \c true, it is checked for each found info whether the
346		respective team is still running. If not, the info is removed from the
347		list (though not from the clipboard).
348	\return \c B_OK, if the update went fine, another error code otherwise. When
349		an error occurs the object state will still be consistent, but might no
350		longer be up-to-date.
351*/
352status_t
353TerminalRoster::_UpdateInfos(bool checkApps)
354{
355	BMessage* data = fClipboard.Data();
356
357	// find out how many infos we can expect
358	type_code type;
359	int32 count;
360	status_t error = data->GetInfo("teams", &type, &count);
361	if (error != B_OK)
362		count = 0;
363
364	// create an info list from the message
365	InfoList infos(10, true);
366	for (int32 i = 0; i < count; i++) {
367		// get the team's message
368		BMessage teamData;
369		error = data->FindMessage("teams", i, &teamData);
370		if (error != B_OK)
371			return error;
372
373		// create the info
374		Info* info = new(std::nothrow) Info(teamData);
375		if (info == NULL)
376			return B_NO_MEMORY;
377		if (info->id < 0 || info->team < 0
378			|| infos.BinarySearchByKey(info->id, &_CompareIDInfo) != NULL
379			|| (checkApps && !_TeamIsRunning(info->team))) {
380			// invalid/duplicate info -- skip
381			delete info;
382			fInfosUpdated = true;
383			continue;
384		}
385
386		// add it to the list
387		if (!infos.BinaryInsert(info, &_CompareInfos)) {
388			delete info;
389			return B_NO_MEMORY;
390		}
391	}
392
393	// update the current info list from the infos we just read
394	int32 oldIndex = 0;
395	int32 newIndex = 0;
396	while (oldIndex < fInfos.CountItems() || newIndex < infos.CountItems()) {
397		Info* oldInfo = fInfos.ItemAt(oldIndex);
398		Info* newInfo = infos.ItemAt(newIndex);
399
400		if (oldInfo == NULL || (newInfo != NULL && oldInfo->id > newInfo->id)) {
401			// new info is not in old list -- transfer it
402			if (!fInfos.AddItem(newInfo, oldIndex++))
403				return B_NO_MEMORY;
404			infos.RemoveItemAt(newIndex);
405			fInfosUpdated = true;
406		} else if (newInfo == NULL || oldInfo->id < newInfo->id) {
407			// old info is not in new list -- delete it, unless it's our own
408			if (oldInfo == fOurInfo) {
409				oldIndex++;
410			} else {
411				delete fInfos.RemoveItemAt(oldIndex);
412				fInfosUpdated = true;
413			}
414		} else {
415			// info is in both lists -- update the old info, unless it's our own
416			if (oldInfo != fOurInfo) {
417				if (*oldInfo != *newInfo) {
418					*oldInfo = *newInfo;
419					fInfosUpdated = true;
420				}
421			}
422
423			oldIndex++;
424			newIndex++;
425		}
426	}
427
428	if (checkApps)
429		fLastCheckedTime = system_time();
430
431	return B_OK;
432}
433
434
435/*!	Updates the clipboard with the object's terminal info list.
436
437	\return \c B_OK, if the update went fine, another error code otherwise. When
438		an error occurs the object state will still be consistent, but might no
439		longer be in sync with the clipboard.
440*/
441status_t
442TerminalRoster::_UpdateClipboard()
443{
444	// get the clipboard data message
445	BMessage* data = fClipboard.Data();
446	if (data == NULL)
447		return B_BAD_VALUE;
448
449	// clear the message and add all infos
450	data->MakeEmpty();
451
452	for (int32 i = 0; const Info* info = TerminalAt(i); i++) {
453		BMessage teamData;
454		status_t error = info->Archive(teamData);
455		if (error != B_OK
456			|| (error = data->AddMessage("teams", &teamData)) != B_OK) {
457			fClipboard.Revert();
458			return error;
459		}
460	}
461
462	// commit the changes
463	status_t error = fClipboard.Commit();
464	if (error != B_OK) {
465		fClipboard.Revert();
466		return error;
467	}
468
469	return B_OK;
470}
471
472
473/*!	Notifies the listener, if something has changed.
474*/
475void
476TerminalRoster::_NotifyListener()
477{
478	if (!fInfosUpdated)
479		return;
480
481	if (fListener != NULL)
482		fListener->TerminalInfosUpdated(this);
483
484	fInfosUpdated = false;
485}
486
487
488/*static*/ int
489TerminalRoster::_CompareInfos(const Info* a, const Info* b)
490{
491	return a->id - b->id;
492}
493
494
495/*static*/ int
496TerminalRoster::_CompareIDInfo(const int32* id, const Info* info)
497{
498	return *id - info->id;
499}
500
501
502bool
503TerminalRoster::_TeamIsRunning(team_id teamID)
504{
505	// we are running for sure
506	if (fOurInfo != NULL && fOurInfo->team == teamID)
507		return true;
508
509	team_info info;
510	return get_team_info(teamID, &info) == B_OK;
511}
512
513
514// #pragma mark - Listener
515
516
517TerminalRoster::Listener::~Listener()
518{
519}
520