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