GSyncTop::scrPrintAt()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 2
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

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

281
							if (!($line["pid"] == $f || $line["ip"] == $f || strtolower((string) $line['command']) == strtolower(/** @scrutinizer ignore-type */ $f) || preg_match("/.*?{$f}.*?/i", (string) $line['user']) ||
Loading history...
282
								preg_match("/.*?{$f}.*?/i", (string) $line['devagent']) || preg_match("/.*?{$f}.*?/i", (string) $line['devid']) || preg_match("/.*?{$f}.*?/i", (string) $line['addinfo']))) {
283
								continue;
284
							}
285
						}
286
287
						$lastUpdate = $this->currenttime - $line["update"];
288
						if ($this->currenttime - $line["update"] < 2) {
289
							$this->linesActive[$line["update"] . $line["pid"]] = $line;
290
						}
291
						elseif (($line['push'] === true && $lastUpdate > ($this->pingInterval + 2)) || ($line['push'] !== true && $lastUpdate > 4)) {
292
							$this->linesUnknown[$line["update"] . $line["pid"]] = $line;
293
						}
294
						else {
295
							$this->linesOpen[$line["update"] . $line["pid"]] = $line;
296
						}
297
					}
298
					else {
299
						// do not show terminated + expired connections
300
						if ($this->currenttime > $line['ended'] + $this->showTermSec) {
301
							continue;
302
						}
303
304
						if ($this->filter !== false) {
305
							$f = $this->filter;
306
							if (
307
								!(
308
									$line['pid'] == $f ||
309
									$line['ip'] == $f ||
310
									strtolower((string) $line['command']) == strtolower($f) ||
311
									preg_match("/.*?{$f}.*?/i", (string) $line['user']) ||
312
									preg_match("/.*?{$f}.*?/i", (string) $line['devagent']) ||
313
									preg_match("/.*?{$f}.*?/i", (string) $line['devid']) ||
314
									preg_match("/.*?{$f}.*?/i", (string) $line['addinfo'])
315
								)) {
316
								continue;
317
							}
318
						}
319
320
						$line['time'] = $line['ended'] - $line['start'];
321
						$this->linesTerm[$line['update'] . $line['pid']] = $line;
322
					}
323
				}
324
			}
325
		}
326
327
		// sort by execution time
328
		krsort($this->linesActive);
329
		krsort($this->linesOpen);
330
		krsort($this->linesUnknown);
331
		krsort($this->linesTerm);
332
	}
333
334
	private function normalizeLine(array $line): array {
335
		$normalized = array_replace(self::LINE_DEFAULTS, $line);
336
337
		$normalized['pid'] = (int) $normalized['pid'];
338
		$normalized['update'] = (int) $normalized['update'];
339
		$normalized['start'] = (int) $normalized['start'];
340
		$normalized['ended'] = (int) $normalized['ended'];
341
		$normalized['time'] = (int) ($normalized['time'] ?? 0);
342
343
		if (!is_bool($normalized['push'])) {
344
			if (is_scalar($normalized['push'])) {
345
				$normalized['push'] = filter_var($normalized['push'], FILTER_VALIDATE_BOOL, FILTER_NULL_ON_FAILURE) ?? false;
346
			}
347
			else {
348
				$normalized['push'] = false;
349
			}
350
		}
351
352
		foreach (['ip', 'user', 'devagent', 'devid', 'addinfo', 'asversion'] as $key) {
353
			$normalized[$key] = (string) $normalized[$key];
354
		}
355
356
		if (is_string($normalized['command']) || is_numeric($normalized['command'])) {
357
			$normalized['command'] = (string) $normalized['command'];
358
		}
359
		else {
360
			$normalized['command'] = '';
361
		}
362
363
		return $normalized;
364
	}
365
366
	/**
367
	 * Prints data to the terminal.
368
	 */
