Bearsampp 2026.9.23
Loading...
Searching...
No Matches
class.servicehelper.php
Go to the documentation of this file.
1<?php
2/*
3 * Copyright (c) 2021-2026 Bearsampp
4 * License: GNU General Public License version 3 or later; see LICENSE.txt
5 * Author: Bear
6 * Website: https://bearsampp.com
7 * Github: https://github.com/Bearsampp
8 */
9
18{
24 private static $serviceMap = null;
25
31 private static $syntaxCheckMap = null;
32
40 private static function initializeMappings($bearsamppBins)
41 {
42 if (self::$serviceMap === null) {
43 self::$serviceMap = [
51 ];
52 }
53
54 if (self::$syntaxCheckMap === null) {
55 self::$syntaxCheckMap = [
59 ];
60 }
61 }
62
71 public static function getBinFromServiceName($serviceName, $bearsamppBins)
72 {
74
75 return isset(self::$serviceMap[$serviceName]) ? self::$serviceMap[$serviceName] : null;
76 }
77
86 public static function getSyntaxCheckCmd($serviceName, $bearsamppBins = null)
87 {
88 if ($bearsamppBins !== null) {
90 }
91
92 return isset(self::$syntaxCheckMap[$serviceName]) ? self::$syntaxCheckMap[$serviceName] : null;
93 }
94
103 public static function getServiceDisplayName($bin, $service)
104 {
105 return $bin->getName() . ' ' . $bin->getVersion() . ' (' . $service->getName() . ')';
106 }
107
116 public static function processServices($bearsamppBins, callable $callback)
117 {
119
120 foreach ($bearsamppBins->getServices() as $serviceName => $service) {
121 $bin = self::getBinFromServiceName($serviceName, $bearsamppBins);
122 $syntaxCheckCmd = self::getSyntaxCheckCmd($serviceName);
123
124 if ($bin !== null) {
125 $callback($serviceName, $service, $bin, $syntaxCheckCmd);
126 }
127 }
128 }
129
139 public static function startService($bin, $syntaxCheckCmd = null, $showWindow = false)
140 {
141 global $bearsamppLang, $bearsamppWinbinder;
142
143 if (method_exists($bin, 'initData')) {
144 $bin->initData();
145 }
146
147 $name = $bin->getName();
148 $service = $bin->getService();
149 $boxTitle = sprintf($bearsamppLang->getValue(Lang::START_SERVICE_TITLE), $name);
150
151 if (!$service->start()) {
152 $serviceError = sprintf($bearsamppLang->getValue(Lang::START_SERVICE_ERROR), $name);
153 $serviceErrorLog = sprintf('Error while starting the %s service', $name);
154 if (!empty($syntaxCheckCmd)) {
155 $cmdSyntaxCheck = $bin->getCmdLineOutput($syntaxCheckCmd);
156 if (!$cmdSyntaxCheck['syntaxOk']) {
157 $serviceError .= PHP_EOL . sprintf($bearsamppLang->getValue(Lang::STARTUP_SERVICE_SYNTAX_ERROR), $cmdSyntaxCheck['content']);
158 $serviceErrorLog .= sprintf(' (conf errors detected : %s)', $cmdSyntaxCheck['content']);
159 }
160 }
161 Log::error($serviceErrorLog);
162 if ($showWindow) {
163 $bearsamppWinbinder->messageBoxError($serviceError, $boxTitle);
164 }
165
166 return false;
167 }
168
169 return true;
170 }
171
179 public static function stopService($service)
180 {
181 return $service->stop();
182 }
183
191 public static function isPortInUse($port)
192 {
193 // Set localIP statically
194 $localIP = APP_LOCALHOST;
195
196 // Save current error reporting level
197 $errorReporting = error_reporting();
198
199 // Disable error reporting temporarily
200 error_reporting(0);
201
202 $connection = @fsockopen($localIP, $port);
203
204 // Restore original error reporting level
205 error_reporting($errorReporting);
206
207 if (is_resource($connection)) {
208 fclose($connection);
210
211 return $process != null ? $process : 'N/A';
212 }
213
214 return false;
215 }
216
227 public static function installService($bin, $port, $syntaxCheckCmd, $showWindow = false)
228 {
229 global $bearsamppLang, $bearsamppWinbinder;
230
231 if (method_exists($bin, 'initData')) {
232 $bin->initData();
233 }
234
235 $name = $bin->getName();
236 $service = $bin->getService();
237 $boxTitle = sprintf($bearsamppLang->getValue(Lang::INSTALL_SERVICE_TITLE), $name);
238
239 $isPortInUse = self::isPortInUse($port);
240 if ($isPortInUse === false) {
241 if (!$service->isInstalled()) {
242 $service->create();
243 if ($service->start()) {
244 Log::info(sprintf('%s service successfully installed. (name: %s ; port: %s)', $name, $service->getName(), $port));
245 if ($showWindow) {
246 $bearsamppWinbinder->messageBoxInfo(
247 sprintf($bearsamppLang->getValue(Lang::SERVICE_INSTALLED), $name, $service->getName(), $port),
248 $boxTitle
249 );
250 }
251
252 return true;
253 } else {
254 $serviceError = sprintf($bearsamppLang->getValue(Lang::SERVICE_INSTALL_ERROR), $name);
255 $serviceErrorLog = sprintf('Error during the installation of %s service', $name);
256 if (!empty($syntaxCheckCmd)) {
257 $cmdSyntaxCheck = $bin->getCmdLineOutput($syntaxCheckCmd);
258 if (!$cmdSyntaxCheck['syntaxOk']) {
259 $serviceError .= PHP_EOL . sprintf($bearsamppLang->getValue(Lang::STARTUP_SERVICE_SYNTAX_ERROR), $cmdSyntaxCheck['content']);
260 $serviceErrorLog .= sprintf(' (conf errors detected : %s)', $cmdSyntaxCheck['content']);
261 }
262 }
263 Log::error($serviceErrorLog);
264 if ($showWindow) {
265 $bearsamppWinbinder->messageBoxError($serviceError, $boxTitle);
266 }
267 }
268 } else {
269 Log::warning(sprintf('%s service already installed', $name));
270 if ($showWindow) {
271 $bearsamppWinbinder->messageBoxWarning(
272 sprintf($bearsamppLang->getValue(Lang::SERVICE_ALREADY_INSTALLED), $name),
273 $boxTitle
274 );
275 }
276
277 return true;
278 }
279 } elseif ($service->isRunning()) {
280 Log::warning(sprintf('%s service already installed and running', $name));
281 if ($showWindow) {
282 $bearsamppWinbinder->messageBoxWarning(
283 sprintf($bearsamppLang->getValue(Lang::SERVICE_ALREADY_INSTALLED), $name),
284 $boxTitle
285 );
286 }
287
288 return true;
289 } else {
290 Log::error(sprintf('Port %s is used by an other application : %s', $port, $isPortInUse));
291 if ($showWindow) {
292 $bearsamppWinbinder->messageBoxError(
293 sprintf($bearsamppLang->getValue(Lang::PORT_NOT_USED_BY), $port, $isPortInUse),
294 $boxTitle
295 );
296 }
297 }
298
299 return false;
300 }
301
310 public static function removeService($service, $name)
311 {
312 if (!($service instanceof Win32Service)) {
313 Log::error('$service not an instance of Win32Service');
314
315 return false;
316 }
317
318 if ($service->isInstalled()) {
319 if ($service->delete()) {
320 Log::info(sprintf('%s service successfully removed', $name));
321
322 return true;
323 } else {
324 Log::error(sprintf('Error during the uninstallation of %s service', $name));
325
326 return false;
327 }
328 } else {
329 Log::warning(sprintf('%s service does not exist', $name));
330 }
331
332 return true;
333 }
334
352
360 public static function hasSyntaxCheck($serviceName)
361 {
362 if (self::$syntaxCheckMap === null) {
363 // Initialize with dummy bins object if needed
364 self::$syntaxCheckMap = [
368 ];
369 }
370
371 return isset(self::$syntaxCheckMap[$serviceName]);
372 }
373
382 public static function getServicePort($serviceName, $bearsamppBins)
383 {
384 $bin = self::getBinFromServiceName($serviceName, $bearsamppBins);
385 if ($bin === null) {
386 return 0;
387 }
388
389 // Different services have different methods to get port
390 if (method_exists($bin, 'getPort')) {
391 return $bin->getPort();
392 } elseif (method_exists($bin, 'getSmtpPort')) {
393 return $bin->getSmtpPort();
394 }
395
396 return 0;
397 }
398
413 public static function stopAllServicesParallel($bearsamppBins, ?callable $progressCallback = null, $shutdownTimeout = 15)
414 {
415 Log::trace('ServiceHelper::stopAllServicesParallel() starting');
416 $startTime = microtime(true);
417
418 $services = $bearsamppBins->getServices();
419 if (empty($services)) {
420 Log::trace('No services to shut down');
421
422 return true;
423 }
424
425 Log::trace('Starting parallel shutdown of ' . count($services) . ' services');
426
427 // Try parallel shutdown
428 $parallelSuccess = self::shutdownServicesParallel($services, $progressCallback, $shutdownTimeout);
429
430 if ($parallelSuccess) {
431 $duration = round(microtime(true) - $startTime, 3);
432 Log::info('Parallel shutdown completed successfully in ' . $duration . 's');
433
434 return true;
435 }
436
437 // Fallback to sequential
438 Log::info('Parallel shutdown timed out, falling back to sequential shutdown');
439 $sequentialSuccess = self::shutdownServicesSequential($services, $progressCallback);
440
441 $totalDuration = round(microtime(true) - $startTime, 3);
442 Log::info('Sequential shutdown completed in ' . $totalDuration . 's');
443
444 return $sequentialSuccess;
445 }
446
460 private static function shutdownServicesParallel($services, ?callable $progressCallback = null, $shutdownTimeout = 15)
461 {
462 Log::trace('Phase 1: Sending stop commands to all services');
463 $totalServices = count($services);
464 $currentIndex = 0;
465
466 // Phase 1: Send stop commands (non-blocking)
467 foreach ($services as $serviceName => $service) {
468 $currentIndex++;
469 if ($progressCallback) {
470 $progressCallback($currentIndex, $totalServices, $serviceName);
471 }
472
473 Log::trace('Sending stop to: ' . $serviceName);
474 $service->stop();
475 }
476
477 // Phase 2: Monitor status
478 Log::trace('Phase 2: Monitoring service status');
479 $monitorStartTime = microtime(true);
480 $monitorTimeout = $shutdownTimeout;
481 $checkInterval = 0.5;
482 $allStopped = false;
483
484 while ((microtime(true) - $monitorStartTime) < $monitorTimeout) {
485 $allStopped = true;
486
487 foreach ($services as $serviceName => $service) {
488 if (!$service->isStopped()) {
489 $allStopped = false;
490 break;
491 }
492 }
493
494 if ($allStopped) {
495 Log::trace('All services stopped in parallel phase');
496
497 return true;
498 }
499
500 usleep($checkInterval * 1000000);
501 }
502
503 // Phase 3: Force kill remaining services
504 Log::trace('Phase 3: Force killing remaining services');
505 foreach ($services as $serviceName => $service) {
506 if (!$service->isStopped()) {
507 Log::trace('Force killing: ' . $serviceName);
508 self::forceKillService($serviceName);
509 }
510 }
511
512 // Final check
513 return self::allServicesStopped($services);
514 }
515
527 private static function shutdownServicesSequential($services, ?callable $progressCallback = null)
528 {
529 Log::trace('Starting sequential shutdown phase');
530 $totalServices = count($services);
531 $currentIndex = 0;
532
533 foreach ($services as $serviceName => $service) {
534 $currentIndex++;
535 if ($progressCallback) {
536 $progressCallback($currentIndex, $totalServices, $serviceName);
537 }
538
539 Log::trace('Sequential stop: ' . $serviceName);
540 $service->stop();
541
542 if (!$service->isStopped()) {
543 Log::trace('Force killing: ' . $serviceName);
544 self::forceKillService($serviceName);
545 }
546 }
547
548 return self::allServicesStopped($services);
549 }
550
558 private static function forceKillService($serviceName)
559 {
560 $processMap = [
561 BinApache::SERVICE_NAME => 'httpd.exe',
562 BinMysql::SERVICE_NAME => 'mysqld.exe',
563 BinMariadb::SERVICE_NAME => 'mysqld.exe',
564 BinMailpit::SERVICE_NAME => 'mailpit.exe',
565 BinMemcached::SERVICE_NAME => 'memcached.exe',
566 BinPostgresql::SERVICE_NAME => 'postgres.exe',
567 BinXlight::SERVICE_NAME => 'xlightftpd.exe',
568 'nodejs' => 'node.exe', // NodeJS - not a Windows service but needs to be killed on exit
569 ];
570
571 if (isset($processMap[$serviceName])) {
572 Log::trace('Killing process: ' . $processMap[$serviceName]);
573 Win32Ps::killBins([$processMap[$serviceName]]);
574
575 return true;
576 }
577
578 return false;
579 }
580
588 private static function allServicesStopped($services)
589 {
590 foreach ($services as $serviceName => $service) {
591 if (!$service->isStopped()) {
592 return false;
593 }
594 }
595
596 return true;
597 }
598
613 public static function startAllServicesParallel($serviceInfos, ?callable $progressCallback = null, $startupTimeout = 30)
614 {
615 Log::trace('ServiceHelper::startAllServicesParallel() starting with ' . count($serviceInfos) . ' services');
616 $startTime = microtime(true);
617
618 if (empty($serviceInfos)) {
619 Log::trace('No services to start');
620
621 return true;
622 }
623
624 Log::trace('Starting parallel startup of ' . count($serviceInfos) . ' services');
625
626 // Try parallel startup
627 $parallelSuccess = self::startServicesParallel($serviceInfos, $progressCallback, $startupTimeout);
628
629 if ($parallelSuccess) {
630 $duration = round(microtime(true) - $startTime, 3);
631 Log::info('Parallel startup completed successfully in ' . $duration . 's');
632
633 return true;
634 }
635
636 // Fallback to sequential
637 Log::info('Parallel startup timed out, falling back to sequential startup');
638 $sequentialSuccess = self::startServicesSequential($serviceInfos, $progressCallback);
639
640 $totalDuration = round(microtime(true) - $startTime, 3);
641 Log::info('Sequential startup completed in ' . $totalDuration . 's');
642
643 return $sequentialSuccess;
644 }
645
659 private static function startServicesParallel($serviceInfos, ?callable $progressCallback = null, $startupTimeout = 30)
660 {
661 Log::trace('Phase 1: Sending start commands to all services');
662 $totalServices = count($serviceInfos);
663 $currentIndex = 0;
664
665 // Phase 1: Send start commands (non-blocking)
666 foreach ($serviceInfos as $serviceName => $serviceInfo) {
667 $currentIndex++;
668 if ($progressCallback) {
669 $progressCallback($currentIndex, $totalServices, $serviceInfo['name']);
670 }
671
672 Log::trace('Sending start to: ' . $serviceName);
673 $service = $serviceInfo['service'];
674
675 // Start the service
676 $service->start();
677 }
678
679 // Phase 2: Monitor status
680 Log::trace('Phase 2: Monitoring service status');
681 $monitorStartTime = microtime(true);
682 $monitorTimeout = $startupTimeout;
683 $checkInterval = 0.5;
684 $allRunning = false;
685 $failedServices = [];
686
687 while ((microtime(true) - $monitorStartTime) < $monitorTimeout) {
688 $allRunning = true;
689
690 foreach ($serviceInfos as $serviceName => $serviceInfo) {
691 $service = $serviceInfo['service'];
692
693 if (!$service->isRunning()) {
694 $allRunning = false;
695 $failedServices[$serviceName] = $serviceInfo;
696 }
697 }
698
699 if ($allRunning) {
700 Log::trace('All services running in parallel phase');
701
702 return true;
703 }
704
705 usleep($checkInterval * 1000000);
706 }
707
708 // Phase 3: Retry failed services
709 if (!empty($failedServices)) {
710 Log::trace('Phase 3: Retrying ' . count($failedServices) . ' failed services');
711
712 foreach ($failedServices as $serviceName => $serviceInfo) {
713 $service = $serviceInfo['service'];
714
715 if (!$service->isRunning()) {
716 Log::trace('Retrying start for: ' . $serviceName);
717 $service->start();
718 usleep(500000); // 500ms delay between retries
719 }
720 }
721
722 // Final check
723 return self::allServicesRunning($serviceInfos);
724 }
725
726 return true;
727 }
728
740 private static function startServicesSequential($serviceInfos, ?callable $progressCallback = null)
741 {
742 Log::trace('Starting sequential startup phase');
743 $totalServices = count($serviceInfos);
744 $currentIndex = 0;
745
746 foreach ($serviceInfos as $serviceName => $serviceInfo) {
747 $currentIndex++;
748 if ($progressCallback) {
749 $progressCallback($currentIndex, $totalServices, $serviceInfo['name']);
750 }
751
752 Log::trace('Sequential start: ' . $serviceName);
753 $service = $serviceInfo['service'];
754
755 $service->start();
756
757 // Small delay between service starts
758 usleep(200000);
759
760 if (!$service->isRunning()) {
761 Log::trace('Service failed to start: ' . $serviceName);
762 // Retry once
763 $service->start();
764 usleep(500000);
765 }
766 }
767
768 return self::allServicesRunning($serviceInfos);
769 }
770
778 private static function allServicesRunning($serviceInfos)
779 {
780 foreach ($serviceInfos as $serviceName => $serviceInfo) {
781 $service = $serviceInfo['service'];
782 if (!$service->isRunning()) {
783 return false;
784 }
785 }
786
787 return true;
788 }
789}
global $bearsamppBins
global $bearsamppLang
$port
static getProcessUsingPort($port)
const CMD_SYNTAX_CHECK
const SERVICE_NAME
const CMD_SYNTAX_CHECK
const START_SERVICE_ERROR
const START_SERVICE_TITLE
const SERVICE_INSTALLED
const INSTALL_SERVICE_TITLE
const STARTUP_SERVICE_SYNTAX_ERROR
const SERVICE_INSTALL_ERROR
const SERVICE_ALREADY_INSTALLED
const PORT_NOT_USED_BY
static info($data, $file=null)
static warning($data, $file=null)
static trace($data, $file=null)
static error($data, $file=null)
static startAllServicesParallel($serviceInfos, ?callable $progressCallback=null, $startupTimeout=30)
static getServicePort($serviceName, $bearsamppBins)
static shutdownServicesSequential($services, ?callable $progressCallback=null)
static installService($bin, $port, $syntaxCheckCmd, $showWindow=false)
static forceKillService($serviceName)
static allServicesStopped($services)
static allServicesRunning($serviceInfos)
static startServicesParallel($serviceInfos, ?callable $progressCallback=null, $startupTimeout=30)
static removeService($service, $name)
static getServiceDisplayName($bin, $service)
static hasSyntaxCheck($serviceName)
static stopAllServicesParallel($bearsamppBins, ?callable $progressCallback=null, $shutdownTimeout=15)
static shutdownServicesParallel($services, ?callable $progressCallback=null, $shutdownTimeout=15)
static stopService($service)
static startService($bin, $syntaxCheckCmd=null, $showWindow=false)
static getSyntaxCheckCmd($serviceName, $bearsamppBins=null)
static isPortInUse($port)
static startServicesSequential($serviceInfos, ?callable $progressCallback=null)
static processServices($bearsamppBins, callable $callback)
static getBinFromServiceName($serviceName, $bearsamppBins)
static initializeMappings($bearsamppBins)
static killBins($refreshProcs=false)
const APP_LOCALHOST
Definition root.php:21