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