Bearsampp 2026.9.23
Loading...
Searching...
No Matches
class.action.quit.php
Go to the documentation of this file.
1<?php
2/*
3 *
4 * * Copyright (c) 2022-2025 Bearsampp
5 * * License: GNU General Public License version 3 or later; see LICENSE.txt
6 * * Website: https://bearsampp.com
7 * * Github: https://github.com/Bearsampp
8 *
9 */
10
17{
21 private $splash;
22
26 const GAUGE_PROCESSES = 1;
27 const GAUGE_OTHERS = 1;
28
35 public function __construct($args)
36 {
37 global $bearsamppCore, $bearsamppLang, $bearsamppBins, $bearsamppWinbinder, $arrayOfCurrents;
38
39 Log::info('ActionQuit constructor called - starting exit process');
40 Log::debug('Number of services to stop: ' . count($bearsamppBins->getServices()));
41
42 // Start splash screen
43 $this->splash = new Splash();
44 $this->splash->init(
45 $bearsamppLang->getValue(Lang::QUIT),
46 self::GAUGE_PROCESSES * count($bearsamppBins->getServices()) + self::GAUGE_OTHERS,
47 sprintf($bearsamppLang->getValue(Lang::EXIT_LEAVING_TEXT), APP_TITLE . ' ' . $bearsamppCore->getAppVersion())
48 );
49
50 Log::debug('Splash screen initialized');
51
52 // Set handler for the splash screen window
53 $bearsamppWinbinder->setHandler($this->splash->getWbWindow(), $this, 'processWindow', 2000);
54 Log::debug('Window handler set, starting main loop');
55
56 $bearsamppWinbinder->mainLoop();
57 Log::debug('Main loop exited');
58
59 $bearsamppWinbinder->reset();
60 Log::info('ActionQuit constructor completed');
61 }
62
63
70 private function getServiceShutdownOrder()
71 {
72 // Define shutdown order: dependent services first, then core services
73 // This prevents connection errors and ensures clean shutdown
74 return [
75 // Tier 1: Application services (no dependencies on other services)
76 BinMailpit::SERVICE_NAME, // Mail testing tool
77 BinMemcached::SERVICE_NAME, // Caching service
78 BinXlight::SERVICE_NAME, // FTP server
79
80 // Tier 2: Database services (web server depends on these)
81 BinPostgresql::SERVICE_NAME, // PostgreSQL database
82 BinMariadb::SERVICE_NAME, // MariaDB database
83 BinMysql::SERVICE_NAME, // MySQL database
84
85 // Tier 3: Web server (depends on databases and other services)
86 BinApache::SERVICE_NAME, // Apache web server (stopped last)
87 ];
88 }
89
98 private function getServiceDisplayName($sName, $service)
99 {
100 global $bearsamppBins;
101
102 $name = '';
103
104 if ($sName == BinApache::SERVICE_NAME) {
105 $name = $bearsamppBins->getApache()->getName() . ' ' . $bearsamppBins->getApache()->getVersion();
106 } elseif ($sName == BinMysql::SERVICE_NAME) {
107 $name = $bearsamppBins->getMysql()->getName() . ' ' . $bearsamppBins->getMysql()->getVersion();
108 } elseif ($sName == BinMailpit::SERVICE_NAME) {
109 $name = $bearsamppBins->getMailpit()->getName() . ' ' . $bearsamppBins->getMailpit()->getVersion();
110 } elseif ($sName == BinMariadb::SERVICE_NAME) {
111 $name = $bearsamppBins->getMariadb()->getName() . ' ' . $bearsamppBins->getMariadb()->getVersion();
112 } elseif ($sName == BinPostgresql::SERVICE_NAME) {
113 $name = $bearsamppBins->getPostgresql()->getName() . ' ' . $bearsamppBins->getPostgresql()->getVersion();
114 } elseif ($sName == BinMemcached::SERVICE_NAME) {
115 $name = $bearsamppBins->getMemcached()->getName() . ' ' . $bearsamppBins->getMemcached()->getVersion();
116 } elseif ($sName == BinXlight::SERVICE_NAME) {
117 $name = $bearsamppBins->getXlight()->getName() . ' ' . $bearsamppBins->getXlight()->getVersion();
118 }
119
120 $name .= ' (' . $service->getName() . ')';
121
122 return $name;
123 }
124
135 public function processWindow($window, $id, $ctrl, $param1, $param2)
136 {
137 global $bearsamppBins, $bearsamppLang, $bearsamppWinbinder;
138
139 Log::info('Starting graceful shutdown process with optimized service order');
140
141 // Get all available services
142 $allServices = $bearsamppBins->getServices();
143
144 // Get optimal shutdown order
145 $shutdownOrder = $this->getServiceShutdownOrder();
146
147 Log::debug('Service shutdown order: ' . implode(' -> ', $shutdownOrder));
148
149 // Stop services in optimal order
150 foreach ($shutdownOrder as $sName) {
151 // Check if this service exists and is installed
152 if (!isset($allServices[$sName])) {
153 Log::debug('Service not found in available services: ' . $sName);
154 continue;
155 }
156
157 $service = $allServices[$sName];
158 $displayName = $this->getServiceDisplayName($sName, $service);
159
160 Log::info('Stopping service: ' . $displayName);
161
162 $this->splash->incrProgressBar();
163 $this->splash->setTextLoading(sprintf($bearsamppLang->getValue(Lang::EXIT_REMOVE_SERVICE_TEXT), $displayName));
164
165 // Delete (stop and remove) the service
166 $result = $service->delete();
167
168 if ($result) {
169 Log::info('Successfully stopped and removed service: ' . $displayName);
170 } else {
171 Log::warning('Failed to stop/remove service: ' . $displayName . ' (may not be installed)');
172 }
173 }
174
175 // Handle any services not in the shutdown order (for extensibility)
176 foreach ($allServices as $sName => $service) {
177 if (!in_array($sName, $shutdownOrder)) {
178 $displayName = $this->getServiceDisplayName($sName, $service);
179 Log::warning('Stopping unlisted service: ' . $displayName);
180
181 $this->splash->incrProgressBar();
182 $this->splash->setTextLoading(sprintf($bearsamppLang->getValue(Lang::EXIT_REMOVE_SERVICE_TEXT), $displayName));
183 $service->delete();
184 }
185 }
186
187 Log::info('All services stopped successfully');
188
189 // Purge "current" symlinks
190 $this->splash->setTextLoading('Removing symlinks...');
192
193 // Stop other processes
194 $this->splash->incrProgressBar();
195 $this->splash->setTextLoading($bearsamppLang->getValue(Lang::EXIT_STOP_OTHER_PROCESS_TEXT));
196 Win32Ps::killBins(true);
197
198 // Explicitly kill NodeJS if still running (it's not a Windows service)
199 Log::trace('Explicitly terminating NodeJS processes');
200 Win32Ps::killBins(['node.exe']);
201
202 // Perform cleanup verification in background (non-blocking)
203 $this->splash->setTextLoading('Performing cleanup verification...');
204 $this->performQuickCleanupVerification($allServices);
205
206 // Terminate any remaining processes
207 // Final termination sequence
208 $this->splash->setTextLoading('Completing shutdown...');
209 if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') {
210 $currentPid = Win32Ps::getCurrentPid();
211
212 // Terminate PHP processes with a timeout of 15 seconds
213 self::terminatePhpProcesses($currentPid, $window, $this->splash, 15);
214
215 // Force exit if still running
216 exit(0);
217 }
218
219 // Non-Windows fallback
220 $bearsamppWinbinder->destroyWindow($window);
221 exit(0);
222 }
223
234 public static function terminatePhpProcesses($excludePid, $window = null, $splash = null, $timeout = 10)
235 {
236 global $bearsamppWinbinder, $bearsamppCore;
237
238 $currentPid = Win32Ps::getCurrentPid();
239 $startTime = microtime(true);
240
241 Log::trace('Starting PHP process termination (excluding PID: ' . $excludePid . ')');
242
243 // Get list of loading PIDs to exclude from termination
244 $loadingPids = array();
245 if (file_exists($bearsamppCore->getLoadingPid())) {
246 $pids = file($bearsamppCore->getLoadingPid());
247 foreach ($pids as $pid) {
248 $loadingPids[] = intval(trim($pid));
249 }
250 Log::trace('Loading PIDs to preserve: ' . implode(', ', $loadingPids));
251 }
252
253 $targets = ['php-win.exe', 'php.exe'];
254 foreach (Win32Ps::getListProcs() as $proc) {
255 // Check if we've exceeded our timeout
256 if (microtime(true) - $startTime > $timeout) {
257 Log::trace('Process termination timeout exceeded, continuing with remaining operations');
258 break;
259 }
260
261 $exe = strtolower(basename($proc[Win32Ps::EXECUTABLE_PATH]));
263
264 // Skip if this is the excluded PID or a loading window PID
265 if (in_array($exe, $targets) && $pid != $excludePid && !in_array($pid, $loadingPids)) {
266 Log::trace('Terminating PHP process: ' . $pid);
267 Win32Ps::kill($pid);
268 usleep(100000); // 100ms delay between terminations
269 } elseif (in_array($pid, $loadingPids)) {
270 Log::trace('Preserving loading window process: ' . $pid);
271 }
272 }
273
274 // Initiate self-termination with timeout
275 if ($splash !== null) {
276 $splash->setTextLoading('Final cleanup...');
277 }
278
279 try {
280 Log::trace('Initiating self-termination for PID: ' . $currentPid);
281 // Add a timeout wrapper around the killProc call
282 $killSuccess = Win32Native::killProcess($currentPid);
283 if (!$killSuccess) {
284 Log::trace('Self-termination via Win32Native::killProcess failed, using alternative method');
285 }
286 } catch (\Exception $e) {
287 Log::trace('Exception during self-termination: ' . $e->getMessage());
288 }
289
290 // Destroy window after process termination
291 // Fix for PHP 8.2: Check if window is not null before destroying
292 if ($window && $bearsamppWinbinder) {
293 try {
294 Log::trace('Destroying window');
295 $bearsamppWinbinder->destroyWindow($window);
296 } catch (\Exception $e) {
297 Log::trace('Exception during window destruction: ' . $e->getMessage());
298 }
299 }
300
301 // Force exit if still running after timeout
302 if (microtime(true) - $startTime > $timeout * 1.5) {
303 Log::trace('Forcing exit due to timeout');
304 exit(0);
305 }
306 }
307
315 private function verifyServicesStoppedAndCleanup($services)
316 {
317 Log::info('Verifying all services are stopped...');
318
319 $results = [
320 'all_stopped' => true,
321 'services' => [],
322 'still_running' => [],
323 'verification_failed' => []
324 ];
325
326 foreach ($services as $sName => $service) {
327 $displayName = $this->getServiceDisplayName($sName, $service);
328
329 try {
330 // Check if service is still installed/running
331 $isInstalled = $service->isInstalled();
332 $isRunning = $isInstalled ? $service->isRunning() : false;
333
334 $results['services'][$sName] = [
335 'name' => $displayName,
336 'installed' => $isInstalled,
337 'running' => $isRunning
338 ];
339
340 if ($isRunning) {
341 Log::warning('Service still running after shutdown: ' . $displayName);
342 $results['still_running'][] = $displayName;
343 $results['all_stopped'] = false;
344
345 // Attempt to force stop
346 Log::info('Attempting to force stop: ' . $displayName);
347 $service->stop();
348 usleep(500000); // Wait 500ms
349
350 // Verify again
351 if ($service->isRunning()) {
352 Log::error('Failed to force stop service: ' . $displayName);
353 } else {
354 Log::info('Successfully force stopped service: ' . $displayName);
355 }
356 } elseif ($isInstalled) {
357 Log::debug('Service stopped but still installed: ' . $displayName);
358 } else {
359 Log::debug('Service verified stopped and removed: ' . $displayName);
360 }
361 } catch (\Exception $e) {
362 Log::error('Failed to verify service status for ' . $displayName . ': ' . $e->getMessage());
363 $results['verification_failed'][] = $displayName;
364 $results['all_stopped'] = false;
365 }
366 }
367
368 if ($results['all_stopped']) {
369 Log::info('All services verified stopped successfully');
370 } else {
371 Log::warning('Some services could not be verified as stopped');
372 }
373
374 return $results;
375 }
376
382 private function verifySymlinksRemoved()
383 {
384 global $bearsamppRoot;
385
386 Log::info('Verifying symlinks are removed...');
387
388 $results = [
389 'success' => true,
390 'remaining' => []
391 ];
392
393 // Check common symlink locations
394 $symlinkPaths = [
395 Path::getRootPath() . '/apache',
396 Path::getRootPath() . '/php',
397 Path::getRootPath() . '/mysql',
398 Path::getRootPath() . '/mariadb',
399 Path::getRootPath() . '/postgresql',
400 Path::getRootPath() . '/nodejs',
401 Path::getRootPath() . '/memcached',
402 Path::getRootPath() . '/mailpit',
403 Path::getRootPath() . '/xlight'
404 ];
405
406 foreach ($symlinkPaths as $path) {
407 if (file_exists($path) || is_link($path)) {
408 Log::warning('Symlink still exists: ' . $path);
409 $results['remaining'][] = basename($path);
410 $results['success'] = false;
411
412 // Attempt to remove it using robust method
413 try {
414 $removed = \Symlinks::safeRemoveSymlink($path);
415
416 // Verify removal
417 if ($removed && !file_exists($path) && !is_link($path)) {
418 Log::info('Successfully removed remaining symlink: ' . $path);
419 $results['remaining'] = array_diff($results['remaining'], [basename($path)]);
420 if (empty($results['remaining'])) {
421 $results['success'] = true;
422 }
423 }
424 } catch (\Exception $e) {
425 Log::error('Failed to remove symlink ' . $path . ': ' . $e->getMessage());
426 }
427 }
428 }
429
430 if ($results['success']) {
431 Log::info('All symlinks verified removed');
432 } else {
433 Log::warning('Some symlinks could not be removed: ' . implode(', ', $results['remaining']));
434 }
435
436 return $results;
437 }
438
444 private function cleanupTemporaryFiles()
445 {
446 global $bearsamppCore;
447
448 Log::info('Cleaning up temporary files...');
449
450 $results = [
451 'success' => true,
452 'cleaned' => 0,
453 'failed' => [],
454 'size_freed' => 0
455 ];
456
457 $tmpPath = Path::getTmpPath();
458
459 if (!is_dir($tmpPath)) {
460 Log::debug('Temp directory does not exist: ' . $tmpPath);
461
462 return $results;
463 }
464
465 try {
466 $files = glob($tmpPath . '/*');
467
468 if ($files === false) {
469 Log::warning('Failed to list temporary files');
470
471 return $results;
472 }
473
474 foreach ($files as $file) {
475 // Skip certain files that should be preserved
476 $basename = basename($file);
477 if (in_array($basename, ['.', '..', '.gitkeep', 'README.md'])) {
478 continue;
479 }
480
481 try {
482 $size = is_file($file) ? filesize($file) : 0;
483
484 if (is_link($file)) {
485 if (@unlink($file) || @rmdir($file)) {
486 $results['cleaned']++;
487 $results['size_freed'] += $size;
488 Log::debug('Removed temp symlink: ' . $basename);
489 } else {
490 $results['failed'][] = $basename;
491 $results['success'] = false;
492 Log::warning('Failed to remove temp symlink: ' . $basename);
493 }
494 } elseif (is_file($file)) {
495 if (@unlink($file)) {
496 $results['cleaned']++;
497 $results['size_freed'] += $size;
498 Log::debug('Removed temp file: ' . $basename);
499 } else {
500 $results['failed'][] = $basename;
501 $results['success'] = false;
502 Log::warning('Failed to remove temp file: ' . $basename);
503 }
504 } elseif (is_dir($file)) {
505 Util::deleteFolder($file);
506 if (!file_exists($file)) {
507 $results['cleaned']++;
508 Log::debug('Removed temp directory: ' . $basename);
509 } else {
510 $results['failed'][] = $basename;
511 $results['success'] = false;
512 Log::warning('Failed to remove temp directory: ' . $basename);
513 }
514 }
515 } catch (\Exception $e) {
516 $results['failed'][] = $basename;
517 $results['success'] = false;
518 Log::error('Error removing temp file ' . $basename . ': ' . $e->getMessage());
519 }
520 }
521
522 $sizeMB = round($results['size_freed'] / 1024 / 1024, 2);
523 Log::info('Cleaned up ' . $results['cleaned'] . ' temporary files (' . $sizeMB . ' MB freed)');
524
525 if (!empty($results['failed'])) {
526 Log::warning('Failed to clean up ' . count($results['failed']) . ' files');
527 }
528 } catch (\Exception $e) {
529 Log::error('Error during temp file cleanup: ' . $e->getMessage());
530 $results['success'] = false;
531 }
532
533 return $results;
534 }
535
541 private function checkForOrphanedProcesses()
542 {
543 global $bearsamppRoot;
544
545 Log::info('Checking for orphaned processes...');
546
547 $orphaned = [
548 'found' => false,
549 'processes' => []
550 ];
551
552 try {
553 $procs = Win32Ps::getListProcs();
554 $bearsamppPath = strtolower(Path::formatUnixPath(Path::getRootPath()));
555 $currentPid = Win32Ps::getCurrentPid();
556
557 foreach ($procs as $proc) {
558 $exePath = strtolower(Path::formatUnixPath($proc[Win32Ps::EXECUTABLE_PATH]));
560
561 // Skip current process
562 if ($pid == $currentPid) {
563 continue;
564 }
565
566 // Check if process is from Bearsampp directory
567 if (strpos($exePath, $bearsamppPath) === 0) {
568 $processName = basename($exePath);
569
570 // Skip www directory processes (user applications)
571 if (strpos($exePath, $bearsamppPath . '/www/') === 0) {
572 continue;
573 }
574
575 // Skip the main Bearsampp executable
576 if (strtolower($processName) === 'bearsampp.exe') {
577 Log::debug('Skipping main Bearsampp process: ' . $processName . ' (PID: ' . $pid . ')');
578 continue;
579 }
580
581 // These are orphaned Bearsampp processes
582 $orphaned['found'] = true;
583 $orphaned['processes'][] = [
584 'pid' => $pid,
585 'name' => $processName,
586 'path' => $exePath
587 ];
588
589 Log::warning('Found orphaned process: ' . $processName . ' (PID: ' . $pid . ')');
590
591 // Attempt to kill orphaned process
592 try {
593 Win32Ps::kill($pid);
594 Log::info('Terminated orphaned process: ' . $processName . ' (PID: ' . $pid . ')');
595 } catch (\Exception $e) {
596 Log::error('Failed to terminate orphaned process ' . $processName . ': ' . $e->getMessage());
597 }
598 }
599 }
600
601 if (!$orphaned['found']) {
602 Log::info('No orphaned processes found');
603 } else {
604 Log::warning('Found ' . count($orphaned['processes']) . ' orphaned process(es)');
605 }
606 } catch (\Exception $e) {
607 Log::error('Error checking for orphaned processes: ' . $e->getMessage());
608 }
609
610 return $orphaned;
611 }
612
623 private function generateCleanupReport($serviceVerification, $symlinkVerification, $tempCleanup, $orphanedProcesses)
624 {
625 $report = [
626 'success' => true,
627 'warnings' => [],
628 'errors' => [],
629 'summary' => []
630 ];
631
632 // Service verification
633 if (!$serviceVerification['all_stopped']) {
634 $report['success'] = false;
635
636 if (!empty($serviceVerification['still_running'])) {
637 $report['errors'][] = 'Services still running: ' . implode(', ', $serviceVerification['still_running']);
638 }
639
640 if (!empty($serviceVerification['verification_failed'])) {
641 $report['warnings'][] = 'Could not verify status of: ' . implode(', ', $serviceVerification['verification_failed']);
642 }
643 }
644
645 $report['summary'][] = 'Services checked: ' . count($serviceVerification['services']);
646
647 // Symlink verification
648 if (!$symlinkVerification['success']) {
649 $report['warnings'][] = 'Symlinks not fully removed: ' . implode(', ', $symlinkVerification['remaining']);
650 }
651
652 // Temp file cleanup
653 if ($tempCleanup['cleaned'] > 0) {
654 $sizeMB = round($tempCleanup['size_freed'] / 1024 / 1024, 2);
655 $report['summary'][] = 'Temp files cleaned: ' . $tempCleanup['cleaned'] . ' (' . $sizeMB . ' MB)';
656 }
657
658 if (!empty($tempCleanup['failed'])) {
659 $report['warnings'][] = 'Failed to clean ' . count($tempCleanup['failed']) . ' temp file(s)';
660 }
661
662 // Orphaned processes
663 if ($orphanedProcesses['found']) {
664 $report['warnings'][] = 'Found ' . count($orphanedProcesses['processes']) . ' orphaned process(es)';
665 foreach ($orphanedProcesses['processes'] as $proc) {
666 $report['summary'][] = 'Orphaned: ' . $proc['name'] . ' (PID: ' . $proc['pid'] . ')';
667 }
668 }
669
670 return $report;
671 }
672
681 private function performQuickCleanupVerification($services)
682 {
683 Log::info('Performing quick cleanup verification...');
684
685 $startTime = microtime(true);
686 $maxTime = 2; // Maximum 2 seconds for verification
687
688 try {
689 // Quick temp file cleanup (non-blocking)
690 $tempCleanup = $this->cleanupTemporaryFiles();
691
692 // Check if we're running out of time
693 if (microtime(true) - $startTime > $maxTime) {
694 Log::debug('Cleanup verification timeout reached, skipping remaining checks');
695
696 return;
697 }
698
699 // Quick orphaned process check (non-blocking)
700 $orphanedProcesses = $this->checkForOrphanedProcesses();
701
702 // Log summary
703 if ($tempCleanup['cleaned'] > 0) {
704 $sizeMB = round($tempCleanup['size_freed'] / 1024 / 1024, 2);
705 Log::info('Quick cleanup: ' . $tempCleanup['cleaned'] . ' temp files removed (' . $sizeMB . ' MB freed)');
706 }
707
708 if ($orphanedProcesses['found']) {
709 Log::info('Quick cleanup: ' . count($orphanedProcesses['processes']) . ' orphaned process(es) terminated');
710 }
711
712 $duration = round(microtime(true) - $startTime, 2);
713 Log::info('Quick cleanup verification completed in ' . $duration . ' seconds');
714 } catch (\Exception $e) {
715 Log::warning('Quick cleanup verification failed: ' . $e->getMessage());
716 }
717 }
718}
$result
global $bearsamppBins
global $bearsamppLang
global $bearsamppRoot
global $bearsamppCore
$proc
Definition ajax.php:130
verifyServicesStoppedAndCleanup($services)
performQuickCleanupVerification($services)
generateCleanupReport($serviceVerification, $symlinkVerification, $tempCleanup, $orphanedProcesses)
static terminatePhpProcesses($excludePid, $window=null, $splash=null, $timeout=10)
getServiceDisplayName($sName, $service)
processWindow($window, $id, $ctrl, $param1, $param2)
const SERVICE_NAME
const QUIT
const EXIT_LEAVING_TEXT
const EXIT_REMOVE_SERVICE_TEXT
const EXIT_STOP_OTHER_PROCESS_TEXT
static info($data, $file=null)
static debug($data, $file=null)
static warning($data, $file=null)
static trace($data, $file=null)
static error($data, $file=null)
static getRootPath($aetrayPath=false)
static formatUnixPath($path)
static getTmpPath($aetrayPath=false)
static deleteFolder($path)
static killProcess($pid)
static getCurrentPid()
static getListProcs()
static killBins($refreshProcs=false)
static kill($pid)
const EXECUTABLE_PATH
const PROCESS_ID
const APP_TITLE
Definition root.php:19