Passed
Branch 45-mixed-bag-of-php-modernizat... (e37abd)
by Michael
04:48
created

grommunio-sync-top.php (3 issues)

1
#!/usr/bin/env php
2
<?php
3
/*
4
 * SPDX-License-Identifier: AGPL-3.0-only
5
 * SPDX-FileCopyrightText: Copyright 2007-2016 Zarafa Deutschland GmbH
6
 * SPDX-FileCopyrightText: Copyright 2020-2022 grommunio GmbH
7
 *
8
 * Shows realtime information about connected devices and active connections
9
 * in a top-style format.
10
 */
11
12
require_once 'vendor/autoload.php';
13
if (!defined('GSYNC_CONFIG')) {
14
	define('GSYNC_CONFIG', 'config.php');
15
}
16
17
include_once GSYNC_CONFIG;
18
19
/*
20
 * MAIN
21
 */
22
	declare(ticks=1);
23
	define('BASE_PATH_CLI', dirname(__FILE__) . "/");
24
	set_include_path(get_include_path() . PATH_SEPARATOR . BASE_PATH_CLI);
25
26
	if (!defined('GSYNC_CONFIG')) {
27
		define('GSYNC_CONFIG', BASE_PATH_CLI . 'config.php');
28
	}
29
30
	include_once GSYNC_CONFIG;
31
32
	try {
33
		GSync::CheckConfig();
34
		if (!function_exists("pcntl_signal")) {
35
			throw new FatalException("Function pcntl_signal() is not available. Please install package 'php5-pcntl' (or similar) on your system.");
36
		}
37
38
		if (php_sapi_name() != "cli") {
39
			throw new FatalException("This script can only be called from the CLI.");
40
		}
41
42
		$zpt = new GSyncTop();
43
44
		// check if help was requested from CLI
45
		if (in_array('-h', $argv) || in_array('--help', $argv)) {
46
			echo $zpt->UsageInstructions();
47
48
			exit(0);
49
		}
50
51
		if ($zpt->IsAvailable()) {
52
			pcntl_signal(SIGINT, [$zpt, "SignalHandler"]);
53
			$zpt->run();
54
			$zpt->scrClear();
55
			system("stty sane");
56
		}
57
		else {
58
			echo "grommunio-sync interprocess communication (IPC) is not available or TopCollector is disabled.\n";
59
		}
60
	}
61
	catch (GSyncException $zpe) {
62
		fwrite(STDERR, get_class($zpe) . ": " . $zpe->getMessage() . "\n");
63
64
		exit(1);
65
	}
66
67
	echo "terminated\n";
68
69
/*
70
 * grommunio-sync-top
71
 */
