Bearsampp 2026.9.23
Loading...
Searching...
No Matches
class.win32service.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
18{
19 // Win32Service Service Status Constants
27 const WIN32_SERVICE_NA = '0';
28
29 // Win32 Error Codes
59 const WIN32_NO_ERROR = '0';
60
63
64 const SERVICE_AUTO_START = '2';
66 const SERVICE_DISABLED = '4';
67
68 const PENDING_TIMEOUT = 20;
69 const SLEEP_TIME = 100000;
70
71 const VBS_NAME = 'Name';
72 const VBS_DISPLAY_NAME = 'DisplayName';
73 const VBS_DESCRIPTION = 'Description';
74 const VBS_PATH_NAME = 'PathName';
75 const SERVICE_STATE = 'State';
76
77 private $name;
78 private $displayName;
79 private $binPath;
80 private $params;
81 private $startType;
83 private $nssm;
84
86 private $latestError;
87
88 // Cache for service list to speed up bulk operations
89 private static $serviceListCache = null;
90
91 // Track which functions have been logged to avoid duplicate log entries
92 private static $loggedFunctions = array();
93
99 public function __construct($name)
100 {
101 Log::initClass($this);
102 $this->name = $name;
103 }
104
110 private function writeLog($log): void
111 {
112 global $bearsamppRoot;
114 }
115
121 public static function getVbsKeys(): array
122 {
123 return array(
124 self::VBS_NAME,
125 self::VBS_DISPLAY_NAME,
126 self::VBS_DESCRIPTION,
127 self::VBS_PATH_NAME,
128 self::SERVICE_STATE
129 );
130 }
131
140 public static function getServices($forceRefresh = false)
141 {
142 if (self::$serviceListCache === null || $forceRefresh) {
143 Log::trace('Fetching service list from Windows (COM/WMI)');
144 $startTime = microtime(true);
145 $services = Win32Native::listServices(self::getVbsKeys());
146
147 self::$serviceListCache = [];
148 if (is_array($services)) {
149 foreach ($services as $service) {
150 if (isset($service[self::VBS_NAME])) {
151 self::$serviceListCache[$service[self::VBS_NAME]] = $service;
152 }
153 }
154 }
155 $duration = round(microtime(true) - $startTime, 3);
156 Log::trace('Service list fetched in ' . $duration . 's');
157 }
158
159 return self::$serviceListCache;
160 }
161
171 private function callWin32Service($function, $param, $checkError = false): mixed
172 {
173 $result = false;
174 if (function_exists($function)) {
175 if (!isset(self::$loggedFunctions[$function])) {
176 Log::trace('Win32 function: ' . $function . ' exists');
177 self::$loggedFunctions[$function] = true;
178 }
179
180 // Special handling for win32_query_service_status to prevent hanging
181 if ($function === 'win32_query_service_status') {
182 Log::trace("Using enhanced handling for win32_query_service_status");
183
184 // Set a shorter timeout for this specific function
185 $originalTimeout = ini_get('max_execution_time');
186 set_time_limit(5); // 5 seconds timeout
187
188 try {
189 // Ensure proper parameter handling for PHP 8.5.7 compatibility
190 $result = call_user_func($function, $param);
191
192 // Reset the timeout
193 set_time_limit($originalTimeout);
194
195 if ($checkError && $result !== null) {
196 // Convert to int before using dechex for PHP 8.5.7 compatibility
197 $resultInt = is_numeric($result) ? (int)$result : 0;
198 if (dechex($resultInt) != self::WIN32_NO_ERROR) {
199 $this->latestError = dechex($resultInt);
200 }
201 }
202 } catch (\Win32ServiceException $e) {
203 // Reset the timeout
204 set_time_limit($originalTimeout);
205
206 Log::trace("Win32ServiceException caught: " . $e->getMessage());
207
208 // Handle "service does not exist" exception
209 if (strpos($e->getMessage(), 'service does not exist') !== false) {
210 Log::trace("Service does not exist exception handled for: " . $param);
211 // Return the appropriate error code for "service does not exist"
212 $result = hexdec(self::WIN32_ERROR_SERVICE_DOES_NOT_EXIST);
213 } else {
214 // For other exceptions, log and return false
215 Log::trace("Unhandled Win32ServiceException: " . $e->getMessage());
216 $result = false;
217 }
218 } catch (\Exception $e) {
219 // Reset the timeout
220 set_time_limit($originalTimeout);
221
222 // Catch any other exceptions to prevent application freeze
223 Log::trace("Exception caught in callWin32Service: " . $e->getMessage());
224 $result = false;
225 } catch (\Throwable $e) {
226 // Reset the timeout
227 set_time_limit($originalTimeout);
228
229 // Catch any other throwable (PHP 7+) to prevent application freeze
230 Log::trace("Throwable caught in callWin32Service: " . $e->getMessage());
231 $result = false;
232 }
233 } else {
234 // Standard handling for other functions
235 try {
236 // Ensure proper parameter handling for PHP 8.5.7 compatibility
237 $result = call_user_func($function, $param);
238 if ($checkError && $result !== null) {
239 // Convert to int before using dechex for PHP 8.5.7 compatibility
240 $resultInt = is_numeric($result) ? (int)$result : 0;
241 if (dechex($resultInt) != self::WIN32_NO_ERROR) {
242 $this->latestError = dechex($resultInt);
243 }
244 }
245 } catch (\Win32ServiceException $e) {
246 Log::trace("Win32ServiceException caught: " . $e->getMessage());
247
248 // Handle "service does not exist" exception
249 if (strpos($e->getMessage(), 'service does not exist') !== false) {
250 Log::trace("Service does not exist exception handled for: " . $param);
251 // Return the appropriate error code for "service does not exist"
252 $result = hexdec(self::WIN32_ERROR_SERVICE_DOES_NOT_EXIST);
253 } else {
254 // For other exceptions, log and return false
255 Log::trace("Unhandled Win32ServiceException: " . $e->getMessage());
256 $result = false;
257 }
258 } catch (\Exception $e) {
259 // Catch any other exceptions to prevent application freeze
260 Log::trace("Exception caught in callWin32Service: " . $e->getMessage());
261 $result = false;
262 } catch (\Throwable $e) {
263 // Catch any other throwable (PHP 7+) to prevent application freeze
264 Log::trace("Throwable caught in callWin32Service: " . $e->getMessage());
265 $result = false;
266 }
267 }
268 } else {
269 if (!isset(self::$loggedFunctions[$function])) {
270 Log::trace('Win32 function: ' . $function . ' missing');
271 self::$loggedFunctions[$function] = true;
272 }
273 }
274
275 return $result;
276 }
277
285 public function status($timeout = true): string
286 {
287 $this->latestStatus = self::WIN32_SERVICE_NA;
288 $maxtime = time() + self::PENDING_TIMEOUT;
289
290 Log::trace("Querying status for service: " . $this->getName() . " (timeout: " . ($timeout ? "enabled" : "disabled") . ")");
291 if ($timeout) {
292 Log::trace("Max timeout time set to: " . date('Y-m-d H:i:s', $maxtime));
293 }
294
295 // Add a safety counter to prevent infinite loops
296 $loopCount = 0;
297 $maxLoops = 5; // Maximum number of attempts
298 $startTime = microtime(true);
299
300 try {
301 while (($this->latestStatus == self::WIN32_SERVICE_NA || $this->isPending($this->latestStatus)) && $loopCount < $maxLoops) {
302 $loopCount++;
303 Log::trace("Calling win32_query_service_status for service: " . $this->getName() . " (attempt " . $loopCount . " of " . $maxLoops . ")");
304
305 // Add a timeout check before making the call
306 if (microtime(true) - $startTime > 10) { // 10 seconds overall timeout
307 Log::trace("Overall timeout reached before making service status call");
308 break;
309 }
310
311 $this->latestStatus = $this->callWin32Service('win32_query_service_status', $this->getName());
312
313 if (is_array($this->latestStatus) && isset($this->latestStatus['CurrentState'])) {
314 // Ensure proper type conversion for PHP 8.5.7 compatibility
315 $stateInt = is_numeric($this->latestStatus['CurrentState']) ? (int)$this->latestStatus['CurrentState'] : 0;
316 $this->latestStatus = dechex($stateInt);
317 Log::trace("Service status returned as array, CurrentState: " . $this->latestStatus);
318 } elseif ($this->latestStatus !== null) {
319 // Ensure proper type conversion for PHP 8.5.7 compatibility
320 $statusInt = is_numeric($this->latestStatus) ? (int)$this->latestStatus : 0;
321 $statusHex = dechex($statusInt);
322 Log::trace("Service status returned as value: " . $statusHex);
323
324 if ($statusHex == self::WIN32_ERROR_SERVICE_DOES_NOT_EXIST) {
325 $this->latestStatus = $statusHex;
326 Log::trace("Service does not exist, breaking loop");
327 break; // Exit the loop immediately if service doesn't exist
328 }
329 } else {
330 Log::trace("Service status query returned null");
331 // If we get a null result, assume service does not exist to avoid hanging
332 if ($loopCount >= 2) // Only do this after at least one retry
333 {
334 Log::trace("Multiple null results, assuming service does not exist");
335 $this->latestStatus = self::WIN32_ERROR_SERVICE_DOES_NOT_EXIST;
336 break;
337 }
338 }
339
340 if ($timeout && $maxtime < time()) {
341 Log::trace("Timeout reached while querying service status");
342 break;
343 }
344
345 // Only sleep if we're going to loop again
346 if ($loopCount < $maxLoops && ($this->latestStatus == self::WIN32_SERVICE_NA || $this->isPending($this->latestStatus))) {
347 Log::trace("Sleeping before next status check attempt");
348 usleep(self::SLEEP_TIME);
349 }
350 }
351 } catch (\Exception $e) {
352 Log::trace("Exception in status method: " . $e->getMessage());
353 // If an exception occurs, assume service does not exist
354 $this->latestStatus = self::WIN32_ERROR_SERVICE_DOES_NOT_EXIST;
355 } catch (\Throwable $e) {
356 Log::trace("Throwable in status method: " . $e->getMessage());
357 // If a throwable occurs, assume service does not exist
358 $this->latestStatus = self::WIN32_ERROR_SERVICE_DOES_NOT_EXIST;
359 }
360
361 if ($loopCount >= $maxLoops) {
362 Log::trace("Maximum query attempts reached for service: " . $this->getName());
363 }
364
365 $elapsedTime = microtime(true) - $startTime;
366 Log::trace("Status check completed in " . round($elapsedTime, 2) . " seconds after " . $loopCount . " attempts");
367
368 if ($this->latestStatus == self::WIN32_ERROR_SERVICE_DOES_NOT_EXIST) {
369 $this->latestError = $this->latestStatus;
370 $this->latestStatus = self::WIN32_SERVICE_NA;
371 Log::trace("Service does not exist, setting status to NA");
372 }
373
374 Log::trace("Final status for service " . $this->getName() . ": " . $this->latestStatus);
375
376 return $this->latestStatus;
377 }
378
384 public function create(): bool
385 {
386 global $bearsamppBins;
387
388 Log::trace("Starting Win32Service::create for service: " . $this->getName());
389
390 if ($this->getName() == BinPostgresql::SERVICE_NAME) {
391 Log::trace("PostgreSQL service detected - using specialized installation");
392 $bearsamppBins->getPostgresql()->rebuildConf();
393 Log::trace("PostgreSQL configuration rebuilt");
394
395 $bearsamppBins->getPostgresql()->initData();
396 Log::trace("PostgreSQL data initialized");
397
399 Log::trace("PostgreSQL service installation " . ($result ? "succeeded" : "failed"));
400
401 return $result;
402 }
403
404 if ($this->getNssm() instanceof Nssm) {
405 Log::trace("Using NSSM for service installation");
406
407 // Ensure Tools are loaded before building environment paths
408 global $bearsamppTools;
409 if (!isset($bearsamppTools)) {
410 Log::trace("Tools not loaded, forcing synchronous load");
412 }
413
414 global $bearsamppRegistry;
415 $nssmEnvPath = $bearsamppRegistry->getAppBinsRegKey(false);
416 Log::trace("NSSM environment path (bins): " . $nssmEnvPath);
417
418 $nssmEnvPath .= Path::getNssmEnvPaths();
419 Log::trace("NSSM environment path (with additional paths): " . $nssmEnvPath);
420
421 $nssmEnvPath .= '%SystemRoot%/system32;';
422 $nssmEnvPath .= '%SystemRoot%;';
423 $nssmEnvPath .= '%SystemRoot%/system32/Wbem;';
424 $nssmEnvPath .= '%SystemRoot%/system32/WindowsPowerShell/v1.0';
425 Log::trace("NSSM final environment PATH: " . $nssmEnvPath);
426
427 $this->getNssm()->setEnvironmentExtra('PATH=' . $nssmEnvPath);
428 Log::trace("NSSM service parameters:");
429 Log::trace("-> Name: " . $this->getNssm()->getName());
430 Log::trace("-> DisplayName: " . $this->getNssm()->getDisplayName());
431 Log::trace("-> BinPath: " . $this->getNssm()->getBinPath());
432 Log::trace("-> Params: " . $this->getNssm()->getParams());
433 Log::trace("-> Start: " . $this->getNssm()->getStart());
434 Log::trace("-> Stdout: " . $this->getNssm()->getStdout());
435 Log::trace("-> Stderr: " . $this->getNssm()->getStderr());
436
437 $result = $this->getNssm()->create();
438 Log::trace("NSSM service creation " . ($result ? "succeeded" : "failed"));
439 if (!$result) {
440 Log::trace("NSSM error: " . $this->getNssm()->getLatestError());
441 }
442
443 return $result;
444 }
445
446 Log::trace("Using win32_create_service for service installation");
447 $serviceParams = array(
448 'service' => $this->getName(),
449 'display' => $this->getDisplayName(),
450 'description' => $this->getDisplayName(),
451 'path' => $this->getBinPath(),
452 'params' => $this->getParams(),
453 'start_type' => $this->getStartType() != null ? $this->getStartType() : self::SERVICE_DEMAND_START,
454 'error_control' => $this->getErrorControl() != null ? $this->getErrorControl() : self::SERVER_ERROR_NORMAL,
455 );
456
457 Log::trace("win32_create_service parameters:");
458 foreach ($serviceParams as $key => $value) {
459 Log::trace("-> $key: $value");
460 }
461
462 $result = $this->callWin32Service('win32_create_service', $serviceParams, true);
463 // Ensure proper type conversion for PHP 8.5.7 compatibility
464 $resultInt = is_numeric($result) ? (int)$result : 0;
465 $create = $result !== null ? dechex($resultInt) : '0';
466 Log::trace("win32_create_service result code: " . $create);
467
468 // Retry once if the SCM has the service marked for deletion from a recent delete()
469 if ($create == self::WIN32_ERROR_SERVICE_MARKED_FOR_DELETE) {
470 Log::trace("Service marked for delete, waiting 2s before retry: " . $this->getName());
471 usleep(2000000);
472 $result = $this->callWin32Service('win32_create_service', $serviceParams, true);
473 $resultInt = is_numeric($result) ? (int)$result : 0;
474 $create = $result !== null ? dechex($resultInt) : '0';
475 Log::trace("win32_create_service retry result code: " . $create);
476 }
477
478 $this->writeLog('Create service: ' . $create . ' (status: ' . $this->status() . ')');
479 $this->writeLog('-> service: ' . $this->getName());
480 $this->writeLog('-> display: ' . $this->getDisplayName());
481 $this->writeLog('-> description: ' . $this->getDisplayName());
482 $this->writeLog('-> path: ' . $this->getBinPath());
483 $this->writeLog('-> params: ' . $this->getParams());
484 $this->writeLog('-> start_type: ' . ($this->getStartType() != null ? $this->getStartType() : self::SERVICE_DEMAND_START));
485 $this->writeLog('-> service: ' . ($this->getErrorControl() != null ? $this->getErrorControl() : self::SERVER_ERROR_NORMAL));
486
487 if ($create != self::WIN32_NO_ERROR) {
488 Log::trace("Service creation failed with error code: " . $create);
489
490 return false;
491 } elseif (!$this->isInstalled()) {
492 Log::trace("Service created but not found as installed");
493 $this->latestError = self::WIN32_NO_ERROR;
494
495 return false;
496 }
497
498 Log::trace("Service created successfully: " . $this->getName());
499
500 return true;
501 }
502
508 public function delete(): bool
509 {
510 Log::trace("Starting Win32Service::delete for service: " . $this->getName());
511 Log::trace("Checking if service is installed: " . $this->getName());
512
513 if (!$this->isInstalled()) {
514 Log::trace("Service is not installed, skipping deletion: " . $this->getName());
515
516 return true;
517 }
518
519 Log::trace("Stopping service before deletion: " . $this->getName());
520 $this->stop();
521
522 if ($this->getNssm() instanceof Nssm) {
523 $childExe = basename($this->getNssm()->getBinPath());
524 Log::trace("Killing NSSM child process after stop: " . $childExe);
525 Win32Ps::killBins([$childExe]);
526 }
527
528 if ($this->getName() == BinPostgresql::SERVICE_NAME) {
529 Log::trace("PostgreSQL service detected - using specialized uninstallation");
531 Log::trace("PostgreSQL service uninstallation " . ($result ? "succeeded" : "failed"));
532
533 return $result;
534 }
535
536 Log::trace("Calling win32_delete_service for service: " . $this->getName());
537 $result = $this->callWin32Service('win32_delete_service', $this->getName(), true);
538 // Ensure proper type conversion for PHP 8.5.7 compatibility
539 $resultInt = is_numeric($result) ? (int)$result : 0;
540 $delete = $result !== null ? dechex($resultInt) : '0';
541 Log::trace("Delete service result code: " . $delete);
542 $this->writeLog('Delete service ' . $this->getName() . ': ' . $delete . ' (status: ' . $this->status() . ')');
543
544 if ($delete != self::WIN32_NO_ERROR && $delete != self::WIN32_ERROR_SERVICE_DOES_NOT_EXIST) {
545 return false;
546 } elseif ($this->isInstalled()) {
547 $this->latestError = self::WIN32_NO_ERROR;
548
549 return false;
550 }
551
552 return true;
553 }
554
560 public function reset(): bool
561 {
562 if ($this->delete()) {
563 usleep(self::SLEEP_TIME);
564
565 return $this->create();
566 }
567
568 return false;
569 }
570
576 public function start(): bool
577 {
578 global $bearsamppBins;
579
580 Log::info('Attempting to start service: ' . $this->getName());
581
582 if ($this->getName() == BinMysql::SERVICE_NAME) {
583 $bearsamppBins->getMysql()->initData();
584 } elseif ($this->getName() == BinMariadb::SERVICE_NAME) {
585 $bearsamppBins->getMariadb()->initData();
586 } elseif ($this->getName() == BinMailpit::SERVICE_NAME) {
587 $bearsamppBins->getMailpit()->rebuildConf();
588 } elseif ($this->getName() == BinMemcached::SERVICE_NAME) {
589 $bearsamppBins->getMemcached()->rebuildConf();
590 } elseif ($this->getName() == BinPostgresql::SERVICE_NAME) {
591 $bearsamppBins->getPostgresql()->rebuildConf();
592 $bearsamppBins->getPostgresql()->initData();
593 } elseif ($this->getName() == BinXlight::SERVICE_NAME) {
594 $bearsamppBins->getXlight()->rebuildConf();
595 }
596
597
598 $result = $this->callWin32Service('win32_start_service', $this->getName(), true);
599 // Ensure proper type conversion for PHP 8.5.7 compatibility
600 $resultInt = is_numeric($result) ? (int)$result : 0;
601 $start = $result !== null ? dechex($resultInt) : '0';
602 Log::debug('Start service ' . $this->getName() . ': ' . $start . ' (status: ' . $this->status() . ')');
603
604 if ($start != self::WIN32_NO_ERROR && $start != self::WIN32_ERROR_SERVICE_ALREADY_RUNNING) {
605 // Write error to log
606 Log::error('Failed to start service: ' . $this->getName() . ' with error code: ' . $start);
607
608 if ($this->getName() == BinApache::SERVICE_NAME) {
609 $cmdOutput = $bearsamppBins->getApache()->getCmdLineOutput(BinApache::CMD_SYNTAX_CHECK);
610 if (!$cmdOutput['syntaxOk']) {
611 file_put_contents(
612 $bearsamppBins->getApache()->getErrorLog(),
613 '[' . date('Y-m-d H:i:s', time()) . '] [error] ' . $cmdOutput['content'] . PHP_EOL,
614 FILE_APPEND
615 );
616 }
617 } elseif ($this->getName() == BinMysql::SERVICE_NAME) {
618 $cmdOutput = $bearsamppBins->getMysql()->getCmdLineOutput(BinMysql::CMD_SYNTAX_CHECK);
619 if (!$cmdOutput['syntaxOk']) {
620 file_put_contents(
621 $bearsamppBins->getMysql()->getErrorLog(),
622 '[' . date('Y-m-d H:i:s', time()) . '] [error] ' . $cmdOutput['content'] . PHP_EOL,
623 FILE_APPEND
624 );
625 }
626 } elseif ($this->getName() == BinMariadb::SERVICE_NAME) {
627 $cmdOutput = $bearsamppBins->getMariadb()->getCmdLineOutput(BinMariadb::CMD_SYNTAX_CHECK);
628 if (!$cmdOutput['syntaxOk']) {
629 file_put_contents(
630 $bearsamppBins->getMariadb()->getErrorLog(),
631 '[' . date('Y-m-d H:i:s', time()) . '] [error] ' . $cmdOutput['content'] . PHP_EOL,
632 FILE_APPEND
633 );
634 }
635 }
636
637 return false;
638 }
639
640 // Wait for the service to actually start before checking if it's running
641 // We use a timeout to avoid hanging if the service fails to start properly
642 $maxtime = time() + self::PENDING_TIMEOUT;
643 while ($this->isPending($this->status(false)) && time() < $maxtime) {
644 usleep(self::SLEEP_TIME);
645 }
646
647 if (!$this->isRunning()) {
648 $this->latestError = self::WIN32_NO_ERROR;
649 Log::error('Service ' . $this->getName() . ' is not running after start attempt (status: ' . $this->status() . ').');
650 $this->latestError = null;
651
652 return false;
653 }
654
655 Log::info('Service ' . $this->getName() . ' started successfully.');
656
657 return true;
658 }
659
665 public function stop(): bool
666 {
667 Log::trace("Starting Win32Service::stop for service: " . $this->getName());
668
669 Log::trace("Calling win32_stop_service for service: " . $this->getName());
670 $result = $this->callWin32Service('win32_stop_service', $this->getName(), true);
671
672 // Ensure proper type conversion for PHP 8.5.7 compatibility
673 $resultInt = is_numeric($result) ? (int)$result : 0;
674 $stop = $result !== null ? dechex($resultInt) : '0';
675 Log::trace("Stop service result code: " . $stop);
676
677 Log::trace("Checking current status after stop attempt");
678 $currentStatus = $this->status();
679 Log::trace("Current status: " . $currentStatus);
680
681 $this->writeLog('Stop service ' . $this->getName() . ': ' . $stop . ' (status: ' . $currentStatus . ')');
682
683 if ($stop != self::WIN32_NO_ERROR) {
684 return false;
685 }
686
687 // Wait for the service to actually stop before checking if it's stopped
688 $maxtime = time() + self::PENDING_TIMEOUT;
689 while ($this->isPending($this->status(false)) && time() < $maxtime) {
690 usleep(self::SLEEP_TIME);
691 }
692
693 if (!$this->isStopped()) {
694 $this->latestError = self::WIN32_NO_ERROR;
695 Log::error('Service ' . $this->getName() . ' is still running after stop attempt (status: ' . $this->status() . ').');
696 $this->latestError = null;
697
698 return false;
699 }
700
701 return true;
702 }
703
709 public function restart(): bool
710 {
711 if ($this->stop()) {
712 return $this->start();
713 }
714
715 return false;
716 }
717
724 public function fastServiceCheck()
725 {
726 Log::trace("Starting fastServiceCheck for service: " . $this->getName());
727
728 $startTime = microtime(true);
729
730 // Use sc.exe to query service - this is very fast and reliable
731 // Execute with hidden window to prevent command prompt flash
732 Log::trace("Executing: sc query " . $this->getName());
733
734 $output = CommandRunner::execCombined('sc', ['query', $this->getName()]);
735 $duration = round(microtime(true) - $startTime, 3);
736
737 Log::trace("sc.exe query completed in " . $duration . "s");
738
739 if ($output === null || $output === false) {
740 Log::trace("sc.exe returned null/false, service likely doesn't exist");
741
742 return false;
743 }
744
745 // Check if service doesn't exist
746 if (stripos($output, 'does not exist') !== false ||
747 stripos($output, 'FAILED') !== false ||
748 stripos($output, '1060') !== false) { // Error code 1060 = service doesn't exist
749 Log::trace("Service doesn't exist: " . $this->getName());
750
751 return false;
752 }
753
754 // Service exists - parse basic info
755 $serviceInfo = [];
756
757 // Extract service name
758 if (preg_match('/SERVICE_NAME:\s*(.+)/i', $output, $matches)) {
759 $serviceInfo[self::VBS_NAME] = trim($matches[1]);
760 }
761
762 // Extract display name
763 if (preg_match('/DISPLAY_NAME:\s*(.+)/i', $output, $matches)) {
764 $serviceInfo[self::VBS_DISPLAY_NAME] = trim($matches[1]);
765 }
766
767 // Extract state
768 if (preg_match('/STATE\s*:\s*\d+\s+(\w+)/i', $output, $matches)) {
769 $state = trim($matches[1]);
770 $serviceInfo[self::SERVICE_STATE] = $state;
771 Log::trace("Service state: " . $state);
772 }
773
774 // If we have basic info, service exists - get full details if needed
775 if (!empty($serviceInfo)) {
776 Log::trace("Service exists, getting full details");
777
778 // Use sc qc to get configuration details (including path)
779 $configOutput = CommandRunner::execCombined('sc', ['qc', $this->getName()]);
780
781 if ($configOutput !== null && $configOutput !== false && preg_match('/BINARY_PATH_NAME\s*:\s*(.+)/i', $configOutput, $matches)) {
782 $serviceInfo[self::VBS_PATH_NAME] = trim($matches[1]);
783 Log::trace("Service path: " . $serviceInfo[self::VBS_PATH_NAME]);
784 }
785
786 // Get description if available
787 if ($configOutput !== null && $configOutput !== false && preg_match('/DISPLAY_NAME\s*:\s*(.+)/i', $configOutput, $matches)) {
788 $serviceInfo[self::VBS_DESCRIPTION] = trim($matches[1]);
789 }
790
791 Log::trace("Fast service check successful for: " . $this->getName());
792
793 return $serviceInfo;
794 }
795
796 Log::trace("Could not parse service info from sc.exe output");
797
798 return false;
799 }
800
807 public function infos()
808 {
809 Log::trace("Starting Win32Service::infos for service: " . $this->getName());
810
811 try {
812 // Set a timeout for the entire operation
813 $startTime = microtime(true);
814 $timeout = 10; // 10 seconds timeout for the entire operation
815
816 if ($this->getNssm() instanceof Nssm) {
817 Log::trace("Using NSSM to get service info");
818 $result = $this->getNssm()->infos();
819 Log::trace("NSSM info retrieval completed in " . round(microtime(true) - $startTime, 2) . " seconds");
820
821 return $result;
822 }
823
824 // Performance optimization: Try fast sc.exe check first
825 Log::trace("Attempting fast service check using sc.exe");
826 $fastResult = $this->fastServiceCheck();
827
828 if ($fastResult !== false) {
829 $duration = round(microtime(true) - $startTime, 3);
830 Log::trace("Fast service check succeeded in " . $duration . "s (saved 5-10s)");
831 Log::debug("Performance: Fast service check used for " . $this->getName() . ", saved 5-10 seconds");
832
833 return $fastResult;
834 }
835
836 // Fast check returned false - service doesn't exist
837 if ($fastResult === false) {
838 $duration = round(microtime(true) - $startTime, 3);
839 Log::trace("Fast service check determined service doesn't exist in " . $duration . "s");
840
841 return false;
842 }
843
844 // Fallback to VBS (should rarely be needed now)
845 Log::trace("Falling back to VBS for service info");
846
847 // Use set_time_limit to prevent PHP script timeout
848 $originalTimeout = ini_get('max_execution_time');
849 set_time_limit(15); // 15 seconds timeout
850
851 // Create a separate process to get service info with a timeout
853
854 // Reset the timeout
855 set_time_limit($originalTimeout);
856
857 // Check if we've exceeded our timeout
858 if (microtime(true) - $startTime > $timeout) {
859 Log::trace("Timeout exceeded in infos() method, returning false");
860
861 return false;
862 }
863
864 Log::trace("VBS info retrieval completed in " . round(microtime(true) - $startTime, 2) . " seconds");
865
866 return $result;
867 } catch (\Exception $e) {
868 Log::trace("Exception in infos() method: " . $e->getMessage() . ", returning false");
869
870 return false;
871 } catch (\Throwable $e) {
872 Log::trace("Throwable in infos() method: " . $e->getMessage() . ", returning false");
873
874 return false;
875 }
876 }
877
883 public function isInstalled(): bool
884 {
885 Log::trace("Checking if service is installed: " . $this->getName());
886
887 try {
888 // Set a timeout for the entire operation
889 $startTime = microtime(true);
890 $timeout = 15; // 15 seconds timeout for the entire operation
891
892 // Call status() with a try-catch to ensure we don't get stuck
893 $status = $this->status();
894
895 // Check if we've exceeded our timeout
896 if (microtime(true) - $startTime > $timeout) {
897 Log::trace("Timeout exceeded in isInstalled() method, assuming service is not installed");
898 $this->writeLog('isInstalled ' . $this->getName() . ': NO (timeout exceeded)');
899
900 return false;
901 }
902
903 $isInstalled = $status != self::WIN32_SERVICE_NA;
904
905 Log::trace("Service " . $this->getName() . " installation status: " . ($isInstalled ? "YES" : "NO") . " (status code: " . $status . ")");
906 $this->writeLog('isInstalled ' . $this->getName() . ': ' . ($isInstalled ? 'YES' : 'NO') . ' (status: ' . $status . ')');
907
908 return $isInstalled;
909 } catch (\Exception $e) {
910 Log::trace("Exception in isInstalled() method: " . $e->getMessage() . ", assuming service is not installed");
911 $this->writeLog('isInstalled ' . $this->getName() . ': NO (exception: ' . $e->getMessage() . ')');
912
913 return false;
914 } catch (\Throwable $e) {
915 Log::trace("Throwable in isInstalled() method: " . $e->getMessage() . ", assuming service is not installed");
916 $this->writeLog('isInstalled ' . $this->getName() . ': NO (throwable: ' . $e->getMessage() . ')');
917
918 return false;
919 }
920 }
921
927 public function isRunning(): bool
928 {
929 Log::trace("Checking if service is running: " . $this->getName());
930
931 $status = $this->status();
932 $isRunning = $status == self::WIN32_SERVICE_RUNNING;
933
934 Log::trace("Service " . $this->getName() . " running status: " . ($isRunning ? "YES" : "NO") . " (status code: " . $status . ")");
935 $this->writeLog('isRunning ' . $this->getName() . ': ' . ($isRunning ? 'YES' : 'NO') . ' (status: ' . $status . ')');
936
937 return $isRunning;
938 }
939
945 public function isStopped(): bool
946 {
947 Log::trace("Checking if service is stopped: " . $this->getName());
948
949 $status = $this->status();
950 $isStopped = $status == self::WIN32_SERVICE_STOPPED;
951
952 Log::trace("Service " . $this->getName() . " stopped status: " . ($isStopped ? "YES" : "NO") . " (status code: " . $status . ")");
953 $this->writeLog('isStopped ' . $this->getName() . ': ' . ($isStopped ? 'YES' : 'NO') . ' (status: ' . $status . ')');
954
955 return $isStopped;
956 }
957
963 public function isPaused(): bool
964 {
965 Log::trace("Checking if service is paused: " . $this->getName());
966
967 $status = $this->status();
968 $isPaused = $status == self::WIN32_SERVICE_PAUSED;
969
970 Log::trace("Service " . $this->getName() . " paused status: " . ($isPaused ? "YES" : "NO") . " (status code: " . $status . ")");
971 $this->writeLog('isPaused ' . $this->getName() . ': ' . ($isPaused ? 'YES' : 'NO') . ' (status: ' . $status . ')');
972
973 return $isPaused;
974 }
975
983 public function isPending($status): bool
984 {
985 $isPending = $status == self::WIN32_SERVICE_START_PENDING || $status == self::WIN32_SERVICE_STOP_PENDING
986 || $status == self::WIN32_SERVICE_CONTINUE_PENDING || $status == self::WIN32_SERVICE_PAUSE_PENDING;
987
988 Log::trace("Checking if status is pending: " . $status . " - Result: " . ($isPending ? "YES" : "NO"));
989
990 if ($isPending) {
991 if ($status == self::WIN32_SERVICE_START_PENDING) {
992 Log::trace("Service is in START_PENDING state");
993 } else {
994 if ($status == self::WIN32_SERVICE_STOP_PENDING) {
995 Log::trace("Service is in STOP_PENDING state");
996 } else {
997 if ($status == self::WIN32_SERVICE_CONTINUE_PENDING) {
998 Log::trace("Service is in CONTINUE_PENDING state");
999 } else {
1000 if ($status == self::WIN32_SERVICE_PAUSE_PENDING) {
1001 Log::trace("Service is in PAUSE_PENDING state");
1002 }
1003 }
1004 }
1005 }
1006 }
1007
1008 return $isPending;
1009 }
1010
1018 private function getWin32ServiceStatusDesc($status): ?string
1019 {
1020 switch ($status) {
1021 case self::WIN32_SERVICE_CONTINUE_PENDING:
1022 return 'The service continue is pending.';
1023
1024 case self::WIN32_SERVICE_PAUSE_PENDING:
1025 return 'The service pause is pending.';
1026
1027 case self::WIN32_SERVICE_PAUSED:
1028 return 'The service is paused.';
1029
1030 case self::WIN32_SERVICE_RUNNING:
1031 return 'The service is running.';
1032
1033 case self::WIN32_SERVICE_START_PENDING:
1034 return 'The service is starting.';
1035
1036 case self::WIN32_SERVICE_STOP_PENDING:
1037 return 'The service is stopping.';
1038
1039 case self::WIN32_SERVICE_STOPPED:
1040 return 'The service is not running.';
1041
1042 case self::WIN32_SERVICE_NA:
1043 return 'Cannot retrieve service status.';
1044
1045 default:
1046 return null;
1047 }
1048 }
1049
1057 private function getWin32ErrorCodeDesc($code): ?string
1058 {
1059 switch ($code) {
1060 case self::WIN32_ERROR_ACCESS_DENIED:
1061 return 'The handle to the SCM database does not have the appropriate access rights.';
1062 // ... other cases ...
1063 default:
1064 return null;
1065 }
1066 }
1067
1073 public function getName(): string
1074 {
1075 return $this->name;
1076 }
1077
1083 public function setName($name): void
1084 {
1085 $this->name = $name;
1086 }
1087
1093 public function getDisplayName(): string
1094 {
1095 return $this->displayName;
1096 }
1097
1103 public function setDisplayName($displayName): void
1104 {
1105 $this->displayName = $displayName;
1106 }
1107
1113 public function getBinPath(): string
1114 {
1115 return $this->binPath;
1116 }
1117
1123 public function setBinPath($binPath): void
1124 {
1125 $this->binPath = str_replace('"', '', Path::formatWindowsPath($binPath));
1126 }
1127
1133 public function getParams(): string
1134 {
1135 return $this->params;
1136 }
1137
1143 public function setParams($params): void
1144 {
1145 $this->params = $params;
1146 }
1147
1153 public function getStartType(): string
1154 {
1155 return $this->startType;
1156 }
1157
1163 public function setStartType($startType): void
1164 {
1165 $this->startType = $startType;
1166 }
1167
1173 public function getErrorControl(): string
1174 {
1175 return $this->errorControl;
1176 }
1177
1183 public function setErrorControl($errorControl): void
1184 {
1185 $this->errorControl = $errorControl;
1186 }
1187
1193 public function getNssm()
1194 {
1195 return $this->nssm;
1196 }
1197
1203 public function setNssm($nssm)
1204 {
1205 if ($nssm instanceof Nssm) {
1206 $this->setDisplayName($nssm->getDisplayName());
1207 $this->setBinPath($nssm->getBinPath());
1208 $this->setParams($nssm->getParams());
1209 $this->setStartType($nssm->getStart());
1210 $this->nssm = $nssm;
1211 }
1212 }
1213
1219 public function getLatestStatus()
1220 {
1221 return $this->latestStatus;
1222 }
1223
1229 public function getLatestError()
1230 {
1231 return $this->latestError;
1232 }
1233
1239 public function getError()
1240 {
1241 global $bearsamppLang;
1242 if ($this->latestError != self::WIN32_NO_ERROR) {
1243 // Ensure proper type conversion for PHP 8.5.7 compatibility
1244 $errorInt = is_numeric($this->latestError) ? hexdec($this->latestError) : 0;
1245
1246 return $bearsamppLang->getValue(Lang::ERROR) . ' ' .
1247 $this->latestError . ' (' . $errorInt . ' : ' . $this->getWin32ErrorCodeDesc($this->latestError) . ')';
1248 } elseif ($this->latestStatus != self::WIN32_SERVICE_NA) {
1249 // Ensure proper type conversion for PHP 8.5.7 compatibility
1250 $statusInt = is_numeric($this->latestStatus) ? hexdec($this->latestStatus) : 0;
1251
1252 return $bearsamppLang->getValue(Lang::STATUS) . ' ' .
1253 $this->latestStatus . ' (' . $statusInt . ' : ' . $this->getWin32ServiceStatusDesc($this->latestStatus) . ')';
1254 }
1255
1256 return null;
1257 }
1258
1268 public function waitForServiceDeletion($maxWaitTime = 30): bool
1269 {
1270 $startTime = time();
1271 $maxTime = $startTime + $maxWaitTime;
1272 $checkCount = 0;
1273
1274 Log::trace("Waiting for service deletion: " . $this->getName() . " (max wait: " . $maxWaitTime . "s)");
1275
1276 while (time() < $maxTime) {
1277 $checkCount++;
1278 $status = $this->status(false);
1279 Log::trace("Service deletion check #" . $checkCount . " - Status: " . $status . " at " . date('Y-m-d H:i:s'));
1280
1281 // Service doesn't exist or is definitely not there
1282 if ($status == self::WIN32_SERVICE_NA ||
1283 $status == self::WIN32_ERROR_SERVICE_DOES_NOT_EXIST) {
1284 $elapsedTime = time() - $startTime;
1285 Log::trace("Service deletion confirmed after " . $elapsedTime . " seconds");
1286
1287 return true;
1288 }
1289
1290 // Wait a bit before checking again
1291 usleep(500000); // 0.5 seconds
1292 }
1293
1294 $totalWaitTime = time() - $startTime;
1295 Log::trace("Service deletion timeout after " . $totalWaitTime . " seconds - service still exists: " . $this->getName());
1296
1297 return false;
1298 }
1299
1306 public function ensureReset(): bool
1307 {
1308 Log::trace("Starting ensureReset for service: " . $this->getName());
1309
1310 // First, make sure service is stopped
1311 if ($this->isRunning()) {
1312 Log::trace("Service is still running, stopping it first");
1313 if (!$this->stop()) {
1314 Log::trace("Failed to stop service during ensureReset");
1315
1316 return false;
1317 }
1318 usleep(1000000); // 1 second wait after stop
1319 }
1320
1321 // Delete the service
1322 Log::trace("Deleting service");
1323 if (!$this->delete()) {
1324 Log::trace("Service deletion failed, but continuing with wait");
1325 }
1326
1327 // Wait for the service to be completely removed
1328 if (!$this->waitForServiceDeletion(30)) {
1329 Log::trace("Service deletion did not complete within timeout, but continuing");
1330 // Even if we timeout, give it a moment and try to create anyway
1331 usleep(2000000); // 2 seconds
1332 }
1333
1334 Log::trace("ensureReset completed for service: " . $this->getName());
1335
1336 return true;
1337 }
1338}
$result
global $bearsamppBins
global $bearsamppLang
global $bearsamppRoot
static installPostgresqlService()
static uninstallPostgresqlService()
const CMD_SYNTAX_CHECK
const SERVICE_NAME
const CMD_SYNTAX_CHECK
static execCombined(string $executable, array $args=[])
const ERROR
const STATUS
static info($data, $file=null)
static debug($data, $file=null)
static trace($data, $file=null)
static error($data, $file=null)
static initClass($classInstance)
static getServicesLogFilePath($aetrayPath=false)
static getNssmEnvPaths()
static formatWindowsPath($path)
static loadTools()
static listServices($properties=[])
static getServiceInfo($serviceName, $properties=[])
static killBins($refreshProcs=false)
setErrorControl($errorControl)
waitForServiceDeletion($maxWaitTime=30)
const WIN32_SERVICE_START_PENDING
const WIN32_ERROR_SERVICE_DATABASE_LOCKED
const WIN32_ERROR_ACCESS_DENIED
callWin32Service($function, $param, $checkError=false)
const WIN32_ERROR_SERVICE_NOT_ACTIVE
const WIN32_ERROR_INVALID_LEVEL
const WIN32_ERROR_FAILED_SERVICE_CONTROLLER_CONNECT
const WIN32_ERROR_DUPLICATE_SERVICE_NAME
const WIN32_ERROR_INVALID_HANDLE
const WIN32_ERROR_SERVICE_MARKED_FOR_DELETE
const WIN32_ERROR_SERVICE_DEPENDENCY_DELETED
const WIN32_ERROR_DATABASE_DOES_NOT_EXIST
const WIN32_ERROR_CIRCULAR_DEPENDENCY
static getServices($forceRefresh=false)
const WIN32_ERROR_INVALID_SERVICE_CONTROL
const WIN32_ERROR_SERVICE_CANNOT_ACCEPT_CTRL
const WIN32_ERROR_INVALID_PARAMETER
getWin32ServiceStatusDesc($status)
const WIN32_ERROR_SERVICE_ALREADY_RUNNING
status($timeout=true)
const WIN32_SERVICE_CONTINUE_PENDING
const WIN32_ERROR_SERVICE_DEPENDENCY_FAIL
const WIN32_ERROR_SERVICE_REQUEST_TIMEOUT
const WIN32_ERROR_SERVICE_NO_THREAD
const WIN32_ERROR_PATH_NOT_FOUND
const WIN32_ERROR_INSUFFICIENT_BUFFER
const WIN32_SERVICE_STOP_PENDING
setStartType($startType)
const WIN32_ERROR_SERVICE_DOES_NOT_EXIST
const WIN32_ERROR_SERVICE_DISABLED
const WIN32_ERROR_SHUTDOWN_IN_PROGRESS
setDisplayName($displayName)
const WIN32_ERROR_INVALID_SERVICE_ACCOUNT
const WIN32_ERROR_DEPENDENT_SERVICES_RUNNING
const WIN32_ERROR_SERVICE_EXISTS
const WIN32_SERVICE_PAUSE_PENDING
const WIN32_ERROR_SERVICE_LOGON_FAILED