| 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: | } |