369
	private function scrOverview() {
370
		$linesAvail = $this->scrSize['height'] - 7;
371
		$lc = 1;
372
		echo "\e[?25l\e[1;1H";
373
		$this->scrPrintAt($lc, 0, sprintf(
374
			"grommunio-sync-top live stats (%s, Gromox %s) %s\n",
375
			$this->getVersion(),
376
			phpversion("mapi"),
377
			date("Y-m-d H:i:s")
378
		));
379
		++$lc;
380
381
		$this->scrPrintAt($lc, 0, sprintf(
382
			"Conn: \e[1m%4d\e[0m open, \e[1m%4d\e[0m push; " .
383
			"\e[1m%4d\e[0m users, \e[1m%4d\e[0m devices, \e[1m%4d\e[0m hosts\n",
384
			count($this->activeConn),
385
			$this->pushConn,
386
			count($this->activeUsers),
387
			count($this->activeDevices),
388
			count($this->activeHosts)
389
		));
390
		++$lc;
391
392
		// remove old status
393
		if ($this->statusexpire < $this->currenttime) {
394
			$this->status = false;
395
		}
396
397
		// show request information and help command
398
		if ($this->starttime + 6 > $this->currenttime) {
399
			$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";
400
			$this->statusexpire = $this->currenttime + 1;
401
		}
402
403
		$str = "";
404
		if (!$this->showPush) {
405
			$str .= "\033[00;32mPush: \033[01;32mNo\033[0m   ";
406
		}
407
408
		if ($this->showOption == self::SHOW_ACTIVE_ONLY) {
409
			$str .= "\033[01;32mActive only\033[0m   ";
410
		}
411
412
		if ($this->showOption == self::SHOW_UNKNOWN_ONLY) {
413
			$str .= "\033[01;32mUnknown only\033[0m   ";
414
		}
415
416
		if ($this->showTermSec != self::SHOW_TERM_DEFAULT_TIME) {
417
			$str .= "\033[01;32mTerminated: " . $this->showTermSec . "s\033[0m   ";
418
		}
419
420
		if ($this->filter !== false || ($this->status !== false && $this->statusexpire > $this->currenttime)) {
421
			// print filter in green
422
			if ($this->filter !== false) {
423
				$str .= "\033[00;32mFilter: \033[01;32m{$this->filter}\033[0m   ";
424
			}
425
			// print status in red
426
			if ($this->status !== false) {
427
				$str .= "\033[00;31m{$this->status}\033[0m";
428
			}
429
		}
430
		$this->scrPrintAt($lc, 0, "Action: \033[01m" . $this->action . "\033[0m\n");
431
		++$lc;
432
		$this->scrPrintAt($lc, 0, $str . "\n");
433
		++$lc;
434
		$header = $this->getLine(['pid' => 'PID', 'ip' => 'ADDRESS', 'user' => 'USER', 'command' => 'COMMAND', 'time' => 'TIME', 'devagent' => 'AGENT', 'devid' => 'DEVID', 'addinfo' => 'Additional Information', 'asversion' => 'EAS']);
435
		$this->scrPrintAt($lc, 0, "\033[7m" . $header . str_repeat(" ", $this->scrSize['width'] - ($this->scrSize['width'] > strlen($header) ? strlen($header) : 0)) . "\033[0m\n");
436
		++$lc;
437
438
		if ($linesAvail < 1) {
439
			echo "Terminal too small, no room to display any useful information.";
440
			++$lc;
441
442
			return;
443
		}
444
445
		// print help text if requested
446
		$help = false;
447
		if ($this->helpexpire > $this->currenttime) {
448
			$help = $this->scrHelp();
449
			$linesAvail -= (count($help) + 1);
450
		}
451
452
		$toPrintActive = $linesAvail;
453
		$toPrintOpen = $linesAvail;
454
		$toPrintUnknown = $linesAvail;
455
		$toPrintTerm = $linesAvail;
456
457
		// default view: show all unknown, no terminated and half active+open
458
		if (count($this->linesActive) + count($this->linesOpen) + count($this->linesUnknown) > $linesAvail) {
459
			$toPrintUnknown = count($this->linesUnknown);
460
			$toPrintActive = count($this->linesActive);
461
			$toPrintOpen = $linesAvail - $toPrintUnknown - $toPrintActive;
462
			$toPrintTerm = 0;
463
		}
464
465
		if ($this->showOption == self::SHOW_ACTIVE_ONLY) {
466
			$toPrintActive = $linesAvail;
467
			$toPrintOpen = 0;
468
			$toPrintUnknown = 0;
469
			$toPrintTerm = 0;
470
		}
471
472
		if ($this->showOption == self::SHOW_UNKNOWN_ONLY) {
473
			$toPrintActive = 0;
474
			$toPrintOpen = 0;
475
			$toPrintUnknown = $linesAvail;
476
			$toPrintTerm = 0;
477
		}
478
479
		$linesprinted = 0;
480
		foreach ($this->linesActive as $time => $l) {
481
			if ($linesprinted >= $toPrintActive) {
482
				break;
483
			}
484
485
			$this->scrPrintAt($lc, 0, "\033[01m" . $this->getLine($l) . "\033[0m\n");
486
			++$lc;
487
			++$linesprinted;
488
		}
489
490
		$linesprinted = 0;
491
		foreach ($this->linesOpen as $time => $l) {
492
			if ($linesprinted >= $toPrintOpen) {
493
				break;
494
			}
495
496
			$this->scrPrintAt($lc, 0, $this->getLine($l) . "\n");
497
			++$lc;
498
			++$linesprinted;
499
		}
500
501
		$linesprinted = 0;
502
		foreach ($this->linesUnknown as $time => $l) {
503
			if ($linesprinted >= $toPrintUnknown) {
504
				break;
505
			}
506
			$time = intval($time);
507
			$color = "0;31m";
508
			if (!isset($l['start'])) {
509
				$l['start'] = $time;
510
			}
511
			if ((!isset($l['push']) || $l['push'] == false) && $time - $l["start"] > 30) {
512
				$color = "1;31m";
513
			}
514
			$this->scrPrintAt($lc, 0, "\033[0" . $color . $this->getLine($l) . "\033[0m\n");
515
			++$lc;
516
			++$linesprinted;
517
		}
518
519
		if ($toPrintTerm > 0) {
520
			$toPrintTerm = $linesAvail - $lc + 5;
521
		}
522
523
		$linesprinted = 0;
524
		foreach ($this->linesTerm as $time => $l) {
525
			if ($linesprinted >= $toPrintTerm) {
526
				break;
527
			}
528
529
			$this->scrPrintAt($lc, 0, "\033[01;30m" . $this->getLine($l) . "\033[0m\n");
530
			++$lc;
531
			++$linesprinted;
532
		}
533
534
		// add the lines used when displaying the help text
535
		if ($help !== false) {
536
			while ($lc < $linesAvail + 7) {
537
				$this->scrPrintAt($lc, 0, "\033[K\n");
538
				++$lc;
539
			}
540
			if ($linesAvail < 1) {
541
				$this->scrPrintAt($lc, 0, "Can't display help text, terminal has not enough lines. Use -h or --help to see help information. \n");
542
				++$lc;
543
			}
544
			else {
545
				foreach ($help as $h) {
546
					$this->scrPrintAt($lc, 0, $h . "\n");
547
					++$lc;
548
				}
549
			}
550
		}
551
		$this->scrPrintAt($lc, 0, "\033[K\n");
552
		++$lc;
553
		$this->scrPrintAt($lc, 0, "Colorscheme: \033[01mActive  \033[0mOpen  \033[01;31mUnknown  \033[01;30mTerminated\033[0m");
554
		/* Clear rest of area */
555
		echo "\e[J";
556
		/* Reposition cursor to Action: line */
557
		printf("\e[3;%dH\e[?25h", 9 + strlen($this->action));
558
	}