72
class GSyncTop {
73
	// show options
74
	public const SHOW_DEFAULT = 0;
75
	public const SHOW_ACTIVE_ONLY = 1;
76
	public const SHOW_UNKNOWN_ONLY = 2;
77
	public const SHOW_TERM_DEFAULT_TIME = 5; // 5 secs
78
79
	private $topCollector;
80
	private $starttime;
81
	private $currenttime;
82
	private $action;
83
	private $filter;
84
	private $status;
85
	private $statusexpire;
86
	private $helpexpire;
87
	private $doingTail;
88
	private $wide;
89
	private $wasEnabled;
90
	private $terminate;
91
	private $scrSize;
92
	private $pingInterval;
93
	private $showPush;
94
	private $showOption;
95
	private $showTermSec;
96
97
	private $linesActive = [];
98
	private $linesOpen = [];
99
	private $linesUnknown = [];
100
	private $linesTerm = [];
101
	private $pushConn = 0;
102
	private $activeConn = [];
103
	private $activeHosts = [];
104
	private $activeUsers = [];
105
	private $activeDevices = [];
106
107
	/**
108
	 * Constructor.
109
	 */
110
	public function __construct() {
111
		$this->starttime = time();
112
		$this->currenttime = time();
113
		$this->action = "";
114
		$this->filter = false;
115
		$this->status = false;
116
		$this->statusexpire = 0;
117
		$this->helpexpire = 0;
118
		$this->doingTail = false;
119
		$this->wide = false;
120
		$this->terminate = false;
121
		$this->showPush = true;
122
		$this->showOption = self::SHOW_DEFAULT;
123
		$this->showTermSec = self::SHOW_TERM_DEFAULT_TIME;
124
		$this->scrSize = ['width' => 80, 'height' => 24];
125
		$this->pingInterval = (defined('PING_INTERVAL') && PING_INTERVAL > 0) ? PING_INTERVAL : 12;
126
127
		// get a TopCollector
128
		$this->topCollector = new TopCollector();
129
	}
130
131
	/**
132
	 * Requests data from the running grommunio-sync processes.
133
	 *
134
	 * @return
135
	 */
136
	private function initialize() {
137
		// request feedback from active processes
138
		$this->wasEnabled = $this->topCollector->CollectData();
139
140
		// remove obsolete data
141
		$this->topCollector->ClearLatest(true);
142
143
		// start with default colours
144
		$this->scrDefaultColors();
145
	}
146
147
	/**
148
	 * Main loop of grommunio-sync-top
149
	 * Runs until termination is requested.
150
	 *
151
	 * @return
152
	 */
153
	public function run() {
154
		$this->initialize();
155
156
		do {
157
			$this->currenttime = time();
158
159
			// see if shared memory is active
160
			if (!$this->IsAvailable()) {
161
				$this->terminate = true;
162
			}
163
164
			// active processes should continue sending data
165
			$this->topCollector->CollectData();
166
167
			// get and process data from processes
168
			$this->topCollector->ClearLatest();
169
			$topdata = $this->topCollector->ReadLatest();
170
			$this->processData($topdata);
171
172
			// clear screen
173
			$this->scrClear();
174
175
			// check if screen size changed
176
			$s = $this->scrGetSize();
177
			if ($this->scrSize['width'] != $s['width']) {
178
				if ($s['width'] > 180) {
179
					$this->wide = true;
180
				}
181
				else {
182
					$this->wide = false;
183
				}
184
			}
185
			$this->scrSize = $s;
186
187
			// print overview
188
			$this->scrOverview();
189
190
			// wait for user input
191
			$this->readLineProcess();
192
		}
193
		while ($this->terminate !== true);
194
	}
195
196
	/**
197
	 * Indicates if TopCollector is available collecting data.
198
	 *
199
	 * @return bool
200
	 */
201
	public function IsAvailable() {
202
		if (defined('TOPCOLLECTOR_DISABLED') && constant('TOPCOLLECTOR_DISABLED') === true) {
203
			return false;
204
		}
205
206
		return $this->topCollector->IsActive();
207
	}
208
209
	/**
210
	 * Processes data written by the running processes.
211
	 *
212
	 * @param array $data
213
	 *
214
	 * @return
215
	 */
216
	private function processData($data) {
217
		$this->linesActive = [];
218
		$this->linesOpen = [];
219
		$this->linesUnknown = [];
220
		$this->linesTerm = [];
221
		$this->pushConn = 0;
222
		$this->activeConn = [];
223
		$this->activeHosts = [];
224
		$this->activeUsers = [];
225
		$this->activeDevices = [];
226
227
		if (!is_array($data)) {
0 ignored issues
show
The condition is_array($data) is always true.
Loading history...
228
			return;
229
		}
230
231
		foreach ($data as $devid => $users) {
232
			foreach ($users as $user => $pids) {
233
				foreach ($pids as $pid => $line) {
234
					if (!is_array($line)) {
235
						continue;
236
					}
237
238
					$line['command'] = Utils::GetCommandFromCode($line['command']);
239
240
					if ($line["ended"] == 0) {
241
						$this->activeDevices[$devid] = 1;
242
						$this->activeUsers[$user] = 1;
243
						$this->activeConn[$pid] = 1;
244
						$this->activeHosts[$line['ip']] = 1;
245
246
						$line["time"] = $this->currenttime - $line['start'];
247
						if ($line['push'] === true) {
248
							++$this->pushConn;
249
						}
250
251
						// ignore push connections
252
						if ($line['push'] === true && !$this->showPush) {
253
							continue;
254
						}
255
256
						if ($this->filter !== false) {
257
							$f = $this->filter;
258
							if (!($line["pid"] == $f || $line["ip"] == $f || strtolower($line['command']) == strtolower($f) || preg_match("/.*?{$f}.*?/i", $line['user']) ||
0 ignored issues
show
$f of type true is incompatible with the type string expected by parameter $string of strtolower(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

258
							if (!($line["pid"] == $f || $line["ip"] == $f || strtolower($line['command']) == strtolower(/** @scrutinizer ignore-type */ $f) || preg_match("/.*?{$f}.*?/i", $line['user']) ||
Loading history...
259
								preg_match("/.*?{$f}.*?/i", $line['devagent']) || preg_match("/.*?{$f}.*?/i", $line['devid']) || preg_match("/.*?{$f}.*?/i", $line['addinfo']))) {
260
								continue;
261
							}
262
						}
263
264
						$lastUpdate = $this->currenttime - $line["update"];
265
						if ($this->currenttime - $line["update"] < 2) {
266
							$this->linesActive[$line["update"] . $line["pid"]] = $line;
267
						}
268
						elseif (($line['push'] === true && $lastUpdate > ($this->pingInterval + 2)) || ($line['push'] !== true && $lastUpdate > 4)) {
269
							$this->linesUnknown[$line["update"] . $line["pid"]] = $line;
270
						}
271
						else {
272
							$this->linesOpen[$line["update"] . $line["pid"]] = $line;
273
						}
274
					}
275
					else {
276
						// do not show terminated + expired connections
277
						if ($this->currenttime > $line['ended'] + $this->showTermSec) {
278
							continue;
279
						}
280
281
						if ($this->filter !== false) {
282
							$f = $this->filter;
283
							if (
284
									!(
285
										$line['pid'] == $f ||
286
										$line['ip'] == $f ||
287
										strtolower($line['command']) == strtolower($f) ||
288
										preg_match("/.*?{$f}.*?/i", $line['user']) ||
289
										preg_match("/.*?{$f}.*?/i", $line['devagent']) ||
290
										preg_match("/.*?{$f}.*?/i", $line['devid']) ||
291
										preg_match("/.*?{$f}.*?/i", $line['addinfo'])
292
									)) {
293
								continue;
294
							}
295
						}
296
297
						$line['time'] = $line['ended'] - $line['start'];
298
						$this->linesTerm[$line['update'] . $line['pid']] = $line;
299
					}
300
				}
301
			}
302
		}
303
304
		// sort by execution time
305
		krsort($this->linesActive);
306
		krsort($this->linesOpen);
307
		krsort($this->linesUnknown);
308
		krsort($this->linesTerm);
309
	}
310
311
	/**
312
	 * Prints data to the terminal.
313
	 *
314
	 * @return
315
	 */
316
	private function scrOverview() {
317
		$linesAvail = $this->scrSize['height'] - 8;
318
		$lc = 1;
319
		$this->scrPrintAt($lc, 0, "\033[1mgrommunio-sync-top live statistics\033[0m\t\t\t\t\t" . @strftime("%d/%m/%Y %T") . "\n");
0 ignored issues
show
Are you sure @strftime('%d/%m/%Y %T') of type false|string can be used in concatenation? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

319
		$this->scrPrintAt($lc, 0, "\033[1mgrommunio-sync-top live statistics\033[0m\t\t\t\t\t" . /** @scrutinizer ignore-type */ @strftime("%d/%m/%Y %T") . "\n");
Loading history...
320
		++$lc;
321
322
		$this->scrPrintAt($lc, 0, sprintf("Open connections: %d\t\t\t\tUsers:\t %d\tgrommunio-sync:   %s ", count($this->activeConn), count($this->activeUsers), $this->getVersion()));
323
		++$lc;
324
		$this->scrPrintAt($lc, 0, sprintf("Push connections: %d\t\t\t\tDevices: %d\tPHP-MAPI: %s", $this->pushConn, count($this->activeDevices), phpversion("mapi")));
325
		++$lc;
326
		$this->scrPrintAt($lc, 0, sprintf("                                                Hosts:\t %d", count($this->activeHosts)));
327
		++$lc;
328
		++$lc;
329
330
		$this->scrPrintAt($lc, 0, "\033[4m" . $this->getLine(['pid' => 'PID', 'ip' => 'IP', 'user' => 'USER', 'command' => 'COMMAND', 'time' => 'TIME', 'devagent' => 'AGENT', 'devid' => 'DEVID', 'addinfo' => 'Additional Information']) . str_repeat(" ", 20) . "\033[0m");
331
		++$lc;
332
333
		// print help text if requested
334
		$hl = 0;
335
		if ($this->helpexpire > $this->currenttime) {
336
			$help = $this->scrHelp();
337
			$linesAvail -= count($help);
338
			$hl = $this->scrSize['height'] - count($help) - 1;
339
			foreach ($help as $h) {
340
				$this->scrPrintAt($hl, 0, $h);
341
				++$hl;
342
			}
343
		}
344
345
		$toPrintActive = $linesAvail;
346
		$toPrintOpen = $linesAvail;
347
		$toPrintUnknown = $linesAvail;
348
		$toPrintTerm = $linesAvail;
349
350
		// default view: show all unknown, no terminated and half active+open
351
		if (count($this->linesActive) + count($this->linesOpen) + count($this->linesUnknown) > $linesAvail) {
352
			$toPrintUnknown = count($this->linesUnknown);
353
			$toPrintActive = count($this->linesActive);
354
			$toPrintOpen = $linesAvail - $toPrintUnknown - $toPrintActive;
355
			$toPrintTerm = 0;
356
		}
357
358
		if ($this->showOption == self::SHOW_ACTIVE_ONLY) {
359
			$toPrintActive = $linesAvail;
360
			$toPrintOpen = 0;
361
			$toPrintUnknown = 0;
362
			$toPrintTerm = 0;
363
		}
364
365
		if ($this->showOption == self::SHOW_UNKNOWN_ONLY) {
366
			$toPrintActive = 0;
367
			$toPrintOpen = 0;
368
			$toPrintUnknown = $linesAvail;
369
			$toPrintTerm = 0;
370
		}
371
372
		$linesprinted = 0;
373
		foreach ($this->linesActive as $time => $l) {
374
			if ($linesprinted >= $toPrintActive) {
375
				break;
376
			}
377
378
			$this->scrPrintAt($lc, 0, "\033[01m" . $this->getLine($l) . "\033[0m");
379
			++$lc;
380
			++$linesprinted;
381
		}
382
383
		$linesprinted = 0;
384
		foreach ($this->linesOpen as $time => $l) {
385
			if ($linesprinted >= $toPrintOpen) {
386
				break;
387
			}
388
389
			$this->scrPrintAt($lc, 0, $this->getLine($l));
390
			++$lc;
391
			++$linesprinted;
392
		}
393
394
		$linesprinted = 0;
395
		foreach ($this->linesUnknown as $time => $l) {
396
			if ($linesprinted >= $toPrintUnknown) {
397
				break;
398
			}
399
400
			$color = "0;31m";
401
			if ($l['push'] == false && $time - $l["start"] > 30) {
402
				$color = "1;31m";
403
			}
404
			$this->scrPrintAt($lc, 0, "\033[0" . $color . $this->getLine($l) . "\033[0m");
405
			++$lc;
406
			++$linesprinted;
407
		}
408
409
		if ($toPrintTerm > 0) {
410
			$toPrintTerm = $linesAvail - $lc + 6;
411
		}
412
413
		$linesprinted = 0;
414
		foreach ($this->linesTerm as $time => $l) {
415
			if ($linesprinted >= $toPrintTerm) {
416
				break;
417
			}
418
419
			$this->scrPrintAt($lc, 0, "\033[01;30m" . $this->getLine($l) . "\033[0m");
420
			++$lc;
421
			++$linesprinted;
422
		}
423
424
		// add the lines used when displaying the help text
425
		$lc += $hl;
426
		$this->scrPrintAt($lc, 0, "\033[K");
427
		++$lc;
428
		$this->scrPrintAt($lc, 0, "Colorscheme: \033[01mActive  \033[0mOpen  \033[01;31mUnknown  \033[01;30mTerminated\033[0m");
429
430
		// remove old status
431
		if ($this->statusexpire < $this->currenttime) {
432
			$this->status = false;
433
		}
434
435
		// show request information and help command
436
		if ($this->starttime + 6 > $this->currenttime) {
437
			$this->status = sprintf("Requesting information (takes up to %dsecs)", $this->pingInterval) . str_repeat(".", ($this->currenttime - $this->starttime)) . "  type \033[01;31mh\033[00;31m or \033[01;31mhelp\033[00;31m for usage instructions";
438
			$this->statusexpire = $this->currenttime + 1;
439
		}
440
441
		$str = "";
442
		if (!$this->showPush) {
443
			$str .= "\033[00;32mPush: \033[01;32mNo\033[0m   ";
444
		}
445
446
		if ($this->showOption == self::SHOW_ACTIVE_ONLY) {
447
			$str .= "\033[01;32mActive only\033[0m   ";
448
		}
449
450
		if ($this->showOption == self::SHOW_UNKNOWN_ONLY) {
451
			$str .= "\033[01;32mUnknown only\033[0m   ";
452
		}
453
454
		if ($this->showTermSec != self::SHOW_TERM_DEFAULT_TIME) {
455
			$str .= "\033[01;32mTerminated: " . $this->showTermSec . "s\033[0m   ";
456
		}
457
458
		if ($this->filter !== false || ($this->status !== false && $this->statusexpire > $this->currenttime)) {
459
			// print filter in green
460
			if ($this->filter !== false) {
461
				$str .= "\033[00;32mFilter: \033[01;32m{$this->filter}\033[0m   ";
462
			}
463
			// print status in red
464
			if ($this->status !== false) {
465
				$str .= "\033[00;31m{$this->status}\033[0m";
466
			}
467
		}
468
		$this->scrPrintAt(5, 0, $str);
469
470
		$this->scrPrintAt(4, 0, "Action: \033[01m" . $this->action . "\033[0m");
471
	}
472
473
	/**
474
	 * Waits for a keystroke and processes the requested command.
475
	 *
476
	 * @return
477
	 */
478
	private function readLineProcess() {
479
		$ans = explode("^^", `bash -c "read -n 1 -t 1 ANS ; echo \\\$?^^\\\$ANS;"`);
480
481
		if ($ans[0] < 128) {
482
			if (isset($ans[1]) && bin2hex(trim($ans[1])) == "7f") {
483
				$this->action = substr($this->action, 0, -1);
484
			}
485
486
			if (isset($ans[1]) && $ans[1] != "") {
487
				$this->action .= trim(preg_replace("/[^A-Za-z0-9:]/", "", $ans[1]));
488
			}
489
490
			if (bin2hex($ans[0]) == "30" && bin2hex($ans[1]) == "0a") {
491
				$cmds = explode(':', $this->action);
492
				if ($cmds[0] == "quit" || $cmds[0] == "q" || (isset($cmds[1]) && $cmds[0] == "" && $cmds[1] == "q")) {
493
					$this->topCollector->CollectData(true);
494
					$this->topCollector->ClearLatest(true);
495
496
					$this->terminate = true;
497
				}
498
				elseif ($cmds[0] == "clear") {
499
					$this->topCollector->ClearLatest(true);
500
					$this->topCollector->CollectData(true);
501
					$this->topCollector->ReInitIPC();
502
				}
503
				elseif ($cmds[0] == "filter" || $cmds[0] == "f") {
504
					if (!isset($cmds[1]) || $cmds[1] == "") {
505
						$this->filter = false;
506
						$this->status = "No filter";
507
						$this->statusexpire = $this->currenttime + 5;
508
					}
509
					else {
510
						$this->filter = $cmds[1];
511
						$this->status = false;
512
					}
513
				}
514
				elseif ($cmds[0] == "option" || $cmds[0] == "o") {
515
					if (!isset($cmds[1]) || $cmds[1] == "") {
516
						$this->status = "Option value needs to be specified. See 'help' or 'h' for instructions";
517
						$this->statusexpire = $this->currenttime + 5;
518
					}
519
					elseif ($cmds[1] == "p" || $cmds[1] == "push" || $cmds[1] == "ping") {
520
						$this->showPush = !$this->showPush;
521
					}
522
					elseif ($cmds[1] == "a" || $cmds[1] == "active") {
523
						$this->showOption = self::SHOW_ACTIVE_ONLY;
524
					}
525
					elseif ($cmds[1] == "u" || $cmds[1] == "unknown") {
526
						$this->showOption = self::SHOW_UNKNOWN_ONLY;
527
					}
528
					elseif ($cmds[1] == "d" || $cmds[1] == "default") {
529
						$this->showOption = self::SHOW_DEFAULT;
530
						$this->showTermSec = self::SHOW_TERM_DEFAULT_TIME;
531
						$this->showPush = true;
532
					}
533
					elseif (is_numeric($cmds[1])) {
534
						$this->showTermSec = $cmds[1];
535
					}
536
					else {
537
						$this->status = sprintf("Option '%s' unknown", $cmds[1]);
538
						$this->statusexpire = $this->currenttime + 5;
539
					}
540
				}
541
				elseif ($cmds[0] == "reset" || $cmds[0] == "r") {
542
					$this->filter = false;
543
					$this->wide = false;
544
					$this->helpexpire = 0;
545
					$this->status = "reset";
546
					$this->statusexpire = $this->currenttime + 2;
547
				}
548
				// enable/disable wide view
549
				elseif ($cmds[0] == "wide" || $cmds[0] == "w") {
550
					$this->wide = !$this->wide;
551
					$this->status = ($this->wide) ? "w i d e  view" : "normal view";
552
					$this->statusexpire = $this->currenttime + 2;
553
				}
554
				elseif ($cmds[0] == "help" || $cmds[0] == "h") {
555
					$this->helpexpire = $this->currenttime + 20;
556
				}
557
				// grep the log file
558
				elseif (($cmds[0] == "log" || $cmds[0] == "l") && isset($cmds[1])) {
559
					if (!file_exists(LOGFILE)) {
560
						$this->status = "Logfile can not be found: " . LOGFILE;
561
					}
562
					else {
563
						system('bash -c "fgrep -a ' . escapeshellarg($cmds[1]) . ' ' . LOGFILE . ' | less +G" > `tty`');
564
						$this->status = "Returning from log, updating data";
565
					}
566
					$this->statusexpire = time() + 5; // it might be much "later" now
567
				}
568
				// tail the log file
569
				elseif (($cmds[0] == "tail" || $cmds[0] == "t")) {
570
					if (!file_exists(LOGFILE)) {
571
						$this->status = "Logfile can not be found: " . LOGFILE;
572
					}
573
					else {
574
						$this->doingTail = true;
575
						$this->scrClear();
576
						$this->scrPrintAt(1, 0, $this->scrAsBold("Press CTRL+C to return to grommunio-sync-top\n\n"));
577
						$secondary = "";
578
						if (isset($cmds[1])) {
579
							$secondary = " -n 200 | grep " . escapeshellarg($cmds[1]);
580
						}
581
						system('bash -c "tail -f ' . LOGFILE . $secondary . '" > `tty`');
582
						$this->doingTail = false;
583
						$this->status = "Returning from tail, updating data";
584
					}
585
					$this->statusexpire = time() + 5; // it might be much "later" now
586
				}
587
				// tail the error log file
588
				elseif (($cmds[0] == "error" || $cmds[0] == "e")) {
589
					if (!file_exists(LOGERRORFILE)) {
590
						$this->status = "Error logfile can not be found: " . LOGERRORFILE;
591
					}
592
					else {
593
						$this->doingTail = true;
594
						$this->scrClear();
595
						$this->scrPrintAt(1, 0, $this->scrAsBold("Press CTRL+C to return to grommunio-sync-top\n\n"));
596
						$secondary = "";
597
						if (isset($cmds[1])) {
598
							$secondary = " -n 200 | grep " . escapeshellarg($cmds[1]);
599
						}
600
						system('bash -c "tail -f ' . LOGERRORFILE . $secondary . '" > `tty`');
601
						$this->doingTail = false;
602
						$this->status = "Returning from tail, updating data";
603
					}
604
					$this->statusexpire = time() + 5; // it might be much "later" now
605
				}
606
				elseif ($cmds[0] != "") {
607
					$this->status = sprintf("Command '%s' unknown", $cmds[0]);
608
					$this->statusexpire = $this->currenttime + 8;
609
				}
610
				$this->action = "";
611
			}
612
		}
613
	}
614
615
	/**
616
	 * Signal handler function.
617
	 *
618
	 * @param int $signo signal number
619
	 *
620
	 * @return
621
	 */
622
	public function SignalHandler($signo) {
623
		// don't terminate if the signal was sent by terminating tail
624
		if (!$this->doingTail) {
625
			$this->topCollector->CollectData(true);
626
			$this->topCollector->ClearLatest(true);
627
			$this->terminate = true;
628
		}
629
	}
630
631
	/**
632
	 * Returns usage instructions.
633
	 *
634
	 * @return string
635
	 */
636
	public function UsageInstructions() {
637
		$help = "Usage:\n\tgrommunio-sync-top.php\n\n" .
638
				"  grommunio-sync-top is a live top-like overview of what grommunio-sync is doing. It does not have specific command line options.\n\n" .
639
				"  When grommunio-sync-top is running you can specify certain actions and options which can be executed (listed below).\n" .
640
				"  This help information can also be shown inside grommunio-sync-top by hitting 'help' or 'h'.\n\n";
641
		$scrhelp = $this->scrHelp();
642
		unset($scrhelp[0]);
643
644
		$help .= implode("\n", $scrhelp);
645
		$help .= "\n\n";
646
647
		return $help;
648
	}
649
650
	/**
651
	 * Prints a 'help' text at the end of the page.
652
	 *
653
	 * @return array with help lines
654
	 */
655
	private function scrHelp() {
656
		$h = [];
657
		$secs = $this->helpexpire - $this->currenttime;
658
		$h[] = "Actions supported by grommunio-sync-top (help page still displayed for " . $secs . "secs)";
659
		$h[] = "  " . $this->scrAsBold("Action") . "\t\t" . $this->scrAsBold("Comment");
660
		$h[] = "  " . $this->scrAsBold("h") . " or " . $this->scrAsBold("help") . "\t\tDisplays this information.";
661
		$h[] = "  " . $this->scrAsBold("q") . ", " . $this->scrAsBold("quit") . " or " . $this->scrAsBold(":q") . "\t\tExits grommunio-sync-top.";
662
		$h[] = "  " . $this->scrAsBold("w") . " or " . $this->scrAsBold("wide") . "\t\tTries not to truncate data. Automatically done if more than 180 columns available.";
663
		$h[] = "  " . $this->scrAsBold("f:VAL") . " or " . $this->scrAsBold("filter:VAL") . "\tOnly display connections which contain VAL. This value is case-insensitive.";
664
		$h[] = "  " . $this->scrAsBold("f:") . " or " . $this->scrAsBold("filter:") . "\t\tWithout a search word: resets the filter.";
665
		$h[] = "  " . $this->scrAsBold("l:STR") . " or " . $this->scrAsBold("log:STR") . "\tIssues 'less +G' on the logfile, after grepping on the optional STR.";
666
		$h[] = "  " . $this->scrAsBold("t:STR") . " or " . $this->scrAsBold("tail:STR") . "\tIssues 'tail -f' on the logfile, grepping for optional STR.";
667
		$h[] = "  " . $this->scrAsBold("e:STR") . " or " . $this->scrAsBold("error:STR") . "\tIssues 'tail -f' on the error logfile, grepping for optional STR.";
668
		$h[] = "  " . $this->scrAsBold("r") . " or " . $this->scrAsBold("reset") . "\t\tResets 'wide' or 'filter'.";
669
		$h[] = "  " . $this->scrAsBold("o:") . " or " . $this->scrAsBold("option:") . "\t\tSets display options. Valid options specified below";
670
		$h[] = "  " . $this->scrAsBold("  p") . " or " . $this->scrAsBold("push") . "\t\tLists/not lists active and open push connections.";
671
		$h[] = "  " . $this->scrAsBold("  a") . " or " . $this->scrAsBold("action") . "\t\tLists only active connections.";
672
		$h[] = "  " . $this->scrAsBold("  u") . " or " . $this->scrAsBold("unknown") . "\tLists only unknown connections.";
673
		$h[] = "  " . $this->scrAsBold("  10") . " or " . $this->scrAsBold("20") . "\t\tLists terminated connections for 10 or 20 seconds. Any other number can be used.";
674
		$h[] = "  " . $this->scrAsBold("  d") . " or " . $this->scrAsBold("default") . "\tUses default options";
675
676
		return $h;
677
	}
678
679
	/**
680
	 * Encapsulates string with different color escape characters.
681
	 *
682
	 * @param string $text
683
	 *
684
	 * @return string same text as bold
685
	 */
686
	private function scrAsBold($text) {
687
		return "\033[01m" . $text . "\033[0m";
688
	}
689
690
	/**
691
	 * Prints one line of precessed data.
692
	 *
693
	 * @param array $l line information
694
	 *
695
	 * @return string
696
	 */
697
	private function getLine($l) {
698
		if ($this->wide === true) {
699
			return sprintf("%s%s%s%s%s%s%s%s", $this->ptStr($l['pid'], 6), $this->ptStr($l['ip'], 16), $this->ptStr($l['user'], 24), $this->ptStr($l['command'], 16), $this->ptStr($this->sec2min($l['time']), 8), $this->ptStr($l['devagent'], 28), $this->ptStr($l['devid'], 33, true), $l['addinfo']);
700
		}
701
702
		return sprintf("%s%s%s%s%s%s%s%s", $this->ptStr($l['pid'], 6), $this->ptStr($l['ip'], 16), $this->ptStr($l['user'], 8), $this->ptStr($l['command'], 8), $this->ptStr($this->sec2min($l['time']), 6), $this->ptStr($l['devagent'], 20), $this->ptStr($l['devid'], 12, true), $l['addinfo']);
703
	}
704
705
	/**
706
	 * Pads and trims string.
707
	 *
708
	 * @param string $str       to be trimmed/padded
709
	 * @param int    $size      characters to be considered
710
	 * @param bool   $cutmiddle (optional) indicates where to long information should
711
	 *                          be trimmed of, false means at the end
712
	 *
713
	 * @return string
714
	 */
715
	private function ptStr($str, $size, $cutmiddle = false) {
716
		if (strlen($str) < $size) {
717
			return str_pad($str, $size);
718
		}
719
		if ($cutmiddle === true) {
720
			$cut = ($size - 2) / 2;
721
722
			return $this->ptStr(substr($str, 0, $cut) . ".." . substr($str, (-1) * ($cut - 1)), $size);
723
		}
724
725
		return substr($str, 0, $size - 3) . ".. ";
726
	}
727
728
	/**
729
	 * Tries to discover the size of the current terminal.
730
	 *
731
	 * @return array 'width' and 'height' as keys
732
	 */
733
	private function scrGetSize() {
734
		$tty = strtolower(exec('stty -a | fgrep columns'));
735
		if (preg_match_all("/rows.([0-9]+);.columns.([0-9]+);/", $tty, $output) ||
736
			preg_match_all("/([0-9]+).rows;.([0-9]+).columns;/", $tty, $output)) {
737
			return ['width' => $output[2][0], 'height' => $output[1][0]];
738
		}
739
740
		return ['width' => 80, 'height' => 24];
741
	}
742
743
	/**
744
	 * Returns the version of the current grommunio-sync installation.
745
	 *
746
	 * @return string
747
	 */
748
	private function getVersion() {
749
		return GROMMUNIOSYNC_VERSION;
750
	}
751
752
	/**
753
	 * Converts seconds in MM:SS.
754
	 *
755
	 * @param int $s seconds
756
	 *
757
	 * @return string
758
	 */
759
	private function sec2min($s) {
760
		if (!is_int($s)) {
761
			return $s;
762
		}
763
764
		return sprintf("%02.2d:%02.2d", floor($s / 60), $s % 60);
765
	}
766
767
	/**
768
	 * Resets the default colors of the terminal.
769
	 *
770
	 * @return
771
	 */
772
	private function scrDefaultColors() {
773
		echo "\033[0m";
774
	}
775
776
	/**
777
	 * Clears screen of the terminal.
778
	 *
779
	 * @param array $data
780
	 *
781
	 * @return
782
	 */
783
	public function scrClear() {
784
		echo "\033[2J";
785
	}
786
787
	/**
788
	 * Prints a text at a specific screen/terminal coordinates.
789
	 *
790
	 * @param int    $row  row number
791
	 * @param int    $col  column number
792
	 * @param string $text to be printed
793
	 *
794
	 * @return
795
	 */
796
	private function scrPrintAt($row, $col, $text = "") {
797
		echo "\033[" . $row . ";" . $col . "H" . $text;
798
	}
799
}
800