1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26: 27: 28: 29: 30: 31: 32: 33: 34: 35: 36: 37: 38: 39: 40: 41: 42: 43: 44: 45: 46: 47: 48: 49: 50: 51: 52: 53: 54: 55: 56: 57: 58: 59: 60: 61: 62: 63: 64: 65: 66: 67: 68: 69: 70: 71: 72: 73: 74: 75: 76: 77: 78: 79: 80: 81: 82: 83: 84: 85: 86: 87: 88: 89: 90: 91: 92: 93: 94: 95: 96: 97: 98: 99: 100: 101: 102: 103: 104: 105: 106: 107: 108: 109: 110: 111: 112: 113: 114: 115: 116: 117: 118: 119: 120: 121: 122: 123: 124: 125: 126: 127: 128: 129: 130: 131: 132: 133: 134: 135: 136: 137: 138: 139: 140: 141: 142: 143: 144: 145: 146: 147: 148: 149: 150: 151: 152: 153: 154: 155: 156: 157: 158: 159: 160: 161: 162: 163: 164: 165: 166: 167: 168: 169: 170: 171: 172: 173: 174: 175: 176: 177: 178: 179: 180: 181: 182: 183: 184: 185: 186: 187: 188: 189: 190: 191: 192: 193: 194: 195: 196: 197: 198: 199: 200: 201: 202: 203: 204: 205: 206: 207: 208: 209: 210: 211: 212: 213: 214: 215: 216: 217: 218: 219: 220: 221: 222: 223: 224: 225: 226: 227: 228: 229: 230: 231: 232: 233: 234: 235: 236: 237: 238: 239: 240: 241: 242: 243: 244: 245: 246: 247: 248: 249: 250: 251: 252: 253: 254: 255: 256: 257: 258: 259: 260: 261: 262: 263: 264: 265: 266: 267: 268: 269: 270: 271: 272: 273: 274: 275: 276: 277: 278: 279: 280: 281: 282: 283: 284: 285: 286: 287: 288: 289: 290: 291: 292: 293: 294: 295: 296: 297: 298: 299: 300: 301: 302: 303: 304: 305: 306: 307: 308: 309: 310: 311: 312: 313: 314: 315: 316: 317: 318: 319: 320: 321: 322: 323: 324: 325: 326: 327: 328: 329: 330: 331: 332: 333: 334: 335: 336: 337: 338: 339: 340: 341: 342: 343: 344: 345: 346: 347: 348: 349: 350: 351: 352: 353: 354: 355: 356: 357: 358: 359: 360: 361: 362: 363: 364: 365: 366: 367: 368: 369: 370: 371: 372: 373: 374: 375: 376: 377: 378: 379: 380: 381: 382: 383: 384: 385: 386: 387: 388: 389: 390: 391: 392: 393: 394: 395: 396: 397: 398: 399: 400: 401: 402: 403: 404: 405: 406: 407: 408: 409: 410: 411: 412: 413: 414: 415: 416: 417: 418: 419: 420: 421: 422: 423: 424: 425: 426: 427: 428: 429: 430: 431: 432: 433: 434: 435: 436: 437: 438: 439: 440: 441: 442: 443: 444: 445: 446: 447: 448: 449: 450: 451: 452: 453: 454: 455: 456: 457: 458: 459: 460: 461: 462: 463: 464: 465: 466: 467: 468: 469: 470: 471: 472: 473: 474: 475: 476: 477: 478: 479: 480: 481: 482: 483: 484: 485: 486: 487: 488: 489: 490: 491: 492: 493: 494: 495: 496: 497: 498: 499: 500: 501: 502: 503: 504: 505: 506: 507: 508: 509: 510: 511: 512: 513: 514: 515: 516: 517: 518: 519: 520: 521: 522: 523: 524: 525: 526: 527: 528: 529: 530: 531: 532: 533: 534: 535: 536: 537: 538: 539: 540: 541: 542: 543: 544: 545: 546: 547: 548: 549: 550: 551: 552: 553: 554: 555: 556: 557: 558: 559: 560: 561: 562: 563: 564: 565: 566: 567: 568: 569: 570: 571: 572: 573: 574: 575: 576: 577: 578: 579: 580: 581: 582: 583: 584: 585: 586: 587: 588: 589: 590: 591: 592: 593: 594: 595: 596: 597: 598: 599: 600: 601: 602: 603: 604: 605: 606: 607: 608: 609: 610: 611: 612: 613: 614: 615: 616: 617: 618: 619: 620: 621: 622: 623: 624: 625: 626: 627: 628: 629: 630: 631: 632: 633: 634: 635: 636: 637: 638: 639: 640: 641: 642: 643: 644: 645: 646: 647: 648: 649: 650: 651: 652: 653: 654: 655: 656: 657: 658: 659: 660: 661: 662: 663: 664: 665: 666: 667: 668: 669: 670: 671: 672: 673: 674: 675: 676: 677: 678: 679: 680: 681: 682: 683: 684: 685: 686: 687: 688: 689: 690: 691: 692: 693: 694: 695: 696: 697: 698: 699: 700: 701: 702: 703: 704: 705: 706: 707: 708: 709: 710: 711: 712: 713: 714: 715: 716: 717: 718: 719: 720:
<?php
declare(strict_types=1);
use Frontend\Css\StyleManager;
use Lararia\Bootstrapper;
use Lararia\JobDaemon;
use Laravel\Horizon\Contracts\JobRepository;
use Opcenter\Apnscp;
use Opcenter\Map;
use Opcenter\System\Memory;
class Misc_Module extends Module_Skeleton
{
const MOUNTRC = '/etc/init.d/vmount';
const MEMTEST_KEY = '_misc_cron_memory_test';
const MOUNTABLE_SERVICES = [
'procfs', 'fcgi'
];
protected $exportedFunctions =
[
'*' => PRIVILEGE_SITE,
'get_job_queue' => PRIVILEGE_ADMIN,
'jobify' => PRIVILEGE_ADMIN,
'flush_cp_version' => PRIVILEGE_ADMIN,
'cp_version' => PRIVILEGE_ALL,
'platform_version' => PRIVILEGE_ALL,
'dashboard_memory_usage' => PRIVILEGE_ALL,
'lservice_memory_usage' => PRIVILEGE_ALL,
'changelog' => PRIVILEGE_ALL,
'run' => PRIVILEGE_SITE,
'notify_installed' => PRIVILEGE_ADMIN,
'notify_update_failure' => PRIVILEGE_ADMIN,
'list_commands' => PRIVILEGE_ALL,
'command_info' => PRIVILEGE_ALL,
'debug_session' => PRIVILEGE_ADMIN,
'theme_inventory' => PRIVILEGE_ADMIN,
'i' => PRIVILEGE_ALL,
'l' => PRIVILEGE_ALL
];
public function cp_version(string $field = '')
{
return \Opcenter::versionData($field);
}
public function flush_cp_version(): bool
{
return Opcenter::forgetVersion();
}
public function platform_version(): string
{
return platform_version();
}
public function dashboard_memory_usage(): int
{
return memory_get_usage();
}
public function apnscpd_memory_usage(): int
{
if (!IS_CLI) {
return $this->query('misc_apnscpd_memory_usage');
}
return memory_get_usage();
}
public function toggle_procfs(): bool
{
if (!$this->getServiceValue('ssh', 'enabled')) {
return error('procfs requires ssh');
}
if ($this->is_mounted('procfs')) {
return $this->unmount_service('procfs');
}
return $this->mount_service('procfs');
}
public function is_mounted(string $svc): bool
{
if (!\in_array($svc, static::MOUNTABLE_SERVICES, true)) {
return error("Unknown service `%s'", $svc);
}
if (version_compare(platform_version(), '6', '>=')) {
return true;
}
$proc = Util_Process::exec('%s mounted %s %s',
self::MOUNTRC,
$this->site,
$svc,
array(0, 1)
);
return $proc['return'] === 0;
}
public function unmount_service(string $svc): bool
{
if (!\in_array($svc, static::MOUNTABLE_SERVICES, true)) {
return error("Unknown service `%s'", $svc);
}
if ($svc == 'procfs' && version_compare(platform_version(), '6', '>=')) {
return true;
}
if (!IS_CLI) {
return $this->query('misc_unmount_service', $svc);
}
$proc = Util_Process::exec(
'%s unmount %s %s',
self::MOUNTRC,
$this->site,
$svc
);
if ($proc['errno'] != 0) {
return false;
}
return $this->_edit_mount_map($svc, false) !== 0;
}
private function _edit_mount_map(string $svc, bool $mount): int
{
$sysconf = '/etc/sysconfig/vmount-' . $svc;
touch($sysconf);
$sites = explode("\n", trim(file_get_contents($sysconf)));
$idx = array_search($this->site, $sites, true);
if ($mount && $idx === false) {
$sites[] = $this->site;
} else if (!$mount && $idx !== false) {
unset($sites[$idx]);
} else {
return -1;
}
file_put_contents($sysconf, join("\n", $sites));
return 1;
}
public function mount_service($svc): bool
{
if (!\in_array($svc, static::MOUNTABLE_SERVICES, true)) {
return error("Unknown service `%s'", $svc);
}
if ($svc == 'fcgi' && version_compare(platform_version(), '4.5', '>=')) {
return true;
}
if ($svc == 'procfs' && version_compare(platform_version(), '6', '>=')) {
return true;
}
if (!IS_CLI) {
return $this->query('misc_mount_service', $svc);
}
$proc = Util_Process::exec(
'%s mount %s %s',
self::MOUNTRC,
$this->site,
$svc
);
if ($proc['return'] !== 0) {
return false;
}
return $this->_edit_mount_map($svc, true) !== 0;
}
public function procfs_enabled(): bool
{
return $this->is_mounted('procfs');
}
public function changelog(): array
{
$cache = \Cache_Global::spawn();
$key = 'misc.changelog';
$changelog = $cache->get($key);
if ($changelog) {
return $changelog;
}
$proc = Util_Process::exec('cd ' . INCLUDE_PATH . ' && git log --submodule -n 15 ');
if (!$proc['success']) {
return [];
}
$res = [];
preg_match_all(Regex::CHANGELOG_COMMIT, $proc['output'], $matches, PREG_SET_ORDER);
foreach ($matches as $match) {
foreach (array_keys($match) as $key) {
if (is_numeric($key)) {
unset($match[$key]);
} else if ($key === 'msg') {
$match[$key] = trim($match[$key]);
} else if ($key === 'date') {
$match['ts'] = strtotime($match[$key]);
unset($match[$key]);
}
}
$res[] = $match;
}
$cache->set($key, $res);
return $res;
}
public function notify_installed(string $password): bool
{
if (!($email = $this->admin_get_email())) {
return error('Cannot send notification email - no email defined! See docs/INSTALL.md');
}
$mail = Illuminate\Support\Facades\Mail::to($email);
$args = [
'hostname' => SERVER_NAME,
'admin_user' => $this->username,
'admin_password' => $password,
'apnscp_root' => INCLUDE_PATH,
'ip' => \Opcenter\Net\Ip4::my_ip()
];
$mail->send(new \Lararia\Mail\PanelInstalled($args));
return true;
}
public function notify_update_failure(): bool
{
if (!($email = $this->admin_get_email())) {
return error('Cannot send notification email - no email defined! See docs/INSTALL.md');
}
if (!file_exists($path = storage_path('.upcp.failure'))) {
return true;
}
$mail = Illuminate\Support\Facades\Mail::to($email);
$msg = (new \Lararia\Mail\Simple('email.admin.update-failed'))
->asMarkdown()->attach($path, ['as' => 'update-log.txt']);
$mail->send($msg);
unlink($path);
return true;
}
public function list_commands(string $filter = ''): array
{
$fns = [];
$modules = \apnscpFunctionInterceptor::list_all_modules();
asort($modules);
foreach ($modules as $module) {
$moduleFns = $this->getApnscpFunctionInterceptor()->authorized_functions($module);
asort($moduleFns);
if ($filter) {
$moduleFns = array_filter($moduleFns, static function ($fn) use ($filter, $module) {
return fnmatch($filter, "${module}_${fn}")
|| fnmatch($filter, "$module:" . str_replace('_', '-', $fn));
});
}
$fns[$module] = array_values($moduleFns);
}
return array_filter($fns);
}
public function debug_session(string $id, bool $state = true): bool
{
if (!is_debug()) {
return error('%s may only be called when debug mode is enabled', __FUNCTION__);
}
if (!apnscpSession::init()->exists($id)) {
return error('Session %s does not exist', $id);
}
if (!$old = session_id()) {
fatal('???');
}
if (extension_loaded('pcntl')) {
$asyncEnabled = pcntl_async_signals(false);
}
$oldId = \session_id();
if (!apnscpSession::restore_from_id($id, false)) {
fatal('Unable to restore session');
}
Session::set('DEBUG', $state);
if (!apnscpSession::restore_from_id($oldId, false)) {
fatal('Failed to revert session');
}
if (extension_loaded('pcntl')) {
pcntl_signal_dispatch();
pcntl_async_signals($asyncEnabled);
}
return true;
}
public function command_info(string $filter = ''): array
{
$fns = $this->list_commands($filter);
if (!$fns) {
return [];
}
$info = [];
foreach ($fns as $module => $moduleFunctions) {
$class = apnscpFunctionInterceptor::get_autoload_class_from_module($module);
$instance = $class::autoloadModule($this->getAuthContext());
try {
$rfxn = new ReflectionClass($instance);
} catch (ReflectionException $e) {
debug("Failed to reflect class `%s': %s", $class, $e->getMessage());
continue;
}
foreach ($moduleFunctions as $fn) {
try {
$rfxnMethod = $rfxn->getMethod($fn);
} catch (ReflectionException $e) {
debug("Failed to reflect `%s'::`%s': %s", $module, $fn, $e->getMessage());
continue;
}
$signature = "${module}_${fn}(";
$args = [];
foreach ($rfxnMethod->getParameters() as $param) {
$parameterSignature = '';
if ($param->isOptional()) {
$parameterSignature .= '[';
}
if ($param->getType()) {
$parameterSignature .= $param->getType()->getName() . ' ';
}
$parameterSignature .= '$' . $param->getName();
$args[] = $parameterSignature;
}
$signature .= implode(',', $args) .
str_repeat(
']',
$rfxnMethod->getNumberOfParameters() - $rfxnMethod->getNumberOfRequiredParameters()
) . ')';
$args = [
'doc' => preg_replace('/^\s+/m', '', $rfxnMethod->getDocComment()),
'parameters' => array_map('\strval', $rfxnMethod->getParameters()),
'min' => $rfxnMethod->getNumberOfRequiredParameters(),
'max' => $rfxnMethod->getNumberOfParameters(),
'return' => $rfxnMethod->getReturnType()->getName(),
'signature' => $signature
];
$info["${module}_${fn}"] = $args;
}
}
if (\count($info) === 1) {
return array_pop($info);
}
return $info;
}
public function l(string $filter = ''): array
{
return $this->list_commands($filter);
}
public function i(string $filter = ''): array
{
return $this->command_info($filter);
}
public function get_job_queue(): array
{
$app = \Lararia\Bootstrapper::minstrap();
$jobs = $app->make(JobRepository::class);
if (!$jobs) {
return [];
}
return $jobs->getRecent()->map(static function ($job) {
$payload = json_decode((string)$job->payload, true);
$job->tag = (array)array_get((array)$payload, 'tags', []);
$job->payload = null;
return $job;
})->filter(static function ($job) {
return !$job->completed_at && !$job->failed_at && $job->status;
})->values()->toArray();
}
public function jobify(string $cmd, array $args = [], string $site = null): bool
{
$context = \Auth::context(null, $site);
$job = \Lararia\Jobs\Job::create(
\Lararia\Jobs\SimpleCommandJob::class,
$context,
$cmd,
...$args
);
$job->setTags([$context->site, $cmd]);
$job->dispatch();
return true;
}
public function _edit()
{
$conf_old = $this->getAuthContext()->getAccount()->old;
$conf_new = $this->getAuthContext()->getAccount()->new;
if ($conf_new == $conf_old) {
return;
}
if (!$conf_new['ssh']['enabled']) {
$this->_delete();
}
return;
}
public function _delete()
{
$services = array('procfs', 'fcgi');
foreach ($services as $s) {
if ($this->is_mounted($s)) {
$this->unmount_service($s);
}
}
}
public function _cron(Cronus $cron) {
static $cfg;
\Opcenter\Http\Apnscp::cull();
if (null === $cfg) {
$cfg = [
'maxmemory' => Memory::stats()['memtotal'] . 'KB'
];
foreach (['redis.conf'] as $f) {
$path = config_path($f);
if (!file_exists($path)) {
continue;
}
$cfg = Map::load($path, 'r', 'textfile')->fetchAll() + $cfg;
}
$cfg['maxmemory'] = Formatter::changeBytes($cfg['maxmemory']);
}
$cache = \Cache_Global::spawn();
$stats = $cache->info();
if ($stats['used_memory'] < ($cfg['maxmemory'] * 0.995 )) {
return;
}
try {
$cache->set(self::MEMTEST_KEY, str_repeat('X', $cfg['maxmemory']-$stats['used_memory']+2), 1);
} catch (RedisException $e) {
warn("Redis memory usage `%.2f' MB within maxmemory `%.2f' MB - raising by 20%%",
Formatter::changeBytes($stats['used_memory'], 'MB', 'B'),
Formatter::changeBytes($cfg['maxmemory'], 'MB', 'B')
);
$path = config_path('redis.conf');
$cfg = Map::load($path, 'r+', 'textfile');
$cfg['maxmemory'] = (int)(Formatter::changeBytes($stats['maxmemory'], 'MB', 'B') * 1.2) . 'MB';
$cfg->save();
silence(static function () use ($cache) {
JobDaemon::get()->running() && JobDaemon::get()->kill();
try {
$cache->rawCommand('SHUTDOWN');
} catch (RedisException $e) {
}
unset($cache);
Apnscp::restart('now');
exit;
});
} finally {
$cache->del(self::MEMTEST_KEY);
}
if (!APNSCPD_HEADLESS && !\Opcenter\License::get()->isDnsOnly()) {
$cron->schedule(86400*5, 'theme', function () {
$this->theme_inventory();
});
}
}
public function theme_inventory() {
$site = \Opcenter\Account\Ephemeral::create();
$driver = new \Service\BulkCapture(new \Service\CaptureDevices\Chromedriver);
$ctx = $site->getContext();
$afi = $site->getApnscpFunctionInterceptor();
$id = $this->admin_hijack($ctx->site, null, 'UI');
debug("Setting id: %s", $id);
$prefs = $afi->common_load_preferences();
foreach (StyleManager::getThemes() as $theme) {
array_set($prefs, Page_Renderer::THEME_KEY, $theme);
$afi->common_save_preferences($prefs);
debug('Capturing theme %s on %s', $theme, $ctx->site);
$driver->snap(\Opcenter\Http\Apnscp::CHECK_URL, '/apps/dashboard?' . session_name() . '=' . $id, null, storage_path('themes/' . $theme . '.png'));
}
}
public function _housekeeping()
{
if (extension_loaded('curl')) {
$adapter = new HTTP_Request2_Adapter_Curl();
} else {
$adapter = new HTTP_Request2_Adapter_Socket();
}
if (!APNSCPD_HEADLESS) {
dlog('Purging CP pagespeed cache');
$url = 'http://localhost:' . Auth_Redirect::CP_PORT . '/*';
$http = new HTTP_Request2(
$url,
'PURGE',
array(
'adapter' => $adapter,
'store_body' => false,
'timeout' => 5,
'connect_timeout' => 3
)
);
try {
$http->send();
} catch (Exception $e) {
dlog("WARN: failed to purge pagespeed cache, %s. Is `%s' reachable?",
$e->getMessage(),
dirname($url));
}
}
$ret = \Util_Process::exec('%s/artisan config:cache', INCLUDE_PATH);
if ($ret['success']) {
dlog('Cached Laravel configuration');
} else {
dlog('Failed to cache Laravel configuration - %s', coalesce($ret['stderr'], $ret['stdout']));
}
$path = Bootstrapper::app()->getCachedConfigPath();
if (file_exists($path) && filesize($path) === 0) {
dlog("Removing zero-byte cached configuration in `%s'", $path);
unlink($path);
}
dlog('Updating browscap');
\Util_Browscap::update();
if (Opcenter::updateTags()) {
dlog('Release tags updated');
}
dlog('Rewriting AOF data');
try {
if (!Cache_Global::spawn()->bgrewriteaof()) {
throw new \RedisException('Failed to perform bgrewrite operation');
}
Cache_Base::disconnect();
} catch (\RedisException $e) {
warn('Failed to rewrite AOF');
}
return true;
}
}