559
560
	/**
561
	 * Waits for a keystroke and processes the requested command.
562
	 */
563
	private function readLineProcess() {
564
		$ans = explode("^^", (string) `bash -c "read -n 1 -t 1 ANS ; echo \\\$?^^\\\$ANS;"`);
565
566
		if ($ans[0] < 128) {
567
			if (isset($ans[1]) && bin2hex(trim($ans[1])) == "7f") {
568
				$this->action = substr($this->action, 0, -1);
569
			}
570
571
			if (isset($ans[1]) && $ans[1] != "") {
572
				$this->action .= trim((string) preg_replace("/[^A-Za-z0-9:]/", "", $ans[1]));
573
			}
574
575
			if (bin2hex($ans[0]) == "30" && bin2hex($ans[1]) == "0a") {
576
				$cmds = explode(':', $this->action);
577
				if ($cmds[0] == "quit" || $cmds[0] == "q" || (isset($cmds[1]) && $cmds[0] == "" && $cmds[1] == "q")) {
578
					$this->topCollector->CollectData(true);
579
					$this->topCollector->ClearLatest(true);
580
581
					$this->terminate = true;
582
				}
583
				elseif ($cmds[0] == "clear") {
584
					$this->topCollector->ClearLatest(true);
585
					$this->topCollector->CollectData(true);
586
					$this->topCollector->ReInitIPC();
587
				}
588
				elseif ($cmds[0] == "filter" || $cmds[0] == "f") {
589
					if (!isset($cmds[1]) || $cmds[1] == "") {
590
						$this->filter = false;
591
						$this->status = "No filter";
592
						$this->statusexpire = $this->currenttime + 5;
593
					}
594
					else {
595
						$this->filter = $cmds[1];
596
						$this->status = false;
597
					}
598
				}
599
				elseif ($cmds[0] == "option" || $cmds[0] == "o") {
600
					if (!isset($cmds[1]) || $cmds[1] == "") {
601
						$this->status = "Option value needs to be specified. See 'help' or 'h' for instructions";
602
						$this->statusexpire = $this->currenttime + 5;
603
					}
604
					elseif ($cmds[1] == "p" || $cmds[1] == "push" || $cmds[1] == "ping") {
605
						$this->showPush = !$this->showPush;
606
					}
607
					elseif ($cmds[1] == "a" || $cmds[1] == "active") {
608
						$this->showOption = self::SHOW_ACTIVE_ONLY;
609
					}
610
					elseif ($cmds[1] == "u" || $cmds[1] == "unknown") {
611
						$this->showOption = self::SHOW_UNKNOWN_ONLY;
612
					}
613
					elseif ($cmds[1] == "d" || $cmds[1] == "default") {
614
						$this->showOption = self::SHOW_DEFAULT;
615
						$this->showTermSec = self::SHOW_TERM_DEFAULT_TIME;
616
						$this->showPush = true;
617
					}
618
					elseif (is_numeric($cmds[1])) {
619
						$this->showTermSec = $cmds[1];
620
					}
621
					else {
622
						$this->status = sprintf("Option '%s' unknown", $cmds[1]);
623
						$this->statusexpire = $this->currenttime + 5;
624
					}
625
				}
626
				elseif ($cmds[0] == "reset" || $cmds[0] == "r") {
627
					$this->filter = false;
628
					$this->wide = false;
629
					$this->helpexpire = 0;
630
					$this->status = "reset";
631
					$this->statusexpire = $this->currenttime + 2;
632
				}
633
				// enable/disable wide view
634
				elseif ($cmds[0] == "wide" || $cmds[0] == "w") {
635
					$this->wide = !$this->wide;
636
					$this->status = ($this->wide) ? "w i d e  view" : "normal view";
637
					$this->statusexpire = $this->currenttime + 2;
638
				}
639
				elseif ($cmds[0] == "help" || $cmds[0] == "h") {
640
					$this->helpexpire = $this->currenttime + 20;
641
				}
642
				// grep the log file
643
				elseif (($cmds[0] == "log" || $cmds[0] == "l") && isset($cmds[1])) {
644
					if (!file_exists(LOGFILE)) {
645
						$this->status = "Logfile can not be found: " . LOGFILE;
646
					}
647
					else {
648
						system('bash -c "fgrep -a ' . escapeshellarg($cmds[1]) . ' ' . LOGFILE . ' | less +G" > `tty`');
649
						$this->status = "Returning from log, updating data";
650
					}
651
					$this->statusexpire = time() + 5; // it might be much "later" now
652
				}
653
				// tail the log file
654
				elseif ($cmds[0] == "tail" || $cmds[0] == "t") {
655
					if (!file_exists(LOGFILE)) {
656
						$this->status = "Logfile can not be found: " . LOGFILE;
657
					}
658
					else {
659
						$this->doingTail = true;
660
						$this->scrClear();
661
						$this->scrPrintAt(1, 0, $this->scrAsBold("Press CTRL+C to return to grommunio-sync-top\n\n"));
662
						$secondary = "";
663
						if (isset($cmds[1])) {
664
							$secondary = " -n 200 | grep " . escapeshellarg($cmds[1]);
665
						}
666
						system('bash -c "tail -f ' . LOGFILE . $secondary . '" > `tty`');
667
						$this->doingTail = false;
668
						$this->status = "Returning from tail, updating data";
669
					}
670
					$this->statusexpire = time() + 5; // it might be much "later" now
671
				}
672
				// tail the error log file
673
				elseif ($cmds[0] == "error" || $cmds[0] == "e") {
674
					if (!file_exists(LOGERRORFILE)) {
675
						$this->status = "Error logfile can not be found: " . LOGERRORFILE;
676
					}
677
					else {
678
						$this->doingTail = true;
679
						$this->scrClear();
680
						$this->scrPrintAt(1, 0, $this->scrAsBold("Press CTRL+C to return to grommunio-sync-top\n\n"));
681
						$secondary = "";
682
						if (isset($cmds[1])) {
683
							$secondary = " -n 200 | grep " . escapeshellarg($cmds[1]);
684
						}
685
						system('bash -c "tail -f ' . LOGERRORFILE . $secondary . '" > `tty`');
686
						$this->doingTail = false;
687
						$this->status = "Returning from tail, updating data";
688
					}
689
					$this->statusexpire = time() + 5; // it might be much "later" now
690
				}
691
				elseif ($cmds[0] != "") {
692
					$this->status = sprintf("Command '%s' unknown", $cmds[0]);
693
					$this->statusexpire = $this->currenttime + 8;
694
				}
695
				$this->action = "";
696
			}
697
		}
698
	}
