1: | <?php |
2: | declare(strict_types=1); |
3: | |
4: | |
5: | |
6: | |
7: | |
8: | |
9: | |
10: | |
11: | |
12: | |
13: | |
14: | |
15: | use Frontend\Css\StyleManager; |
16: | use Lararia\Bootstrapper; |
17: | use Lararia\JobDaemon; |
18: | use Laravel\Horizon\Contracts\JobRepository; |
19: | use Module\Skeleton\Contracts\Tasking; |
20: | use Opcenter\Apnscp; |
21: | use Opcenter\Map; |
22: | use Opcenter\Process; |
23: | use Opcenter\Service\ServiceLayer; |
24: | use Opcenter\System\Cgroup\Attributes\Freezer\State; |
25: | use Opcenter\System\Cgroup\Controller; |
26: | use Opcenter\System\Cgroup\Group; |
27: | use Opcenter\System\Memory; |
28: | |
29: | |
30: | |
31: | |
32: | |
33: | |
34: | class Misc_Module extends Module_Skeleton implements Tasking |
35: | { |
36: | const MOUNTRC = '/etc/init.d/vmount'; |
37: | const MEMTEST_KEY = '_misc_cron_memory_test'; |
38: | const MOUNTABLE_SERVICES = [ |
39: | 'procfs', 'fcgi' |
40: | ]; |
41: | protected $exportedFunctions = |
42: | [ |
43: | '*' => PRIVILEGE_SITE, |
44: | 'run_cron' => PRIVILEGE_ADMIN, |
45: | 'get_job_queue' => PRIVILEGE_ADMIN|PRIVILEGE_SITE, |
46: | 'jobify' => PRIVILEGE_ADMIN, |
47: | 'flush_cp_version' => PRIVILEGE_ADMIN, |
48: | 'cp_version' => PRIVILEGE_ALL, |
49: | 'platform_version' => PRIVILEGE_ALL, |
50: | 'dashboard_memory_usage' => PRIVILEGE_ALL, |
51: | 'lservice_memory_usage' => PRIVILEGE_ALL, |
52: | 'changelog' => PRIVILEGE_ALL, |
53: | 'run' => PRIVILEGE_SITE, |
54: | 'notify_installed' => PRIVILEGE_ADMIN, |
55: | 'notify_update_failure' => PRIVILEGE_ADMIN, |
56: | 'list_commands' => PRIVILEGE_ALL, |
57: | 'command_info' => PRIVILEGE_ALL, |
58: | 'debug_session' => PRIVILEGE_ADMIN, |
59: | 'release_fsghost' => PRIVILEGE_ADMIN, |
60: | 'theme_inventory' => PRIVILEGE_ADMIN, |
61: | |
62: | 'i' => PRIVILEGE_ALL, |
63: | 'l' => PRIVILEGE_ALL, |
64: | ]; |
65: | |
66: | |
67: | |
68: | |
69: | |
70: | |
71: | |
72: | public function cp_version(string $field = '') |
73: | { |
74: | return \Opcenter::versionData($field) + ['debug' => is_debug()]; |
75: | } |
76: | |
77: | |
78: | |
79: | |
80: | |
81: | |
82: | public function flush_cp_version(): bool |
83: | { |
84: | return Opcenter::forgetVersion(); |
85: | } |
86: | |
87: | |
88: | |
89: | |
90: | |
91: | |
92: | public function platform_version(): string |
93: | { |
94: | return platform_version(); |
95: | } |
96: | |
97: | |
98: | |
99: | |
100: | |
101: | |
102: | |
103: | public function dashboard_memory_usage(): int |
104: | { |
105: | return memory_get_usage(); |
106: | } |
107: | |
108: | |
109: | |
110: | |
111: | |
112: | |
113: | public function apnscpd_memory_usage(): int |
114: | { |
115: | if (!IS_CLI) { |
116: | return $this->query('misc_apnscpd_memory_usage'); |
117: | } |
118: | |
119: | return memory_get_usage(); |
120: | } |
121: | |
122: | |
123: | |
124: | |
125: | |
126: | |
127: | public function toggle_procfs(): bool |
128: | { |
129: | if (!$this->getServiceValue('ssh', 'enabled')) { |
130: | return error('procfs requires ssh'); |
131: | } |
132: | if ($this->is_mounted('procfs')) { |
133: | return $this->unmount_service('procfs'); |
134: | } |
135: | |
136: | return $this->mount_service('procfs'); |
137: | } |
138: | |
139: | |
140: | |
141: | |
142: | |
143: | |
144: | |
145: | public function is_mounted(string $svc): bool |
146: | { |
147: | if (!\in_array($svc, static::MOUNTABLE_SERVICES, true)) { |
148: | return error("Unknown service `%s'", $svc); |
149: | } |
150: | |
151: | if (version_compare(platform_version(), '6', '>=')) { |
152: | |
153: | return true; |
154: | } |
155: | $proc = Util_Process::exec('%s mounted %s %s', |
156: | self::MOUNTRC, |
157: | $this->site, |
158: | $svc, |
159: | array(0, 1) |
160: | ); |
161: | |
162: | return $proc['return'] === 0; |
163: | } |
164: | |
165: | |
166: | |
167: | |
168: | |
169: | |
170: | |
171: | public function unmount_service(string $svc): bool |
172: | { |
173: | if (!\in_array($svc, static::MOUNTABLE_SERVICES, true)) { |
174: | return error("Unknown service `%s'", $svc); |
175: | } |
176: | |
177: | if ($svc == 'procfs' && version_compare(platform_version(), '6', '>=')) { |
178: | return true; |
179: | } |
180: | |
181: | if (!IS_CLI) { |
182: | return $this->query('misc_unmount_service', $svc); |
183: | } |
184: | $proc = Util_Process::exec( |
185: | '%s unmount %s %s', |
186: | self::MOUNTRC, |
187: | $this->site, |
188: | $svc |
189: | ); |
190: | if ($proc['errno'] != 0) { |
191: | return false; |
192: | } |
193: | |
194: | return $this->_edit_mount_map($svc, false) !== 0; |
195: | } |
196: | |
197: | |
198: | |
199: | |
200: | |
201: | |
202: | |
203: | |
204: | private function _edit_mount_map(string $svc, bool $mount): int |
205: | { |
206: | $sysconf = '/etc/sysconfig/vmount-' . $svc; |
207: | touch($sysconf); |
208: | $sites = explode("\n", trim(file_get_contents($sysconf))); |
209: | $idx = array_search($this->site, $sites, true); |
210: | if ($mount && $idx === false) { |
211: | $sites[] = $this->site; |
212: | } else if (!$mount && $idx !== false) { |
213: | unset($sites[$idx]); |
214: | } else { |
215: | return -1; |
216: | } |
217: | file_put_contents($sysconf, join("\n", $sites)); |
218: | |
219: | return 1; |
220: | } |
221: | |
222: | |
223: | |
224: | |
225: | |
226: | |
227: | |
228: | public function mount_service($svc): bool |
229: | { |
230: | if (!\in_array($svc, static::MOUNTABLE_SERVICES, true)) { |
231: | return error("Unknown service `%s'", $svc); |
232: | } |
233: | |
234: | if ($svc == 'fcgi' && version_compare(platform_version(), '4.5', '>=')) { |
235: | return true; |
236: | } |
237: | if ($svc == 'procfs' && version_compare(platform_version(), '6', '>=')) { |
238: | return true; |
239: | } |
240: | if (!IS_CLI) { |
241: | return $this->query('misc_mount_service', $svc); |
242: | } |
243: | $proc = Util_Process::exec( |
244: | '%s mount %s %s', |
245: | self::MOUNTRC, |
246: | $this->site, |
247: | $svc |
248: | ); |
249: | if ($proc['return'] !== 0) { |
250: | return false; |
251: | } |
252: | |
253: | return $this->_edit_mount_map($svc, true) !== 0; |
254: | } |
255: | |
256: | |
257: | |
258: | |
259: | |
260: | |
261: | public function procfs_enabled(): bool |
262: | { |
263: | return $this->is_mounted('procfs'); |
264: | } |
265: | |
266: | |
267: | |
268: | |
269: | |
270: | |
271: | public function changelog(): array |
272: | { |
273: | $cache = \Cache_Global::spawn(); |
274: | $key = 'misc.changelog'; |
275: | $changelog = $cache->get($key); |
276: | if ($changelog) { |
277: | return $changelog; |
278: | } |
279: | |
280: | $proc = Util_Process::exec('cd ' . INCLUDE_PATH . ' && git log --submodule -n 15 '); |
281: | if (!$proc['success']) { |
282: | return []; |
283: | } |
284: | $res = []; |
285: | preg_match_all(Regex::CHANGELOG_COMMIT, $proc['output'], $matches, PREG_SET_ORDER); |
286: | foreach ($matches as $match) { |
287: | foreach (array_keys($match) as $key) { |
288: | if (is_numeric($key)) { |
289: | unset($match[$key]); |
290: | } else if ($key === 'msg') { |
291: | $match[$key] = trim($match[$key]); |
292: | } else if ($key === 'date') { |
293: | |
294: | $match['ts'] = strtotime($match[$key]); |
295: | unset($match[$key]); |
296: | } |
297: | } |
298: | $res[] = $match; |
299: | } |
300: | $cache->set($key, $res); |
301: | |
302: | return $res; |
303: | } |
304: | |
305: | |
306: | |
307: | |
308: | |
309: | |
310: | |
311: | public function notify_installed(string $password): bool |
312: | { |
313: | if (!($email = $this->admin_get_email())) { |
314: | return error('Cannot send notification email - no email defined! See docs/INSTALL.md'); |
315: | } |
316: | $ip = \Opcenter\Net\Ip4::my_ip(); |
317: | $link = "https://" . $ip . ":" . Auth_Redirect::CP_SSL_PORT; |
318: | |
319: | if ($ip === Net_Gethost::gethostbyname_t(SERVER_NAME, 1500) && $this->common_get_email()) { |
320: | $link = "https://" . SERVER_NAME . ":" . Auth_Redirect::CP_SSL_PORT; |
321: | } |
322: | |
323: | $mail = Illuminate\Support\Facades\Mail::to($email); |
324: | $args = [ |
325: | 'secure_link' => $link, |
326: | 'hostname' => SERVER_NAME, |
327: | 'admin_user' => $this->username, |
328: | 'admin_password' => $password, |
329: | 'apnscp_root' => INCLUDE_PATH, |
330: | 'ip' => \Opcenter\Net\Ip4::my_ip() |
331: | ]; |
332: | $mail->send(new \Lararia\Mail\PanelInstalled($args)); |
333: | |
334: | return true; |
335: | } |
336: | |
337: | |
338: | |
339: | |
340: | |
341: | |
342: | public function notify_update_failure(): bool |
343: | { |
344: | |
345: | |
346: | if (!($email = $this->admin_get_email())) { |
347: | return error('Cannot send notification email - no email defined! See docs/INSTALL.md'); |
348: | } |
349: | |
350: | if (!file_exists($path = storage_path('.upcp.failure'))) { |
351: | return true; |
352: | } |
353: | |
354: | $subject = \ArgumentFormatter::format('%s Update Failure', [PANEL_BRAND]); |
355: | $mail = Illuminate\Support\Facades\Mail::to($email); |
356: | $msg = (new \Lararia\Mail\Simple('email.admin.update-failed')) |
357: | ->asMarkdown()->subject($subject)->attach($path, ['as' => 'update-log.txt', 'mime' => 'text/plain']); |
358: | $mail->send($msg); |
359: | unlink($path); |
360: | return true; |
361: | } |
362: | |
363: | |
364: | |
365: | |
366: | |
367: | |
368: | |
369: | public function list_commands(string $filter = ''): array |
370: | { |
371: | $fns = []; |
372: | $modules = \apnscpFunctionInterceptor::list_all_modules(); |
373: | asort($modules); |
374: | foreach ($modules as $module) { |
375: | $moduleFns = $this->getApnscpFunctionInterceptor()->authorized_functions($module); |
376: | asort($moduleFns); |
377: | if ($filter) { |
378: | $moduleFns = array_filter($moduleFns, static function ($fn) use ($filter, $module) { |
379: | return $filter === $module || fnmatch($filter, "{$module}_{$fn}") |
380: | || fnmatch($filter, "$module:" . str_replace('_', '-', $fn)); |
381: | }); |
382: | } |
383: | $fns[$module] = array_values($moduleFns); |
384: | } |
385: | |
386: | return array_filter($fns); |
387: | } |
388: | |
389: | |
390: | |
391: | |
392: | |
393: | |
394: | |
395: | |
396: | public function debug_session(string $id, bool $state = true): bool |
397: | { |
398: | if (!is_debug()) { |
399: | return error('%s may only be called when debug mode is enabled', __FUNCTION__); |
400: | } |
401: | if (!apnscpSession::init()->exists($id)) { |
402: | return error('Session %s does not exist', $id); |
403: | } |
404: | |
405: | if (!$old = session_id()) { |
406: | fatal('???'); |
407: | } |
408: | |
409: | if (extension_loaded('pcntl')) { |
410: | $asyncEnabled = pcntl_async_signals(false); |
411: | } |
412: | $oldId = \session_id(); |
413: | if (!apnscpSession::restore_from_id($id, false)) { |
414: | fatal('Unable to restore session'); |
415: | } |
416: | |
417: | Session::set('DEBUG', $state); |
418: | |
419: | if (!apnscpSession::restore_from_id($oldId, false)) { |
420: | fatal('Failed to revert session'); |
421: | } |
422: | |
423: | if (extension_loaded('pcntl')) { |
424: | pcntl_signal_dispatch(); |
425: | pcntl_async_signals($asyncEnabled); |
426: | } |
427: | |
428: | return true; |
429: | } |
430: | |
431: | |
432: | |
433: | |
434: | |
435: | |
436: | |
437: | public function command_info(string $filter = ''): array |
438: | { |
439: | $fns = $this->list_commands($filter); |
440: | if (!$fns) { |
441: | return []; |
442: | } |
443: | $info = []; |
444: | |
445: | foreach ($fns as $module => $moduleFunctions) { |
446: | $class = apnscpFunctionInterceptor::get_autoload_class_from_module($module); |
447: | $instance = $class::autoloadModule($this->getAuthContext()); |
448: | try { |
449: | $rfxn = new ReflectionClass($instance); |
450: | } catch (ReflectionException $e) { |
451: | debug("Failed to reflect class `%s': %s", $class, $e->getMessage()); |
452: | continue; |
453: | } |
454: | foreach ($moduleFunctions as $fn) { |
455: | try { |
456: | $rfxnMethod = $rfxn->getMethod($fn); |
457: | } catch (ReflectionException $e) { |
458: | debug("Failed to reflect `%s'::`%s': %s", $module, $fn, $e->getMessage()); |
459: | continue; |
460: | } |
461: | $signature = "{$module}_{$fn}("; |
462: | $args = []; |
463: | foreach ($rfxnMethod->getParameters() as $param) { |
464: | $parameterSignature = ''; |
465: | if ($param->isOptional()) { |
466: | $parameterSignature .= '['; |
467: | } |
468: | if ($param->getType()) { |
469: | $parameterSignature .= $param->getType()->getName() . ' '; |
470: | } |
471: | $parameterSignature .= '$' . $param->getName(); |
472: | $args[] = $parameterSignature; |
473: | } |
474: | $signature .= implode(',', $args) . |
475: | str_repeat( |
476: | ']', |
477: | $rfxnMethod->getNumberOfParameters() - $rfxnMethod->getNumberOfRequiredParameters() |
478: | ) . ')'; |
479: | $return = null; |
480: | if ($rfxnMethod->getReturnType()) { |
481: | $return = $rfxnMethod->getReturnType()->getName(); |
482: | } |
483: | $args = [ |
484: | 'doc' => preg_replace('/^\s+/m', '', $rfxnMethod->getDocComment()), |
485: | 'parameters' => array_map('\strval', $rfxnMethod->getParameters()), |
486: | 'min' => $rfxnMethod->getNumberOfRequiredParameters(), |
487: | 'max' => $rfxnMethod->getNumberOfParameters(), |
488: | 'return' => $return, |
489: | 'signature' => $signature |
490: | ]; |
491: | $info["{$module}_{$fn}"] = $args; |
492: | } |
493: | } |
494: | |
495: | if (\count($info) === 1) { |
496: | return array_pop($info); |
497: | } |
498: | |
499: | return $info; |
500: | } |
501: | |
502: | |
503: | |
504: | |
505: | |
506: | |
507: | |
508: | |
509: | public function l(string $filter = ''): array |
510: | { |
511: | return $this->list_commands($filter); |
512: | } |
513: | |
514: | |
515: | |
516: | |
517: | |
518: | |
519: | |
520: | public function i(string $filter = ''): array |
521: | { |
522: | return $this->command_info($filter); |
523: | } |
524: | |
525: | |
526: | |
527: | |
528: | |
529: | |
530: | public function get_job_queue(): array |
531: | { |
532: | $app = \Lararia\Bootstrapper::minstrap(); |
533: | $jobs = $app->make(JobRepository::class); |
534: | if (!$jobs) { |
535: | return []; |
536: | } |
537: | return $jobs->getRecent()->map(static function ($job) { |
538: | $payload = json_decode((string)$job->payload, true); |
539: | $job->tag = (array)array_get((array)$payload, 'tags', []); |
540: | $job->payload = null; |
541: | return $job; |
542: | })->filter(function ($job) { |
543: | return (!$this->site || in_array($this->site, $job->tag, true)) && |
544: | !$job->completed_at && !$job->failed_at && $job->status; |
545: | })->values()->toArray(); |
546: | } |
547: | |
548: | |
549: | |
550: | |
551: | |
552: | |
553: | |
554: | |
555: | |
556: | public function jobify(string $cmd, array $args = [], string $site = null): bool |
557: | { |
558: | if (DEMO_ADMIN_LOCK && posix_getuid()) { |
559: | return error("Demo may not schedule jobs"); |
560: | } |
561: | $context = \Auth::context(null, $site); |
562: | $job = \Lararia\Jobs\Job::create( |
563: | \Lararia\Jobs\SimpleCommandJob::class, |
564: | $context, |
565: | $cmd, |
566: | ...$args |
567: | ); |
568: | $job->setTags([$context->site, $cmd]); |
569: | $job->dispatch(); |
570: | |
571: | return true; |
572: | } |
573: | |
574: | |
575: | public function _edit() |
576: | { |
577: | $conf_old = $this->getAuthContext()->getAccount()->old; |
578: | $conf_new = $this->getAuthContext()->getAccount()->new; |
579: | if ($conf_new == $conf_old) { |
580: | return; |
581: | } |
582: | if (!$conf_new['ssh']['enabled']) { |
583: | $this->_delete(); |
584: | } |
585: | |
586: | return; |
587: | } |
588: | |
589: | public function _delete() |
590: | { |
591: | $services = array('procfs', 'fcgi'); |
592: | foreach ($services as $s) { |
593: | if ($this->is_mounted($s)) { |
594: | $this->unmount_service($s); |
595: | } |
596: | } |
597: | } |
598: | |
599: | public function _cron(Cronus $cron) { |
600: | \Opcenter\Http\Apnscp::cull(); |
601: | if (JobDaemon::isStandalone() && !JobDaemon::checkState()) { |
602: | JobDaemon::get()->start(); |
603: | } |
604: | $this->checkMemory(); |
605: | |
606: | if (!APNSCPD_HEADLESS && SCREENSHOTS_ENABLED) { |
607: | $cron->schedule(86400*5, 'theme', function () { |
608: | $this->theme_inventory(); |
609: | }); |
610: | } |
611: | |
612: | if (\Opcenter\License::get()->isTrial() && ($email = $this->admin_get_email())) { |
613: | $cron->schedule(86400, 'notify-trial', function () use ($email) { |
614: | $license = \Opcenter\License::get(); |
615: | if (in_array($license->daysUntilExpire(), [1, 3, 7], true)) { |
616: | $ip = \Opcenter\Net\Ip4::my_ip(); |
617: | $link = "https://" . $ip . ":" . Auth_Redirect::CP_SSL_PORT; |
618: | |
619: | if ($ip === Net_Gethost::gethostbyname_t(SERVER_NAME, 1500) && $this->common_get_email()) { |
620: | $link = "https://" . SERVER_NAME . ":" . Auth_Redirect::CP_SSL_PORT; |
621: | } |
622: | |
623: | $mail = Illuminate\Support\Facades\Mail::to($email); |
624: | $args = [ |
625: | 'secure_link' => $link, |
626: | 'hostname' => SERVER_NAME, |
627: | 'expire' => $license->daysUntilExpire(), |
628: | 'ip' => \Opcenter\Net\Ip4::my_ip() |
629: | ]; |
630: | $mail->send(new \Lararia\Mail\TrialEnding($args)); |
631: | } |
632: | }); |
633: | } |
634: | } |
635: | |
636: | private function checkMemory(): void |
637: | { |
638: | static $cfg; |
639: | |
640: | if (null === $cfg) { |
641: | $cfg = [ |
642: | 'maxmemory' => Memory::stats()['memtotal'] . 'KB' |
643: | ]; |
644: | foreach (['redis.conf'] as $f) { |
645: | $path = config_path($f); |
646: | if (!file_exists($path)) { |
647: | continue; |
648: | } |
649: | $cfg = Map::load($path, 'r', 'textfile')->fetchAll() + $cfg; |
650: | } |
651: | $cfg['maxmemory'] = Formatter::changeBytes($cfg['maxmemory']); |
652: | } |
653: | |
654: | $cache = \Cache_Global::spawn(); |
655: | $stats = $cache->info(); |
656: | |
657: | if ($stats['used_memory'] < ($cfg['maxmemory'] * 0.995 )) { |
658: | return; |
659: | } |
660: | |
661: | try { |
662: | |
663: | $count = (int)(max(0, $cfg['maxmemory'] - $stats['used_memory']) + 2); |
664: | $payload = str_repeat('X', $count); |
665: | if (!$cache->rawCommand("SET", \Cache_Global::$key . self::MEMTEST_KEY, $payload, 1)) { |
666: | throw new RuntimeException($cache->getLastError()); |
667: | } |
668: | } catch (RedisException|RuntimeException $e) { |
669: | warn("Redis memory usage `%.2f' MB within maxmemory `%.2f' MB - raising by 20%%", |
670: | Formatter::changeBytes($stats['used_memory'], 'MB', 'B'), |
671: | Formatter::changeBytes($cfg['maxmemory'], 'MB', 'B') |
672: | ); |
673: | |
674: | $path = config_path('redis.conf'); |
675: | $contents = file_get_contents($path); |
676: | $replacement = 'maxmemory ' . (int)(Formatter::changeBytes($stats['maxmemory'], 'MB', 'B') * 1.2) . 'MB'; |
677: | $re = Regex::compile(Regex::REDIS_DIRECTIVE_C, ['directive' => 'maxmemory']); |
678: | $new = preg_replace($re, $replacement, $contents); |
679: | if ($new === $contents) { |
680: | $new .= "\n" . $replacement; |
681: | } |
682: | file_put_contents($path, $new); |
683: | silence(static function () use ($cache) { |
684: | Lararia\Bootstrapper::minstrap(); |
685: | JobDaemon::get()->running() && JobDaemon::get()->kill(); |
686: | try { |
687: | |
688: | $cache->rawCommand('SHUTDOWN'); |
689: | } catch (RedisException $e) { |
690: | } |
691: | unset($cache); |
692: | Apnscp::restart('now'); |
693: | exit; |
694: | }); |
695: | } finally { |
696: | $cache->del(self::MEMTEST_KEY); |
697: | } |
698: | } |
699: | |
700: | public function theme_inventory() { |
701: | if (!IS_CLI) { |
702: | return $this->query('misc_theme_inventory'); |
703: | } |
704: | |
705: | $site = \Opcenter\Account\Ephemeral::create(['apache.enabled' => false]); |
706: | $driver = new \Service\BulkCapture(new \Service\CaptureDevices\Chromedriver); |
707: | $ctx = $site->getContext(); |
708: | $afi = $site->getApnscpFunctionInterceptor(); |
709: | $id = $this->admin_hijack($ctx->site, null, 'UI'); |
710: | debug("Setting id: %s", $id); |
711: | $prefs = $afi->common_load_preferences(); |
712: | foreach (StyleManager::getThemes() as $theme) { |
713: | array_set($prefs, Page_Renderer::THEME_KEY, $theme); |
714: | $afi->common_save_preferences($prefs); |
715: | debug('Capturing theme %s on %s', $theme, $ctx->site); |
716: | $driver->snap(\Opcenter\Http\Apnscp::CHECK_URL, '/apps/dashboard?' . session_name() . '=' . $id, null, storage_path('themes/' . $theme . '.png')); |
717: | usleep(500000); |
718: | } |
719: | $site->destroy(); |
720: | |
721: | return true; |
722: | } |
723: | |
724: | |
725: | |
726: | |
727: | |
728: | |
729: | |
730: | public function run_cron(mixed $module = null): void |
731: | { |
732: | if (!IS_CLI && posix_getuid()) { |
733: | $this->query('misc_run_cron', $module); |
734: | |
735: | return; |
736: | } |
737: | |
738: | if (!$module) { |
739: | $module = \apnscpFunctionInterceptor::list_all_modules(); |
740: | } else { |
741: | $module = (array)$module; |
742: | } |
743: | |
744: | foreach ($module as $m) { |
745: | $class = \apnscpFunctionInterceptor::get_class_from_module($m); |
746: | if (!method_exists($class, '_cron')) { |
747: | debug("No cron method on %(module)s (%(impl)s)", ['module' => $m, 'impl' => $class]); |
748: | continue; |
749: | } |
750: | |
751: | $instance = $class::instantiateContexted($this->getAuthContext()); |
752: | $cron = new Cronus; |
753: | $cron->force = true; |
754: | $instance->_cron($cron); |
755: | |
756: | } |
757: | } |
758: | |
759: | public function _housekeeping() |
760: | { |
761: | $this->checkMemory(); |
762: | |
763: | |
764: | if (extension_loaded('curl')) { |
765: | $adapter = new HTTP_Request2_Adapter_Curl(); |
766: | } else { |
767: | $adapter = new HTTP_Request2_Adapter_Socket(); |
768: | } |
769: | if (!APNSCPD_HEADLESS) { |
770: | dlog('Purging CP pagespeed cache'); |
771: | $url = 'http://localhost:' . Auth_Redirect::CP_PORT . '/*'; |
772: | |
773: | $http = new HTTP_Request2( |
774: | $url, |
775: | 'PURGE', |
776: | array( |
777: | 'adapter' => $adapter, |
778: | 'store_body' => false, |
779: | 'timeout' => 5, |
780: | 'connect_timeout' => 3 |
781: | ) |
782: | ); |
783: | try { |
784: | $http->send(); |
785: | } catch (Exception $e) { |
786: | dlog("WARN: failed to purge pagespeed cache, %s. Is `%s' reachable?", |
787: | $e->getMessage(), |
788: | dirname($url)); |
789: | } |
790: | } |
791: | |
792: | $ret = \Util_Process::exec(['%s/artisan', 'config:cache'], INCLUDE_PATH); |
793: | if ($ret['success']) { |
794: | dlog('Cached Laravel configuration'); |
795: | } else if (str_contains($ret['stderr'], 'type array, int given')) { |
796: | foreach(['packages', 'services'] as $cfg) { |
797: | unlink(storage_path("cache/{$cfg}.php")); |
798: | } |
799: | Apnscp::restart('now'); |
800: | } else { |
801: | dlog('Failed to cache Laravel configuration - %s', coalesce($ret['stderr'], $ret['stdout'])); |
802: | } |
803: | $path = Bootstrapper::app()->getCachedConfigPath(); |
804: | if (file_exists($path) && filesize($path) === 0) { |
805: | dlog("Removing zero-byte cached configuration in `%s'", $path); |
806: | unlink($path); |
807: | } |
808: | |
809: | dlog('Updating browscap'); |
810: | |
811: | \Util_Browscap::update(); |
812: | |
813: | if (Opcenter::updateTags()) { |
814: | dlog('Release tags updated'); |
815: | } |
816: | |
817: | dlog('Rewriting AOF data'); |
818: | try { |
819: | |
820: | |
821: | |
822: | |
823: | |
824: | if (!Cache_Global::spawn()->bgrewriteaof()) { |
825: | throw new \RedisException('Failed to perform bgrewrite operation'); |
826: | } |
827: | Cache_Base::disconnect(); |
828: | } catch (\RedisException $e) { |
829: | warn('Failed to rewrite AOF'); |
830: | } |
831: | |
832: | return true; |
833: | } |
834: | |
835: | |
836: | |
837: | |
838: | |
839: | |
840: | |
841: | public function release_fsghost(string $bin): bool |
842: | { |
843: | if (!IS_CLI && posix_getuid()) { |
844: | return $this->query('misc_release_fsghost', $bin); |
845: | } |
846: | if ($bin[0] !== '/') { |
847: | return error("Path must be absolute"); |
848: | } |
849: | |
850: | $inode = null; |
851: | foreach(ServiceLayer::available() as $service) { |
852: | if (is_file($tmp = FILESYSTEM_TEMPLATE . "/{$service}/{$bin}")) { |
853: | $inode = stat($tmp)['ino']; |
854: | debug("Detected binary %(path)s under %(service)s with inode %(inode)d", [ |
855: | 'path' => $bin, 'service' => $service, 'inode' => $inode |
856: | ]); |
857: | break; |
858: | } |
859: | } |
860: | if (null === $inode) { |
861: | return error("File does not exist within `%(path)s'", ['path' => FILESYSTEM_TEMPLATE]); |
862: | } |
863: | |
864: | |
865: | $bin = realpath($bin); |
866: | $inodeWhitelist = [$inode]; |
867: | if (is_file($bin) && ($tmp = stat($bin)['ino']) !== $inode) { |
868: | debug("Detected system binary %(path)s with additional inode %(inode)d", ['path' => $bin, 'inode' => $tmp]); |
869: | $inodeWhitelist[] = $tmp; |
870: | } |
871: | $queue = new Deferred; |
872: | $siteKill = []; |
873: | Process::all(function($pid) use ($bin, $inodeWhitelist, &$siteKill, $queue) { |
874: | debug("Examining PID %d", $pid); |
875: | $filemap = Process::maps($pid); |
876: | foreach ((array)$filemap as $entry) { |
877: | if (null === $entry['pathname']) { |
878: | continue; |
879: | } |
880: | |
881: | if ($entry['pathname'] !== $bin && !fnmatch(FILESYSTEM_VIRTBASE . "/site[0-9]*/fst{$bin}", $entry['pathname'])) { |
882: | continue; |
883: | } |
884: | |
885: | if (in_array($entry['inode'], $inodeWhitelist, true)) { |
886: | continue; |
887: | } |
888: | |
889: | info("Process %(pid)s with path %(path)s - inode %(inode)d ghosted", |
890: | ['pid' => $pid, 'path' => $entry['pathname'], 'inode' => $entry['inode']]); |
891: | $siteid = Process::siteProcess($pid); |
892: | if (null === $siteid) { |
893: | warn("Cannot detect site root from process %(pid)d", ['pid' => $pid]); |
894: | continue; |
895: | } |
896: | |
897: | if (!isset($siteKill[$siteid])) { |
898: | $controller = Controller::make(new Group("site{$siteid}"), 'freezer'); |
899: | if ($controller->exists()) { |
900: | $controller->createAttribute('state', State::STATE_FROZEN)->activate(); |
901: | } else { |
902: | warn("cgroup missing on %(site)s - vfs may not update", ['site' => "site{$siteid}"]); |
903: | } |
904: | defer($queue, static function() use ($siteid, $controller) { |
905: | $controller->exists() && $controller->createAttribute('state', State::STATE_THAWED)->activate(); |
906: | debug("Flushing site{$siteid}"); |
907: | }); |
908: | $siteKill[$siteid] = 1; |
909: | } |
910: | |
911: | |
912: | Process::killWait($pid, SIGKILL); |
913: | (new ServiceLayer("site{$siteid}"))->flush(); |
914: | info("Forced process kill on %(pid)d in %(site)s", ['pid' => $pid, 'site' => "site{$siteid}"]); |
915: | |
916: | break; |
917: | } |
918: | }); |
919: | |
920: | unset($queue); |
921: | foreach (\Opcenter\Account\Enumerate::sites() as $site) { |
922: | if (isset($siteKill[(int)substr($site, 4)])) { |
923: | continue; |
924: | } |
925: | (new ServiceLayer($site))->flush(); |
926: | } |
927: | |
928: | return true; |
929: | } |
930: | } |