Bearsampp 2026.3.26
API documentation
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 Util::logInfo('ActionQuit constructor called - starting exit process');
40 Util::logDebug('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 Util::logDebug('Splash screen initialized');
51
52 // Set handler for the splash screen window
53 $bearsamppWinbinder->setHandler( $this->splash->getWbWindow(), $this, 'processWindow', 2000 );
54 Util::logDebug('Window handler set, starting main loop');
55
56 $bearsamppWinbinder->mainLoop();
57 Util::logDebug('Main loop exited');
58
59 $bearsamppWinbinder->reset();
60 Util::logInfo('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 Util::logInfo('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 Util::logDebug('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 Util::logDebug('Service not found in available services: ' . $sName);
158 continue;
159 }
160
161 $service = $allServices[$sName];
162 $displayName = $this->getServiceDisplayName($sName, $service);
163
164 Util::logInfo('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 Util::logInfo('Successfully stopped and removed service: ' . $displayName);
174 } else {
175 Util::logWarning('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 Util::logWarning('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 Util::logInfo('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 // 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
233 public static function terminatePhpProcesses($excludePid, $window = null, $splash = null, $timeout = 10)
234 {
235 global $bearsamppWinbinder, $bearsamppCore;
236
237 $currentPid = Win32Ps::getCurrentPid();
238 $startTime = microtime(true);
239
240 Util::logTrace('Starting PHP process termination (excluding PID: ' . $excludePid . ')');
241
242 // Get list of loading PIDs to exclude from termination
243 $loadingPids = array();
244 if (file_exists($bearsamppCore->getLoadingPid())) {
245 $pids = file($bearsamppCore->getLoadingPid());
246 foreach ($pids as $pid) {
247 $loadingPids[] = intval(trim($pid));
248 }
249 Util::logTrace('Loading PIDs to preserve: ' . implode(', ', $loadingPids));
250 }
251
252 $targets = ['php-win.exe', 'php.exe'];
253 foreach (Win32Ps::getListProcs() as $proc) {
254 // Check if we've exceeded our timeout
255 if (microtime(true) - $startTime > $timeout) {
256 Util::logTrace('Process termination timeout exceeded, continuing with remaining operations');
257 break;
258 }
259
260 $exe = strtolower(basename($proc[Win32Ps::EXECUTABLE_PATH]));
262
263 // Skip if this is the excluded PID or a loading window PID
264 if (in_array($exe, $targets) && $pid != $excludePid && !in_array($pid, $loadingPids)) {
265 Util::logTrace('Terminating PHP process: ' . $pid);
266 Win32Ps::kill($pid);
267 usleep(100000); // 100ms delay between terminations
268 } elseif (in_array($pid, $loadingPids)) {
269 Util::logTrace('Preserving loading window process: ' . $pid);
270 }
271 }
272
273 // Initiate self-termination with timeout
274 if ($splash !== null) {
275 $splash->setTextLoading('Final cleanup...');
276 }
277
278 try {
279 Util::logTrace('Initiating self-termination for PID: ' . $currentPid);
280 // Add a timeout wrapper around the killProc call
281 $killSuccess = Vbs::killProc($currentPid);
282 if (!$killSuccess) {
283 Util::logTrace('Self-termination via Vbs::killProc failed, using alternative method');
284 }
285 } catch (\Exception $e) {
286 Util::logTrace('Exception during self-termination: ' . $e->getMessage());
287 }
288
289 // Destroy window after process termination
290 // Fix for PHP 8.2: Check if window is not null before destroying
291 if ($window && $bearsamppWinbinder) {
292 try {
293 Util::logTrace('Destroying window');
294 $bearsamppWinbinder->destroyWindow($window);
295 } catch (\Exception $e) {
296 Util::logTrace('Exception during window destruction: ' . $e->getMessage());
297 }
298 }
299
300 // Force exit if still running after timeout
301 if (microtime(true) - $startTime > $timeout * 1.5) {
302 Util::logTrace('Forcing exit due to timeout');
303 exit(0);
304 }
305 }
306
313 private function verifyServicesStoppedAndCleanup($services)
314 {
315 Util::logInfo('Verifying all services are stopped...');
316
317 $results = [
318 'all_stopped' => true,
319 'services' => [],
320 'still_running' => [],
321 'verification_failed' => []
322 ];
323
324 foreach ($services as $sName => $service) {
325 $displayName = $this->getServiceDisplayName($sName, $service);
326
327 try {
328 // Check if service is still installed/running
329 $isInstalled = $service->isInstalled();
330 $isRunning = $isInstalled ? $service->isRunning() : false;
331
332 $results['services'][$sName] = [
333 'name' => $displayName,
334 'installed' => $isInstalled,
335 'running' => $isRunning
336 ];
337
338 if ($isRunning) {
339 Util::logWarning('Service still running after shutdown: ' . $displayName);
340 $results['still_running'][] = $displayName;
341 $results['all_stopped'] = false;
342
343 // Attempt to force stop
344 Util::logInfo('Attempting to force stop: ' . $displayName);
345 $service->stop();
346 usleep(500000); // Wait 500ms
347
348 // Verify again
349 if ($service->isRunning()) {
350 Util::logError('Failed to force stop service: ' . $displayName);
351 } else {
352 Util::logInfo('Successfully force stopped service: ' . $displayName);
353 }
354 } elseif ($isInstalled) {
355 Util::logDebug('Service stopped but still installed: ' . $displayName);
356 } else {
357 Util::logDebug('Service verified stopped and removed: ' . $displayName);
358 }
359
360 } catch (\Exception $e) {
361 Util::logError('Failed to verify service status for ' . $displayName . ': ' . $e->getMessage());
362 $results['verification_failed'][] = $displayName;
363 $results['all_stopped'] = false;
364 }
365 }
366
367 if ($results['all_stopped']) {
368 Util::logInfo('All services verified stopped successfully');
369 } else {
370 Util::logWarning('Some services could not be verified as stopped');
371 }
372
373 return $results;
374 }
375
381 private function verifySymlinksRemoved()
382 {
383 global $bearsamppRoot;
384
385 Util::logInfo('Verifying symlinks are removed...');
386
387 $results = [
388 'success' => true,
389 'remaining' => []
390 ];
391
392 // Check common symlink locations
393 $symlinkPaths = [
394 $bearsamppRoot->getCurrentPath() . '/apache',
395 $bearsamppRoot->getCurrentPath() . '/php',
396 $bearsamppRoot->getCurrentPath() . '/mysql',
397 $bearsamppRoot->getCurrentPath() . '/mariadb',
398 $bearsamppRoot->getCurrentPath() . '/postgresql',
399 $bearsamppRoot->getCurrentPath() . '/nodejs',
400 $bearsamppRoot->getCurrentPath() . '/memcached',
401 $bearsamppRoot->getCurrentPath() . '/mailpit',
402 $bearsamppRoot->getCurrentPath() . '/xlight'
403 ];
404
405 foreach ($symlinkPaths as $path) {
406 if (file_exists($path) || is_link($path)) {
407 Util::logWarning('Symlink still exists: ' . $path);
408 $results['remaining'][] = basename($path);
409 $results['success'] = false;
410
411 // Attempt to remove it
412 try {
413 if (is_link($path)) {
414 @unlink($path);
415 } elseif (is_dir($path)) {
416 @rmdir($path);
417 }
418
419 // Verify removal
420 if (!file_exists($path)) {
421 Util::logInfo('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 Util::logError('Failed to remove symlink ' . $path . ': ' . $e->getMessage());
429 }
430 }
431 }
432
433 if ($results['success']) {
434 Util::logInfo('All symlinks verified removed');
435 } else {
436 Util::logWarning('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 Util::logInfo('Cleaning up temporary files...');
452
453 $results = [
454 'success' => true,
455 'cleaned' => 0,
456 'failed' => [],
457 'size_freed' => 0
458 ];
459
460 $tmpPath = $bearsamppCore->getTmpPath();
461
462 if (!is_dir($tmpPath)) {
463 Util::logDebug('Temp directory does not exist: ' . $tmpPath);
464 return $results;
465 }
466
467 try {
468 $files = glob($tmpPath . '/*');
469
470 if ($files === false) {
471 Util::logWarning('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_file($file)) {
486 if (@unlink($file)) {
487 $results['cleaned']++;
488 $results['size_freed'] += $size;
489 Util::logDebug('Removed temp file: ' . $basename);
490 } else {
491 $results['failed'][] = $basename;
492 $results['success'] = false;
493 Util::logWarning('Failed to remove temp file: ' . $basename);
494 }
495 } elseif (is_dir($file)) {
496 // Don't remove directories, just files
497 Util::logDebug('Skipping temp directory: ' . $basename);
498 }
499 } catch (\Exception $e) {
500 $results['failed'][] = $basename;
501 $results['success'] = false;
502 Util::logError('Error removing temp file ' . $basename . ': ' . $e->getMessage());
503 }
504 }
505
506 $sizeMB = round($results['size_freed'] / 1024 / 1024, 2);
507 Util::logInfo('Cleaned up ' . $results['cleaned'] . ' temporary files (' . $sizeMB . ' MB freed)');
508
509 if (!empty($results['failed'])) {
510 Util::logWarning('Failed to clean up ' . count($results['failed']) . ' files');
511 }
512
513 } catch (\Exception $e) {
514 Util::logError('Error during temp file cleanup: ' . $e->getMessage());
515 $results['success'] = false;
516 }
517
518 return $results;
519 }
520
526 private function checkForOrphanedProcesses()
527 {
528 global $bearsamppRoot;
529
530 Util::logInfo('Checking for orphaned processes...');
531
532 $orphaned = [
533 'found' => false,
534 'processes' => []
535 ];
536
537 try {
538 $procs = Win32Ps::getListProcs();
539 $bearsamppPath = strtolower(Util::formatUnixPath($bearsamppRoot->getRootPath()));
540 $currentPid = Win32Ps::getCurrentPid();
541
542 foreach ($procs as $proc) {
543 $exePath = strtolower(Util::formatUnixPath($proc[Win32Ps::EXECUTABLE_PATH]));
545
546 // Skip current process
547 if ($pid == $currentPid) {
548 continue;
549 }
550
551 // Check if process is from Bearsampp directory
552 if (strpos($exePath, $bearsamppPath) === 0) {
553 $processName = basename($exePath);
554
555 // Skip www directory processes (user applications)
556 if (strpos($exePath, $bearsamppPath . '/www/') === 0) {
557 continue;
558 }
559
560 // Skip the main Bearsampp executable
561 if (strtolower($processName) === 'bearsampp.exe') {
562 Util::logDebug('Skipping main Bearsampp process: ' . $processName . ' (PID: ' . $pid . ')');
563 continue;
564 }
565
566 // These are orphaned Bearsampp processes
567 $orphaned['found'] = true;
568 $orphaned['processes'][] = [
569 'pid' => $pid,
570 'name' => $processName,
571 'path' => $exePath
572 ];
573
574 Util::logWarning('Found orphaned process: ' . $processName . ' (PID: ' . $pid . ')');
575
576 // Attempt to kill orphaned process
577 try {
578 Win32Ps::kill($pid);
579 Util::logInfo('Terminated orphaned process: ' . $processName . ' (PID: ' . $pid . ')');
580 } catch (\Exception $e) {
581 Util::logError('Failed to terminate orphaned process ' . $processName . ': ' . $e->getMessage());
582 }
583 }
584 }
585
586 if (!$orphaned['found']) {
587 Util::logInfo('No orphaned processes found');
588 } else {
589 Util::logWarning('Found ' . count($orphaned['processes']) . ' orphaned process(es)');
590 }
591
592 } catch (\Exception $e) {
593 Util::logError('Error checking for orphaned processes: ' . $e->getMessage());
594 }
595
596 return $orphaned;
597 }
598
608 private function generateCleanupReport($serviceVerification, $symlinkVerification, $tempCleanup, $orphanedProcesses)
609 {
610 $report = [
611 'success' => true,
612 'warnings' => [],
613 'errors' => [],
614 'summary' => []
615 ];
616
617 // Service verification
618 if (!$serviceVerification['all_stopped']) {
619 $report['success'] = false;
620
621 if (!empty($serviceVerification['still_running'])) {
622 $report['errors'][] = 'Services still running: ' . implode(', ', $serviceVerification['still_running']);
623 }
624
625 if (!empty($serviceVerification['verification_failed'])) {
626 $report['warnings'][] = 'Could not verify status of: ' . implode(', ', $serviceVerification['verification_failed']);
627 }
628 }
629
630 $report['summary'][] = 'Services checked: ' . count($serviceVerification['services']);
631
632 // Symlink verification
633 if (!$symlinkVerification['success']) {
634 $report['warnings'][] = 'Symlinks not fully removed: ' . implode(', ', $symlinkVerification['remaining']);
635 }
636
637 // Temp file cleanup
638 if ($tempCleanup['cleaned'] > 0) {
639 $sizeMB = round($tempCleanup['size_freed'] / 1024 / 1024, 2);
640 $report['summary'][] = 'Temp files cleaned: ' . $tempCleanup['cleaned'] . ' (' . $sizeMB . ' MB)';
641 }
642
643 if (!empty($tempCleanup['failed'])) {
644 $report['warnings'][] = 'Failed to clean ' . count($tempCleanup['failed']) . ' temp file(s)';
645 }
646
647 // Orphaned processes
648 if ($orphanedProcesses['found']) {
649 $report['warnings'][] = 'Found ' . count($orphanedProcesses['processes']) . ' orphaned process(es)';
650 foreach ($orphanedProcesses['processes'] as $proc) {
651 $report['summary'][] = 'Orphaned: ' . $proc['name'] . ' (PID: ' . $proc['pid'] . ')';
652 }
653 }
654
655 return $report;
656 }
657
665 private function performQuickCleanupVerification($services)
666 {
667 Util::logInfo('Performing quick cleanup verification...');
668
669 $startTime = microtime(true);
670 $maxTime = 2; // Maximum 2 seconds for verification
671
672 try {
673 // Quick temp file cleanup (non-blocking)
674 $tempCleanup = $this->cleanupTemporaryFiles();
675
676 // Check if we're running out of time
677 if (microtime(true) - $startTime > $maxTime) {
678 Util::logDebug('Cleanup verification timeout reached, skipping remaining checks');
679 return;
680 }
681
682 // Quick orphaned process check (non-blocking)
683 $orphanedProcesses = $this->checkForOrphanedProcesses();
684
685 // Log summary
686 if ($tempCleanup['cleaned'] > 0) {
687 $sizeMB = round($tempCleanup['size_freed'] / 1024 / 1024, 2);
688 Util::logInfo('Quick cleanup: ' . $tempCleanup['cleaned'] . ' temp files removed (' . $sizeMB . ' MB freed)');
689 }
690
691 if ($orphanedProcesses['found']) {
692 Util::logInfo('Quick cleanup: ' . count($orphanedProcesses['processes']) . ' orphaned process(es) terminated');
693 }
694
695 $duration = round(microtime(true) - $startTime, 2);
696 Util::logInfo('Quick cleanup verification completed in ' . $duration . ' seconds');
697
698 } catch (\Exception $e) {
699 Util::logWarning('Quick cleanup verification failed: ' . $e->getMessage());
700 }
701 }
702}
$result
global $bearsamppBins
global $bearsamppLang
global $bearsamppRoot
global $bearsamppCore
$proc
Definition ajax.php:61
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 logError($data, $file=null)
static logTrace($data, $file=null)
static logInfo($data, $file=null)
static logDebug($data, $file=null)
static formatUnixPath($path)
static logWarning($data, $file=null)
static killProc($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