699
700
	/**
701
	 * Signal handler function.
702
	 *
703
	 * @param int $signo signal number
704
	 */
705
	public function SignalHandler($signo) {
0 ignored issues
show
Unused Code introduced by
The parameter $signo is not used and could be removed. ( Ignorable by Annotation )

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

705
	public function SignalHandler(/** @scrutinizer ignore-unused */ $signo) {

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
706
		// don't terminate if the signal was sent by terminating tail
707
		if (!$this->doingTail) {
708
			$this->topCollector->CollectData(true);
709
			$this->topCollector->ClearLatest(true);
710
			$this->terminate = true;
711
		}
712
	}
713
714
	/**
715
	 * Returns usage instructions.
716
	 *
717
	 * @return string
718
	 */
719
	public function UsageInstructions() {
720
		$help = "Usage:\n\tgrommunio-sync-top\n\n" .
721
				"  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" .
722
				"  When grommunio-sync-top is running you can specify certain actions and options which can be executed (listed below).\n" .
723
				"  This help information can also be shown inside grommunio-sync-top by hitting 'help' or 'h'.\n\n";
724
		$scrhelp = $this->scrHelp();
725
		unset($scrhelp[0]);
726
727
		$help .= implode("\n", $scrhelp);
728
		$help .= "\n\n";
729
730
		return $help;
731
	}
732
733
	/**
734
	 * Prints a 'help' text at the end of the page.
735
	 *
736
	 * @return array with help lines
737
	 */
738
	private function scrHelp() {
739
		$h = [];
740
		$secs = $this->helpexpire - $this->currenttime;
741
		$h[] = "Actions supported by grommunio-sync-top (help page still displayed for " . $secs . "secs)";
742
		$h[] = "  " . $this->scrAsBold("Action") . "                " . $this->scrAsBold("Comment");
743
		$h[] = "  " . $this->scrAsBold("h") . " or " . $this->scrAsBold("help") . "             Displays this information.";
744
		$h[] = "  " . $this->scrAsBold("q") . ", " . $this->scrAsBold("quit") . " or " . $this->scrAsBold(":q") . "         Exits grommunio-sync-top.";
745
		$h[] = "  " . $this->scrAsBold("w") . " or " . $this->scrAsBold("wide") . "             Tries not to truncate data. Automatically done if more than 180 columns available.";
746
		$h[] = "  " . $this->scrAsBold("f:VAL") . " or " . $this->scrAsBold("filter:VAL") . "   Only display connections which contain VAL. This value is case-insensitive.";
747
		$h[] = "  " . $this->scrAsBold("f:") . " or " . $this->scrAsBold("filter:") . "         Without a search word: resets the filter.";
748
		$h[] = "  " . $this->scrAsBold("l:STR") . " or " . $this->scrAsBold("log:STR") . "      Issues 'less +G' on the logfile, after grepping on the optional STR.";
749
		$h[] = "  " . $this->scrAsBold("t:STR") . " or " . $this->scrAsBold("tail:STR") . "     Issues 'tail -f' on the logfile, grepping for optional STR.";
750
		$h[] = "  " . $this->scrAsBold("e:STR") . " or " . $this->scrAsBold("error:STR") . "    Issues 'tail -f' on the error logfile, grepping for optional STR.";
751
		$h[] = "  " . $this->scrAsBold("r") . " or " . $this->scrAsBold("reset") . "            Resets 'wide' or 'filter'.";
752
		$h[] = "  " . $this->scrAsBold("o:") . " or " . $this->scrAsBold("option:") . "         Sets display options. Valid options specified below";
753
		$h[] = "  " . $this->scrAsBold("  p") . " or " . $this->scrAsBold("push") . "           Lists/not lists active and open push connections.";
754
		$h[] = "  " . $this->scrAsBold("  a") . " or " . $this->scrAsBold("action") . "         Lists only active connections.";
755
		$h[] = "  " . $this->scrAsBold("  u") . " or " . $this->scrAsBold("unknown") . "        Lists only unknown connections.";
756
		$h[] = "  " . $this->scrAsBold("  10") . " or " . $this->scrAsBold("20") . "            Lists terminated connections for 10 or 20 seconds. Any other number can be used.";
757
		$h[] = "  " . $this->scrAsBold("  d") . " or " . $this->scrAsBold("default") . "        Uses default options";
758
759
		return $h;
760
	}
761
762
	/**
763
	 * Encapsulates string with different color escape characters.
764
	 *
765
	 * @param string $text
766
	 *
767
	 * @return string same text as bold
768
	 */
769
	private function scrAsBold($text) {
770
		return "\033[01m" . $text . "\033[0m";
771
	}
772
773
	/**
774
	 * Prints one line of precessed data.
775
	 *
776
	 * @param array $l line information
777
	 *
778
	 * @return string
779
	 */
780
	private function getLine($l) {
781
		if (!isset($l['pid']) || !isset($l['ip']) || !isset($l['user']) || !isset($l['command']) || !isset($l['time']) || !isset($l['devagent']) || !isset($l['devid']) || !isset($l['addinfo']) || !isset($l['asversion'])) {
782
			return "";
783
		}
784
		if ($this->wide === true) {
785
			return sprintf("%s%s%s%s%s%s%s%s%s", $this->ptStr($l['pid'], 7), $this->ptStr($l['ip'], 40), $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['asversion'], 5), $this->ptStr($l['devid'], 33, true), $l['addinfo']);
786
		}
787
788
		return sprintf("%s%s%s%s%s%s%s%s%s", $this->ptStr($l['pid'], 7), $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['asversion'], 5), $this->ptStr($l['devid'], 12, true), $l['addinfo']);
