Bearsampp 2026.7.11
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
97 private function getServiceDisplayName($sName, $service)
98 {
99 global $bearsamppBins;
100
101 $name = '';
102
103 if ($sName == BinApache::SERVICE_NAME) {
104 $name = $bearsamppBins->getApache()->getName() . ' ' . $bearsamppBins->getApache()->getVersion();
105 }
106 elseif ($sName == BinMysql::SERVICE_NAME) {
107 $name = $bearsamppBins->getMysql()->getName() . ' ' . $bearsamppBins->getMysql()->getVersion();
108 }
109 elseif ($sName == BinMailpit::SERVICE_NAME) {
110 $name = $bearsamppBins->getMailpit()->getName() . ' ' . $bearsamppBins->getMailpit()->getVersion();
111 }
112 elseif ($sName == BinMariadb::SERVICE_NAME) {
113 $name = $bearsamppBins->getMariadb()->getName() . ' ' . $bearsamppBins->getMariadb()->getVersion();
114 }
115 elseif ($sName == BinPostgresql::SERVICE_NAME) {
116 $name = $bearsamppBins->getPostgresql()->getName() . ' ' . $bearsamppBins->getPostgresql()->getVersion();
117 }
118 elseif ($sName == BinMemcached::SERVICE_NAME) {
119 $name = $bearsamppBins->getMemcached()->getName() . ' ' . $bearsamppBins->getMemcached()->getVersion();
120 }
121 elseif ($sName == BinXlight::SERVICE_NAME) {
122 $name = $bearsamppBins->getXlight()->getName() . ' ' . $bearsamppBins->getXlight()->getVersion();
123 }
124
125 $name .= ' (' . $service->getName() . ')';
126 return $name;
127 }
128
139 public function processWindow($window, $id, $ctrl, $param1, $param2)
140 {
141 global $bearsamppBins, $bearsamppLang, $bearsamppWinbinder;
142
143 Log::info('Starting graceful shutdown process with optimized service order');
144
145 // Get all available services
146 $allServices = $bearsamppBins->getServices();
147
148 // Get optimal shutdown order
149 $shutdownOrder = $this->getServiceShutdownOrder();
150
151 Log::debug('Service shutdown order: ' . implode(' -> ', $shutdownOrder));
152
153 // Stop services in optimal order
154 foreach ($shutdownOrder as $sName) {
155 // Check if this service exists and is installed
156 if (!isset($allServices[$sName])) {
157 Log::debug('Service not found in available services: ' . $sName);
158 continue;
159 }
160
161 $service = $allServices[$sName];
162 $displayName = $this->getServiceDisplayName($sName, $service);
163
164 Log::info('Stopping service: ' . $displayName);
165
166 $this->splash->incrProgressBar();
167 $this->splash->setTextLoading(sprintf($bearsamppLang->getValue(Lang::EXIT_REMOVE_SERVICE_TEXT), $displayName));
168
169 // Delete (stop and remove) the service
170 $result = $service->delete();
171
172 if ($result) {
173 Log::info('Successfully stopped and removed service: ' . $displayName);
174 } else {
175 Log::warning('Failed to stop/remove service: ' . $displayName . ' (may not be installed)');
176 }
177 }
178
179 // Handle any services not in the shutdown order (for extensibility)
180 foreach ($allServices as $sName => $service) {
181 if (!in_array($sName, $shutdownOrder)) {
182 $displayName = $this->getServiceDisplayName($sName, $service);
183 Log::warning('Stopping unlisted service: ' . $displayName);
184
185 $this->splash->incrProgressBar();
186 $this->splash->setTextLoading(sprintf($bearsamppLang->getValue(Lang::EXIT_REMOVE_SERVICE_TEXT), $displayName));
187 $service->delete();
188 }
189 }
190
191 Log::info('All services stopped successfully');
192
193 // Purge "current" symlinks
194 $this->splash->setTextLoading('Removing symlinks...');
196
197 // Stop other processes
198 $this->splash->incrProgressBar();
199 $this->splash->setTextLoading($bearsamppLang->getValue(Lang::EXIT_STOP_OTHER_PROCESS_TEXT));
200 Win32Ps::killBins(true);
201
202 // Explicitly kill NodeJS if still running (it's not a Windows service)
203 Log::trace('Explicitly terminating NodeJS processes');
204 Win32Ps::killBins(['node.exe']);
205
206 // Perform cleanup verification in background (non-blocking)
207 $this->splash->setTextLoading('Performing cleanup verification...');
208 $this->performQuickCleanupVerification($allServices);
209
210 // Terminate any remaining processes
211 // Final termination sequence
212 $this->splash->setTextLoading('Completing shutdown...');
213 if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') {
214 $currentPid = Win32Ps::getCurrentPid();
215
216 // Terminate PHP processes with a timeout of 15 seconds
217 self::terminatePhpProcesses($currentPid, $window, $this->splash, 15);
218
219 // Force exit if still running
220 exit(0);
221 }
222
223 // Non-Windows fallback
224 $bearsamppWinbinder->destroyWindow($window);
225 exit(0);
226 }
227
237 public static function terminatePhpProcesses($excludePid, $window = null, $splash = null, $timeout = 10)
238 {
239 global $bearsamppWinbinder, $bearsamppCore;
240
241 $currentPid = Win32Ps::getCurrentPid();
242 $startTime = microtime(true);
243
244 Log::trace('Starting PHP process termination (excluding PID: ' . $excludePid . ')');
245
246 // Get list of loading PIDs to exclude from termination
247 $loadingPids = array();
248 if (file_exists($bearsamppCore->getLoadingPid())) {
249 $pids = file($bearsamppCore->getLoadingPid());
250 foreach ($pids as $pid) {
251 $loadingPids[] = intval(trim($pid));
252 }
253 Log::trace('Loading PIDs to preserve: ' . implode(', ', $loadingPids));
254 }
255
256 $targets = ['php-win.exe', 'php.exe'];
257 foreach (Win32Ps::getListProcs() as $proc) {
258 // Check if we've exceeded our timeout
259 if (microtime(true) - $startTime > $timeout) {
260 Log::trace('Process termination timeout exceeded, continuing with remaining operations');
261 break;
262 }
263
264 $exe = strtolower(basename($proc[Win32Ps::EXECUTABLE_PATH]));
266
267 // Skip if this is the excluded PID or a loading window PID
268 if (in_array($exe, $targets) && $pid != $excludePid && !in_array($pid, $loadingPids)) {
269 Log::trace('Terminating PHP process: ' . $pid);
270 Win32Ps::kill($pid);
271 usleep(100000); // 100ms delay between terminations
272 } elseif (in_array($pid, $loadingPids)) {
273 Log::trace('Preserving loading window process: ' . $pid);
274 }
275 }
276
277 // Initiate self-termination with timeout
278 if ($splash !== null) {
279 $splash->setTextLoading('Final cleanup...');
280 }
281
282 try {
283 Log::trace('Initiating self-termination for PID: ' . $currentPid);
284 // Add a timeout wrapper around the killProc call
285 $killSuccess = Win32Native::killProcess($currentPid);
286 if (!$killSuccess) {
287 Log::trace('Self-termination via Win32Native::killProcess failed, using alternative method');
288 }
289 } catch (\Exception $e) {
290 Log::trace('Exception during self-termination: ' . $e->getMessage());
291 }
292
293 // Destroy window after process termination
294 // Fix for PHP 8.2: Check if window is not null before destroying
295 if ($window && $bearsamppWinbinder) {
296 try {
297 Log::trace('Destroying window');
298 $bearsamppWinbinder->destroyWindow($window);
299 } catch (\Exception $e) {
300 Log::trace('Exception during window destruction: ' . $e->getMessage());
301 }
302 }
303
304 // Force exit if still running after timeout
305 if (microtime(true) - $startTime > $timeout * 1.5) {
306 Log::trace('Forcing exit due to timeout');
307 exit(0);
308 }
309 }
310
317 private function verifyServicesStoppedAndCleanup($services)
318 {
319 Log::info('Verifying all services are stopped...');
320
321 $results = [
322 'all_stopped' => true,
323 'services' => [],
324 'still_running' => [],
325 'verification_failed' => []
326 ];
327
328 foreach ($services as $sName => $service) {
329 $displayName = $this->getServiceDisplayName($sName, $service);
330
331 try {
332 // Check if service is still installed/running
333 $isInstalled = $service->isInstalled();
334 $isRunning = $isInstalled ? $service->isRunning() : false;
335
336 $results['services'][$sName] = [
337 'name' => $displayName,
338 'installed' => $isInstalled,
339 'running' => $isRunning
340 ];
341
342 if ($isRunning) {
343 Log::warning('Service still running after shutdown: ' . $displayName);
344 $results['still_running'][] = $displayName;
345 $results['all_stopped'] = false;
346
347 // Attempt to force stop
348 Log::info('Attempting to force stop: ' . $displayName);
349 $service->stop();
350 usleep(500000); // Wait 500ms
351
352 // Verify again
353 if ($service->isRunning()) {
354 Log::error('Failed to force stop service: ' . $displayName);
355 } else {
356 Log::info('Successfully force stopped service: ' . $displayName);
357 }
358 } elseif ($isInstalled) {
359 Log::debug('Service stopped but still installed: ' . $displayName);
360 } else {
361 Log::debug('Service verified stopped and removed: ' . $displayName);
362 }
363
364 } catch (\Exception $e) {
365 Log::error('Failed to verify service status for ' . $displayName . ': ' . $e->getMessage());
366 $results['verification_failed'][] = $displayName;
367 $results['all_stopped'] = false;
368 }
369 }
370
371 if ($results['all_stopped']) {
372 Log::info('All services verified stopped successfully');
373 } else {
374 Log::warning('Some services could not be verified as stopped');
375 }
376
377 return $results;
378 }
379
385 private function verifySymlinksRemoved()
386 {
387 global $bearsamppRoot;
388
389 Log::info('Verifying symlinks are removed...');
390
391 $results = [
392 'success' => true,
393 'remaining' => []
394 ];
395
396 // Check common symlink locations
397 $symlinkPaths = [
398 Path::getRootPath() . '/apache',
399 Path::getRootPath() . '/php',
400 Path::getRootPath() . '/mysql',
401 Path::getRootPath() . '/mariadb',
402 Path::getRootPath() . '/postgresql',
403 Path::getRootPath() . '/nodejs',
404 Path::getRootPath() . '/memcached',
405 Path::getRootPath() . '/mailpit',
406 Path::getRootPath() . '/xlight'
407 ];
408
409 foreach ($symlinkPaths as $path) {
410 if (file_exists($path) || is_link($path)) {
411 Log::warning('Symlink still exists: ' . $path);
412 $results['remaining'][] = basename($path);
413 $results['success'] = false;
414
415 // Attempt to remove it using robust method
416 try {
417 $removed = \Symlinks::safeRemoveSymlink($path);
418
419 // Verify removal
420 if ($removed && !file_exists($path) && !is_link($path)) {
421 Log::info('Successfully removed remaining symlink: ' . $path);
422 $results['remaining'] = array_diff($results['remaining'], [basename($path)]);
423 if (empty($results['remaining'])) {
424 $results['success'] = true;
425 }
426 }
427 } catch (\Exception $e) {
428 Log::error('Failed to remove symlink ' . $path . ': ' . $e->getMessage());
429 }
430 }
431 }
432
433 if ($results['success']) {
434 Log::info('All symlinks verified removed');
435 } else {
436 Log::warning('Some symlinks could not be removed: ' . implode(', ', $results['remaining']));
437 }
438
439 return $results;
440 }
441
447 private function cleanupTemporaryFiles()
448 {
449 global $bearsamppCore;
450
451 Log::info('Cleaning up temporary files...');
452
453 $results = [
454 'success' => true,
455 'cleaned' => 0,
456 'failed' => [],
457 'size_freed' => 0
458 ];
459
460 $tmpPath = Path::getTmpPath();
461
462 if (!is_dir($tmpPath)) {
463 Log::debug('Temp directory does not exist: ' . $tmpPath);
464 return $results;
465 }
466
467 try {
468 $files = glob($tmpPath . '/*');
469
470 if ($files === false) {
471 Log::warning('Failed to list temporary files');
472 return $results;
473 }
474
475 foreach ($files as $file) {
476 // Skip certain files that should be preserved
477 $basename = basename($file);
478 if (in_array($basename, ['.', '..', '.gitkeep', 'README.md'])) {
479 continue;
480 }
481
482 try {
483 $size = is_file($file) ? filesize($file) : 0;
484
485 if (is_link($file)) {
486 if (@unlink($file) || @rmdir($file)) {
487 $results['cleaned']++;
488 $results['size_freed'] += $size;
489 Log::debug('Removed temp symlink: ' . $basename);
490 } else {
491 $results['failed'][] = $basename;
492 $results['success'] = false;
493 Log::warning('Failed to remove temp symlink: ' . $basename);
494 }
495 } elseif (is_file($file)) {
496 if (@unlink($file)) {
497 $results['cleaned']++;
498 $results['size_freed'] += $size;
499 Log::debug('Removed temp file: ' . $basename);
500 } else {
501 $results['failed'][] = $basename;
502 $results['success'] = false;
503 Log::warning('Failed to remove temp file: ' . $basename);
504 }
505 } elseif (is_dir($file)) {
506 Util::deleteFolder($file);
507 if (!file_exists($file)) {
508 $results['cleaned']++;
509 Log::debug('Removed temp directory: ' . $basename);
510 } else {
511 $results['failed'][] = $basename;
512 $results['success'] = false;
513 Log::warning('Failed to remove temp directory: ' . $basename);
514 }
515 }
516 } catch (\Exception $e) {
517 $results['failed'][] = $basename;
518 $results['success'] = false;
519 Log::error('Error removing temp file ' . $basename . ': ' . $e->getMessage());
520 }
521 }
522
523 $sizeMB = round($results['size_freed'] / 1024 / 1024, 2);
524 Log::info('Cleaned up ' . $results['cleaned'] . ' temporary files (' . $sizeMB . ' MB freed)');
525
526 if (!empty($results['failed'])) {
527 Log::warning('Failed to clean up ' . count($results['failed']) . ' files');
528 }
529
530 } catch (\Exception $e) {
531 Log::error('Error during temp file cleanup: ' . $e->getMessage());
532 $results['success'] = false;
533 }
534
535 return $results;
536 }
537
543 private function checkForOrphanedProcesses()
544 {
545 global $bearsamppRoot;
546
547 Log::info('Checking for orphaned processes...');
548
549 $orphaned = [
550 'found' => false,
551 'processes' => []
552 ];
553
554 try {
555 $procs = Win32Ps::getListProcs();
556 $bearsamppPath = strtolower(Path::formatUnixPath(Path::getRootPath()));
557 $currentPid = Win32Ps::getCurrentPid();
558
559 foreach ($procs as $proc) {
560 $exePath = strtolower(Path::formatUnixPath($proc[Win32Ps::EXECUTABLE_PATH]));
562
563 // Skip current process
564 if ($pid == $currentPid) {
565 continue;
566 }
567
568 // Check if process is from Bearsampp directory
569 if (strpos($exePath, $bearsamppPath) === 0) {
570 $processName = basename($exePath);
571
572 // Skip www directory processes (user applications)
573 if (strpos($exePath, $bearsamppPath . '/www/') === 0) {
574 continue;
575 }
576
577 // Skip the main Bearsampp executable
578 if (strtolower($processName) === 'bearsampp.exe') {
579 Log::debug('Skipping main Bearsampp process: ' . $processName . ' (PID: ' . $pid . ')');
580 continue;
581 }
582
583 // These are orphaned Bearsampp processes
584 $orphaned['found'] = true;
585 $orphaned['processes'][] = [
586 'pid' => $pid,
587 'name' => $processName,
588 'path' => $exePath
589 ];
590
591 Log::warning('Found orphaned process: ' . $processName . ' (PID: ' . $pid . ')');
592
593 // Attempt to kill orphaned process
594 try {
595 Win32Ps::kill($pid);
596 Log::info('Terminated orphaned process: ' . $processName . ' (PID: ' . $pid . ')');
597 } catch (\Exception $e) {
598 Log::error('Failed to terminate orphaned process ' . $processName . ': ' . $e->getMessage());
599 }
600 }
601 }
602
603 if (!$orphaned['found']) {
604 Log::info('No orphaned processes found');
605 } else {
606 Log::warning('Found ' . count($orphaned['processes']) . ' orphaned process(es)');
607 }
608
609 } catch (\Exception $e) {
610 Log::error('Error checking for orphaned processes: ' . $e->getMessage());
611 }
612
613 return $orphaned;
614 }
615
625 private function generateCleanupReport($serviceVerification, $symlinkVerification, $tempCleanup, $orphanedProcesses)
626 {
627 $report = [
628 'success' => true,
629 'warnings' => [],
630 'errors' => [],
631 'summary' => []
632 ];
633
634 // Service verification
635 if (!$serviceVerification['all_stopped']) {
636 $report['success'] = false;
637
638 if (!empty($serviceVerification['still_running'])) {
639 $report['errors'][] = 'Services still running: ' . implode(', ', $serviceVerification['still_running']);
640 }
641
642 if (!empty($serviceVerification['verification_failed'])) {
643 $report['warnings'][] = 'Could not verify status of: ' . implode(', ', $serviceVerification['verification_failed']);
644 }
645 }
646
647 $report['summary'][] = 'Services checked: ' . count($serviceVerification['services']);
648
649 // Symlink verification
650 if (!$symlinkVerification['success']) {
651 $report['warnings'][] = 'Symlinks not fully removed: ' . implode(', ', $symlinkVerification['remaining']);
652 }
653
654 // Temp file cleanup
655 if ($tempCleanup['cleaned'] > 0) {
656 $sizeMB = round($tempCleanup['size_freed'] / 1024 / 1024, 2);
657 $report['summary'][] = 'Temp files cleaned: ' . $tempCleanup['cleaned'] . ' (' . $sizeMB . ' MB)';
658 }
659
660 if (!empty($tempCleanup['failed'])) {
661 $report['warnings'][] = 'Failed to clean ' . count($tempCleanup['failed']) . ' temp file(s)';
662 }
663
664 // Orphaned processes
665 if ($orphanedProcesses['found']) {
666 $report['warnings'][] = 'Found ' . count($orphanedProcesses['processes']) . ' orphaned process(es)';
667 foreach ($orphanedProcesses['processes'] as $proc) {
668 $report['summary'][] = 'Orphaned: ' . $proc['name'] . ' (PID: ' . $proc['pid'] . ')';
669 }
670 }
671
672 return $report;
673 }
674
682 private function performQuickCleanupVerification($services)
683 {
684 Log::info('Performing quick cleanup verification...');
685
686 $startTime = microtime(true);
687 $maxTime = 2; // Maximum 2 seconds for verification
688
689 try {
690 // Quick temp file cleanup (non-blocking)
691 $tempCleanup = $this->cleanupTemporaryFiles();
692
693 // Check if we're running out of time
694 if (microtime(true) - $startTime > $maxTime) {
695 Log::debug('Cleanup verification timeout reached, skipping remaining checks');
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
715 } catch (\Exception $e) {
716 Log::warning('Quick cleanup verification failed: ' . $e->getMessage());
717 }
718 }
719}
$result
global $bearsamppBins
global $bearsamppLang
global $bearsamppRoot
global $bearsamppCore
$proc
Definition ajax.php:45
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:13