Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
freebsd
GitHub Repository: freebsd/phabricator
Path: blob/master/src/applications/aphlict/management/PhabricatorAphlictManagementWorkflow.php
12256 views
1
<?php
2
3
abstract class PhabricatorAphlictManagementWorkflow
4
extends PhabricatorManagementWorkflow {
5
6
private $debug = false;
7
private $configData;
8
private $configPath;
9
10
final protected function setDebug($debug) {
11
$this->debug = $debug;
12
return $this;
13
}
14
15
protected function getLaunchArguments() {
16
return array(
17
array(
18
'name' => 'config',
19
'param' => 'file',
20
'help' => pht(
21
'Use a specific configuration file instead of the default '.
22
'configuration.'),
23
),
24
);
25
}
26
27
protected function parseLaunchArguments(PhutilArgumentParser $args) {
28
$config_file = $args->getArg('config');
29
if ($config_file) {
30
$full_path = Filesystem::resolvePath($config_file);
31
$show_path = $full_path;
32
} else {
33
$root = dirname(dirname(phutil_get_library_root('phabricator')));
34
35
$try = array(
36
'phabricator/conf/aphlict/aphlict.custom.json',
37
'phabricator/conf/aphlict/aphlict.default.json',
38
);
39
40
foreach ($try as $config) {
41
$full_path = $root.'/'.$config;
42
$show_path = $config;
43
if (Filesystem::pathExists($full_path)) {
44
break;
45
}
46
}
47
}
48
49
echo tsprintf(
50
"%s\n",
51
pht(
52
'Reading configuration from: %s',
53
$show_path));
54
55
try {
56
$data = Filesystem::readFile($full_path);
57
} catch (Exception $ex) {
58
throw new PhutilArgumentUsageException(
59
pht(
60
'Failed to read configuration file. %s',
61
$ex->getMessage()));
62
}
63
64
try {
65
$data = phutil_json_decode($data);
66
} catch (Exception $ex) {
67
throw new PhutilArgumentUsageException(
68
pht(
69
'Configuration file is not properly formatted JSON. %s',
70
$ex->getMessage()));
71
}
72
73
try {
74
PhutilTypeSpec::checkMap(
75
$data,
76
array(
77
'servers' => 'list<wild>',
78
'logs' => 'optional list<wild>',
79
'cluster' => 'optional list<wild>',
80
'pidfile' => 'string',
81
'memory.hint' => 'optional int',
82
));
83
} catch (Exception $ex) {
84
throw new PhutilArgumentUsageException(
85
pht(
86
'Configuration file has improper configuration keys at top '.
87
'level. %s',
88
$ex->getMessage()));
89
}
90
91
$servers = $data['servers'];
92
$has_client = false;
93
$has_admin = false;
94
$port_map = array();
95
foreach ($servers as $index => $server) {
96
PhutilTypeSpec::checkMap(
97
$server,
98
array(
99
'type' => 'string',
100
'port' => 'int',
101
'listen' => 'optional string|null',
102
'ssl.key' => 'optional string|null',
103
'ssl.cert' => 'optional string|null',
104
'ssl.chain' => 'optional string|null',
105
));
106
107
$port = $server['port'];
108
if (!isset($port_map[$port])) {
109
$port_map[$port] = $index;
110
} else {
111
throw new PhutilArgumentUsageException(
112
pht(
113
'Two servers (at indexes "%s" and "%s") both bind to the same '.
114
'port ("%s"). Each server must bind to a unique port.',
115
$port_map[$port],
116
$index,
117
$port));
118
}
119
120
$type = $server['type'];
121
switch ($type) {
122
case 'admin':
123
$has_admin = true;
124
break;
125
case 'client':
126
$has_client = true;
127
break;
128
default:
129
throw new PhutilArgumentUsageException(
130
pht(
131
'A specified server (at index "%s", on port "%s") has an '.
132
'invalid type ("%s"). Valid types are: admin, client.',
133
$index,
134
$port,
135
$type));
136
}
137
138
$ssl_key = idx($server, 'ssl.key');
139
$ssl_cert = idx($server, 'ssl.cert');
140
if (($ssl_key && !$ssl_cert) || ($ssl_cert && !$ssl_key)) {
141
throw new PhutilArgumentUsageException(
142
pht(
143
'A specified server (at index "%s", on port "%s") specifies '.
144
'only one of "%s" and "%s". Each server must specify neither '.
145
'(to disable SSL) or specify both (to enable it).',
146
$index,
147
$port,
148
'ssl.key',
149
'ssl.cert'));
150
}
151
152
$ssl_chain = idx($server, 'ssl.chain');
153
if ($ssl_chain && (!$ssl_key && !$ssl_cert)) {
154
throw new PhutilArgumentUsageException(
155
pht(
156
'A specified server (at index "%s", on port "%s") specifies '.
157
'a value for "%s", but no value for "%s" or "%s". Servers '.
158
'should only provide an SSL chain if they also provide an SSL '.
159
'key and SSL certificate.',
160
$index,
161
$port,
162
'ssl.chain',
163
'ssl.key',
164
'ssl.cert'));
165
}
166
}
167
168
if (!$servers) {
169
throw new PhutilArgumentUsageException(
170
pht(
171
'Configuration file does not specify any servers. This service '.
172
'will not be able to interact with the outside world if it does '.
173
'not listen on any ports. You must specify at least one "%s" '.
174
'server and at least one "%s" server.',
175
'admin',
176
'client'));
177
}
178
179
if (!$has_client) {
180
throw new PhutilArgumentUsageException(
181
pht(
182
'Configuration file does not specify any client servers. This '.
183
'service will be unable to transmit any notifications without a '.
184
'client server. You must specify at least one server with '.
185
'type "%s".',
186
'client'));
187
}
188
189
if (!$has_admin) {
190
throw new PhutilArgumentUsageException(
191
pht(
192
'Configuration file does not specify any administrative '.
193
'servers. This service will be unable to receive messages. '.
194
'You must specify at least one server with type "%s".',
195
'admin'));
196
}
197
198
$logs = idx($data, 'logs', array());
199
foreach ($logs as $index => $log) {
200
PhutilTypeSpec::checkMap(
201
$log,
202
array(
203
'path' => 'string',
204
));
205
206
$path = $log['path'];
207
208
try {
209
$dir = dirname($path);
210
if (!Filesystem::pathExists($dir)) {
211
Filesystem::createDirectory($dir, 0755, true);
212
}
213
} catch (FilesystemException $ex) {
214
throw new PhutilArgumentUsageException(
215
pht(
216
'Failed to create directory "%s" for specified log file (with '.
217
'index "%s"). You should manually create this directory or '.
218
'choose a different logfile location. %s',
219
$dir,
220
$index,
221
$ex->getMessage()));
222
}
223
}
224
225
$peer_map = array();
226
227
$cluster = idx($data, 'cluster', array());
228
foreach ($cluster as $index => $peer) {
229
PhutilTypeSpec::checkMap(
230
$peer,
231
array(
232
'host' => 'string',
233
'port' => 'int',
234
'protocol' => 'string',
235
));
236
237
$host = $peer['host'];
238
$port = $peer['port'];
239
$protocol = $peer['protocol'];
240
241
switch ($protocol) {
242
case 'http':
243
case 'https':
244
break;
245
default:
246
throw new PhutilArgumentUsageException(
247
pht(
248
'Configuration file specifies cluster peer ("%s", at index '.
249
'"%s") with an invalid protocol, "%s". Valid protocols are '.
250
'"%s" or "%s".',
251
$host,
252
$index,
253
$protocol,
254
'http',
255
'https'));
256
}
257
258
$peer_key = "{$host}:{$port}";
259
if (!isset($peer_map[$peer_key])) {
260
$peer_map[$peer_key] = $index;
261
} else {
262
throw new PhutilArgumentUsageException(
263
pht(
264
'Configuration file specifies cluster peer "%s" more than '.
265
'once (at indexes "%s" and "%s"). Each peer must have a '.
266
'unique host and port combination.',
267
$peer_key,
268
$peer_map[$peer_key],
269
$index));
270
}
271
}
272
273
$this->configData = $data;
274
$this->configPath = $full_path;
275
276
$pid_path = $this->getPIDPath();
277
try {
278
$dir = dirname($pid_path);
279
if (!Filesystem::pathExists($dir)) {
280
Filesystem::createDirectory($dir, 0755, true);
281
}
282
} catch (FilesystemException $ex) {
283
throw new PhutilArgumentUsageException(
284
pht(
285
'Failed to create directory "%s" for specified PID file. You '.
286
'should manually create this directory or choose a different '.
287
'PID file location. %s',
288
$dir,
289
$ex->getMessage()));
290
}
291
}
292
293
final public function getPIDPath() {
294
return $this->configData['pidfile'];
295
}
296
297
final public function getPID() {
298
$pid = null;
299
if (Filesystem::pathExists($this->getPIDPath())) {
300
$pid = (int)Filesystem::readFile($this->getPIDPath());
301
}
302
return $pid;
303
}
304
305
final public function cleanup($signo = null) {
306
global $g_future;
307
if ($g_future) {
308
$g_future->resolveKill();
309
$g_future = null;
310
}
311
312
Filesystem::remove($this->getPIDPath());
313
314
if ($signo !== null) {
315
$signame = phutil_get_signal_name($signo);
316
error_log("Caught signal {$signame}, exiting.");
317
}
318
319
exit(1);
320
}
321
322
public static function requireExtensions() {
323
self::mustHaveExtension('pcntl');
324
self::mustHaveExtension('posix');
325
}
326
327
private static function mustHaveExtension($ext) {
328
if (!extension_loaded($ext)) {
329
echo pht(
330
"ERROR: The PHP extension '%s' is not installed. You must ".
331
"install it to run Aphlict on this machine.",
332
$ext)."\n";
333
exit(1);
334
}
335
336
$extension = new ReflectionExtension($ext);
337
foreach ($extension->getFunctions() as $function) {
338
$function = $function->name;
339
if (!function_exists($function)) {
340
echo pht(
341
'ERROR: The PHP function %s is disabled. You must '.
342
'enable it to run Aphlict on this machine.',
343
$function.'()')."\n";
344
exit(1);
345
}
346
}
347
}
348
349
final protected function willLaunch() {
350
$console = PhutilConsole::getConsole();
351
352
$pid = $this->getPID();
353
if ($pid) {
354
throw new PhutilArgumentUsageException(
355
pht(
356
'Unable to start notifications server because it is already '.
357
'running. Use `%s` to restart it.',
358
'aphlict restart'));
359
}
360
361
if (posix_getuid() == 0) {
362
throw new PhutilArgumentUsageException(
363
pht('The notification server should not be run as root.'));
364
}
365
366
// Make sure we can write to the PID file.
367
if (!$this->debug) {
368
Filesystem::writeFile($this->getPIDPath(), '');
369
}
370
371
// First, start the server in configuration test mode with --test. This
372
// will let us error explicitly if there are missing modules, before we
373
// fork and lose access to the console.
374
$test_argv = $this->getServerArgv();
375
$test_argv[] = '--test=true';
376
377
378
execx('%C', $this->getStartCommand($test_argv));
379
}
380
381
private function getServerArgv() {
382
$server_argv = array();
383
$server_argv[] = '--config='.$this->configPath;
384
return $server_argv;
385
}
386
387
final protected function launch() {
388
$console = PhutilConsole::getConsole();
389
390
if ($this->debug) {
391
$console->writeOut(
392
"%s\n",
393
pht('Starting Aphlict server in foreground...'));
394
} else {
395
Filesystem::writeFile($this->getPIDPath(), getmypid());
396
}
397
398
$command = $this->getStartCommand($this->getServerArgv());
399
400
if (!$this->debug) {
401
declare(ticks = 1);
402
pcntl_signal(SIGINT, array($this, 'cleanup'));
403
pcntl_signal(SIGTERM, array($this, 'cleanup'));
404
}
405
register_shutdown_function(array($this, 'cleanup'));
406
407
if ($this->debug) {
408
$console->writeOut(
409
"%s\n\n $ %s\n\n",
410
pht('Launching server:'),
411
$command);
412
413
$err = phutil_passthru('%C', $command);
414
$console->writeOut(">>> %s\n", pht('Server exited!'));
415
exit($err);
416
} else {
417
while (true) {
418
global $g_future;
419
$g_future = new ExecFuture('exec %C', $command);
420
421
// Discard all output the subprocess produces: it writes to the log on
422
// disk, so we don't need to send the output anywhere and can just
423
// throw it away.
424
$g_future
425
->setStdoutSizeLimit(0)
426
->setStderrSizeLimit(0);
427
428
$g_future->resolve();
429
430
// If the server exited, wait a couple of seconds and restart it.
431
unset($g_future);
432
sleep(2);
433
}
434
}
435
}
436
437
438
/* -( Commands )----------------------------------------------------------- */
439
440
441
final protected function executeStartCommand() {
442
$console = PhutilConsole::getConsole();
443
$this->willLaunch();
444
445
$log = $this->getOverseerLogPath();
446
if ($log !== null) {
447
echo tsprintf(
448
"%s\n",
449
pht(
450
'Writing logs to: %s',
451
$log));
452
}
453
454
$pid = pcntl_fork();
455
if ($pid < 0) {
456
throw new Exception(
457
pht(
458
'Failed to %s!',
459
'fork()'));
460
} else if ($pid) {
461
$console->writeErr("%s\n", pht('Aphlict Server started.'));
462
exit(0);
463
}
464
465
// Redirect process errors to the error log. If we do not do this, any
466
// error the `aphlict` process itself encounters vanishes into thin air.
467
if ($log !== null) {
468
ini_set('error_log', $log);
469
}
470
471
// When we fork, the child process will inherit its parent's set of open
472
// file descriptors. If the parent process of bin/aphlict is waiting for
473
// bin/aphlict's file descriptors to close, it will be stuck waiting on
474
// the daemonized process. (This happens if e.g. bin/aphlict is started
475
// in another script using passthru().)
476
fclose(STDOUT);
477
fclose(STDERR);
478
479
$this->launch();
480
return 0;
481
}
482
483
484
final protected function executeStopCommand() {
485
$console = PhutilConsole::getConsole();
486
487
$pid = $this->getPID();
488
if (!$pid) {
489
$console->writeErr("%s\n", pht('Aphlict is not running.'));
490
return 0;
491
}
492
493
$console->writeErr("%s\n", pht('Stopping Aphlict Server (%s)...', $pid));
494
posix_kill($pid, SIGINT);
495
496
$start = time();
497
do {
498
if (!PhabricatorDaemonReference::isProcessRunning($pid)) {
499
$console->writeOut(
500
"%s\n",
501
pht('Aphlict Server (%s) exited normally.', $pid));
502
$pid = null;
503
break;
504
}
505
usleep(100000);
506
} while (time() < $start + 5);
507
508
if ($pid) {
509
$console->writeErr("%s\n", pht('Sending %s a SIGKILL.', $pid));
510
posix_kill($pid, SIGKILL);
511
unset($pid);
512
}
513
514
Filesystem::remove($this->getPIDPath());
515
return 0;
516
}
517
518
private function getNodeBinary() {
519
if (Filesystem::binaryExists('nodejs')) {
520
return 'nodejs';
521
}
522
523
if (Filesystem::binaryExists('node')) {
524
return 'node';
525
}
526
527
throw new PhutilArgumentUsageException(
528
pht(
529
'No `%s` or `%s` binary was found in %s. You must install '.
530
'Node.js to start the Aphlict server.',
531
'nodejs',
532
'node',
533
'$PATH'));
534
}
535
536
private function getAphlictScriptPath() {
537
$root = dirname(phutil_get_library_root('phabricator'));
538
return $root.'/support/aphlict/server/aphlict_server.js';
539
}
540
541
private function getNodeArgv() {
542
$argv = array();
543
544
$hint = idx($this->configData, 'memory.hint');
545
$hint = nonempty($hint, 256);
546
547
$argv[] = sprintf('--max-old-space-size=%d', $hint);
548
549
return $argv;
550
}
551
552
private function getStartCommand(array $server_argv) {
553
$launch_argv = array();
554
555
if ($this->debug) {
556
$launch_argv[] = '--debug=1';
557
}
558
559
return csprintf(
560
'%R %Ls -- %s %Ls %Ls',
561
$this->getNodeBinary(),
562
$this->getNodeArgv(),
563
$this->getAphlictScriptPath(),
564
$launch_argv,
565
$server_argv);
566
}
567
568
private function getOverseerLogPath() {
569
// For now, just return the first log. We could refine this eventually.
570
$logs = idx($this->configData, 'logs', array());
571
572
foreach ($logs as $log) {
573
return $log['path'];
574
}
575
576
return null;
577
}
578
579
}
580
581