789
	}
790
791
	/**
792
	 * Pads and trims string.
793
	 *
794
	 * @param string $str       to be trimmed/padded
795
	 * @param int    $size      characters to be considered
796
	 * @param bool   $cutmiddle (optional) indicates where to long information should
797
	 *                          be trimmed of, false means at the end
798
	 *
799
	 * @return string
800
	 */
801
	private function ptStr($str, $size, $cutmiddle = false) {
802
		if (strlen($str) < $size) {
803
			return str_pad($str, $size);
804
		}
805
		if ($cutmiddle === true) {
806
			$cut = ($size - 2) / 2;
807
808
			return $this->ptStr(substr($str, 0, $cut) . ".." . substr($str, (-1) * ($cut - 1)), $size);
809
		}
810
811
		return substr($str, 0, $size - 3) . ".. ";
812
	}
813
814
	/**
815
	 * Tries to discover the size of the current terminal.
816
	 *
817
	 * @return array 'width' and 'height' as keys
818
	 */
819
	private function scrGetSize() {
820
		$tty = strtolower(exec('stty -a | fgrep columns'));
821
		if (preg_match_all("/rows.([0-9]+);.columns.([0-9]+);/", $tty, $output) ||
822
			preg_match_all("/([0-9]+).rows;.([0-9]+).columns;/", $tty, $output)) {
823
			return ['width' => $output[2][0], 'height' => $output[1][0]];
824
		}
825
826
		return ['width' => 80, 'height' => 24];
827
	}
828
829
	/**
830
	 * Returns the version of the current grommunio-sync installation.
831
	 *
832
	 * @return string
833
	 */
834
	private function getVersion() {
835
		return GROMMUNIOSYNC_VERSION;
836
	}
837
838
	/**
839
	 * Converts seconds in MM:SS.
840
	 *
841
	 * @param int $s seconds
842
	 *
843
	 * @return string
844
	 */
845
	private function sec2min($s) {
846
		if (!is_int($s)) {
0 ignored issues
show
introduced by
The condition is_int($s) is always true.
Loading history...
847
			return $s;
848
		}
849
850
		return sprintf("%02.2d:%02.2d", floor($s / 60), $s % 60);
851
	}
852
853
	/**
854
	 * Resets the default colors of the terminal.
855
	 */
856
	private function scrDefaultColors() {
857
		echo "\033[0m";
858
	}
859
860
	/**
861
	 * Clears screen of the terminal.
862
	 */
863
	public function scrClear() {
864
		echo "\033[2J";
865
	}
866
867
	/**
868
	 * Prints a text at a specific screen/terminal coordinates.
869
	 *
870
	 * @param int    $row  row number
871
	 * @param int    $col  column number
872
	 * @param string $text to be printed
873
	 */
874
	private function scrPrintAt($row, $col, $text = "") {
875
		echo "\033[" . $row . ";" . $col . "H" . preg_replace("/\n/", "\e[K\n", $text);
876
	}
877
}
878