Bearsampp 2026.7.11
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
139 public static function getServices($forceRefresh = false)
140 {
141 if (self::$serviceListCache === null || $forceRefresh) {
142 Log::trace('Fetching service list from Windows (COM/WMI)');
143 $startTime = microtime(true);
144 $services = Win32Native::listServices(self::getVbsKeys());
145
146 self::$serviceListCache = [];
147 if (is_array($services)) {
148 foreach ($services as $service) {
149 if (isset($service[self::VBS_NAME])) {
150 self::$serviceListCache[$service[self::VBS_NAME]] = $service;
151 }
152 }
153 }
154 $duration = round(microtime(true) - $startTime, 3);
155 Log::trace('Service list fetched in ' . $duration . 's');
156 }
157
158 return self::$serviceListCache;
159 }
160
170 private function callWin32Service($function, $param, $checkError = false): mixed
171 {
172 $result = false;
173 if ( function_exists( $function ) ) {
174 if (!isset(self::$loggedFunctions[$function])) {
175 Log::trace('Win32 function: ' . $function . ' exists');
176 self::$loggedFunctions[$function] = true;
177 }
178
179 // Special handling for win32_query_service_status to prevent hanging
180 if ($function === 'win32_query_service_status') {
181 Log::trace("Using enhanced handling for win32_query_service_status");
182
183 // Set a shorter timeout for this specific function
184 $originalTimeout = ini_get('max_execution_time');
185 set_time_limit(5); // 5 seconds timeout
186
187 try {
188 // Ensure proper parameter handling for PHP 8.5.7 compatibility
189 $result = call_user_func($function, $param);
190
191 // Reset the timeout
192 set_time_limit($originalTimeout);
193
194 if ($checkError && $result !== null) {
195 // Convert to int before using dechex for PHP 8.5.7 compatibility
196 $resultInt = is_numeric($result) ? (int)$result : 0;
197 if (dechex($resultInt) != self::WIN32_NO_ERROR) {
198 $this->latestError = dechex($resultInt);
199 }
200 }
201 } catch (\Win32ServiceException $e) {
202 // Reset the timeout
203 set_time_limit($originalTimeout);
204
205 Log::trace("Win32ServiceException caught: " . $e->getMessage());
206
207 // Handle "service does not exist" exception
208 if (strpos($e->getMessage(), 'service does not exist') !== false) {
209 Log::trace("Service does not exist exception handled for: " . $param);
210 // Return the appropriate error code for "service does not exist"
211 $result = hexdec(self::WIN32_ERROR_SERVICE_DOES_NOT_EXIST);
212 } else {
213 // For other exceptions, log and return false
214 Log::trace("Unhandled Win32ServiceException: " . $e->getMessage());
215 $result = false;
216 }
217 } catch (\Exception $e) {
218 // Reset the timeout
219 set_time_limit($originalTimeout);
220
221 // Catch any other exceptions to prevent application freeze
222 Log::trace("Exception caught in callWin32Service: " . $e->getMessage());
223 $result = false;
224 } catch (\Throwable $e) {
225 // Reset the timeout
226 set_time_limit($originalTimeout);
227
228 // Catch any other throwable (PHP 7+) to prevent application freeze
229 Log::trace("Throwable caught in callWin32Service: " . $e->getMessage());
230 $result = false;
231 }
232 } else {
233 // Standard handling for other functions
234 try {
235 // Ensure proper parameter handling for PHP 8.5.7 compatibility
236 $result = call_user_func($function, $param);
237 if ($checkError && $result !== null) {
238 // Convert to int before using dechex for PHP 8.5.7 compatibility
239 $resultInt = is_numeric($result) ? (int)$result : 0;
240 if (dechex($resultInt) != self::WIN32_NO_ERROR) {
241 $this->latestError = dechex($resultInt);
242 }
243 }
244 } catch (\Win32ServiceException $e) {
245 Log::trace("Win32ServiceException caught: " . $e->getMessage());
246
247 // Handle "service does not exist" exception
248 if (strpos($e->getMessage(), 'service does not exist') !== false) {
249 Log::trace("Service does not exist exception handled for: " . $param);
250 // Return the appropriate error code for "service does not exist"
251 $result = hexdec(self::WIN32_ERROR_SERVICE_DOES_NOT_EXIST);
252 } else {
253 // For other exceptions, log and return false
254 Log::trace("Unhandled Win32ServiceException: " . $e->getMessage());
255 $result = false;
256 }
257 } catch (\Exception $e) {
258 // Catch any other exceptions to prevent application freeze
259 Log::trace("Exception caught in callWin32Service: " . $e->getMessage());
260 $result = false;
261 } catch (\Throwable $e) {
262 // Catch any other throwable (PHP 7+) to prevent application freeze
263 Log::trace("Throwable caught in callWin32Service: " . $e->getMessage());
264 $result = false;
265 }
266 }
267 } else {
268 if (!isset(self::$loggedFunctions[$function])) {
269 Log::trace('Win32 function: ' . $function . ' missing');
270 self::$loggedFunctions[$function] = true;
271 }
272 }
273 return $result;
274 }
275
283 public function status($timeout = true): string
284 {
285 $this->latestStatus = self::WIN32_SERVICE_NA;
286 $maxtime = time() + self::PENDING_TIMEOUT;
287
288 Log::trace("Querying status for service: " . $this->getName() . " (timeout: " . ($timeout ? "enabled" : "disabled") . ")");
289 if ($timeout) {
290 Log::trace("Max timeout time set to: " . date('Y-m-d H:i:s', $maxtime));
291 }
292
293 // Add a safety counter to prevent infinite loops
294 $loopCount = 0;
295 $maxLoops = 5; // Maximum number of attempts
296 $startTime = microtime(true);
297
298 try {
299 while ( ($this->latestStatus == self::WIN32_SERVICE_NA || $this->isPending( $this->latestStatus )) && $loopCount < $maxLoops ) {
300 $loopCount++;
301 Log::trace("Calling win32_query_service_status for service: " . $this->getName() . " (attempt " . $loopCount . " of " . $maxLoops . ")");
302
303 // Add a timeout check before making the call
304 if (microtime(true) - $startTime > 10) { // 10 seconds overall timeout
305 Log::trace("Overall timeout reached before making service status call");
306 break;
307 }
308
309 $this->latestStatus = $this->callWin32Service( 'win32_query_service_status', $this->getName() );
310
311 if ( is_array( $this->latestStatus ) && isset( $this->latestStatus['CurrentState'] ) ) {
312 // Ensure proper type conversion for PHP 8.5.7 compatibility
313 $stateInt = is_numeric($this->latestStatus['CurrentState']) ? (int)$this->latestStatus['CurrentState'] : 0;
314 $this->latestStatus = dechex( $stateInt );
315 Log::trace("Service status returned as array, CurrentState: " . $this->latestStatus);
316 }
317 elseif ( $this->latestStatus !== null ) {
318 // Ensure proper type conversion for PHP 8.5.7 compatibility
319 $statusInt = is_numeric($this->latestStatus) ? (int)$this->latestStatus : 0;
320 $statusHex = dechex( $statusInt );
321 Log::trace("Service status returned as value: " . $statusHex);
322
323 if ( $statusHex == self::WIN32_ERROR_SERVICE_DOES_NOT_EXIST ) {
324 $this->latestStatus = $statusHex;
325 Log::trace("Service does not exist, breaking loop");
326 break; // Exit the loop immediately if service doesn't exist
327 }
328 } else {
329 Log::trace("Service status query returned null");
330 // If we get a null result, assume service does not exist to avoid hanging
331 if ($loopCount >= 2) // Only do this after at least one retry
332 {
333 Log::trace("Multiple null results, assuming service does not exist");
334 $this->latestStatus = self::WIN32_ERROR_SERVICE_DOES_NOT_EXIST;
335 break;
336 }
337 }
338
339 if ( $timeout && $maxtime < time() ) {
340 Log::trace("Timeout reached while querying service status");
341 break;
342 }
343
344 // Only sleep if we're going to loop again
345 if ($loopCount < $maxLoops && ($this->latestStatus == self::WIN32_SERVICE_NA || $this->isPending($this->latestStatus))) {
346 Log::trace("Sleeping before next status check attempt");
347 usleep(self::SLEEP_TIME);
348 }
349 }
350 } catch (\Exception $e) {
351 Log::trace("Exception in status method: " . $e->getMessage());
352 // If an exception occurs, assume service does not exist
353 $this->latestStatus = self::WIN32_ERROR_SERVICE_DOES_NOT_EXIST;
354 } catch (\Throwable $e) {
355 Log::trace("Throwable in status method: " . $e->getMessage());
356 // If a throwable occurs, assume service does not exist
357 $this->latestStatus = self::WIN32_ERROR_SERVICE_DOES_NOT_EXIST;
358 }
359
360 if ($loopCount >= $maxLoops) {
361 Log::trace("Maximum query attempts reached for service: " . $this->getName());
362 }
363
364 $elapsedTime = microtime(true) - $startTime;
365 Log::trace("Status check completed in " . round($elapsedTime, 2) . " seconds after " . $loopCount . " attempts");
366
367 if ( $this->latestStatus == self::WIN32_ERROR_SERVICE_DOES_NOT_EXIST ) {
368 $this->latestError = $this->latestStatus;
369 $this->latestStatus = self::WIN32_SERVICE_NA;
370 Log::trace("Service does not exist, setting status to NA");
371 }
372
373 Log::trace("Final status for service " . $this->getName() . ": " . $this->latestStatus);
374 return $this->latestStatus;
375 }
376
382 public function create(): bool
383 {
384 global $bearsamppBins;
385
386 Log::trace("Starting Win32Service::create for service: " . $this->getName());
387
388 if ( $this->getName() == BinPostgresql::SERVICE_NAME ) {
389 Log::trace("PostgreSQL service detected - using specialized installation");
390 $bearsamppBins->getPostgresql()->rebuildConf();
391 Log::trace("PostgreSQL configuration rebuilt");
392
393 $bearsamppBins->getPostgresql()->initData();
394 Log::trace("PostgreSQL data initialized");
395
397 Log::trace("PostgreSQL service installation " . ($result ? "succeeded" : "failed"));
398 return $result;
399 }
400
401 if ( $this->getNssm() instanceof Nssm ) {
402 Log::trace("Using NSSM for service installation");
403
404 // Ensure Tools are loaded before building environment paths
405 global $bearsamppTools;
406 if (!isset($bearsamppTools)) {
407 Log::trace("Tools not loaded, forcing synchronous load");
409 }
410
411 global $bearsamppRegistry;
412 $nssmEnvPath = $bearsamppRegistry->getAppBinsRegKey( false );
413 Log::trace("NSSM environment path (bins): " . $nssmEnvPath);
414
415 $nssmEnvPath .= Path::getNssmEnvPaths();
416 Log::trace("NSSM environment path (with additional paths): " . $nssmEnvPath);
417
418 $nssmEnvPath .= '%SystemRoot%/system32;';
419 $nssmEnvPath .= '%SystemRoot%;';
420 $nssmEnvPath .= '%SystemRoot%/system32/Wbem;';
421 $nssmEnvPath .= '%SystemRoot%/system32/WindowsPowerShell/v1.0';
422 Log::trace("NSSM final environment PATH: " . $nssmEnvPath);
423
424 $this->getNssm()->setEnvironmentExtra( 'PATH=' . $nssmEnvPath );
425 Log::trace("NSSM service parameters:");
426 Log::trace("-> Name: " . $this->getNssm()->getName());
427 Log::trace("-> DisplayName: " . $this->getNssm()->getDisplayName());
428 Log::trace("-> BinPath: " . $this->getNssm()->getBinPath());
429 Log::trace("-> Params: " . $this->getNssm()->getParams());
430 Log::trace("-> Start: " . $this->getNssm()->getStart());
431 Log::trace("-> Stdout: " . $this->getNssm()->getStdout());
432 Log::trace("-> Stderr: " . $this->getNssm()->getStderr());
433
434 $result = $this->getNssm()->create();
435 Log::trace("NSSM service creation " . ($result ? "succeeded" : "failed"));
436 if (!$result) {
437 Log::trace("NSSM error: " . $this->getNssm()->getLatestError());
438 }
439 return $result;
440 }
441
442 Log::trace("Using win32_create_service for service installation");
443 $serviceParams = array(
444 'service' => $this->getName(),
445 'display' => $this->getDisplayName(),
446 'description' => $this->getDisplayName(),
447 'path' => $this->getBinPath(),
448 'params' => $this->getParams(),
449 'start_type' => $this->getStartType() != null ? $this->getStartType() : self::SERVICE_DEMAND_START,
450 'error_control' => $this->getErrorControl() != null ? $this->getErrorControl() : self::SERVER_ERROR_NORMAL,
451 );
452
453 Log::trace("win32_create_service parameters:");
454 foreach ($serviceParams as $key => $value) {
455 Log::trace("-> $key: $value");
456 }
457
458 $result = $this->callWin32Service( 'win32_create_service', $serviceParams, true );
459 // Ensure proper type conversion for PHP 8.5.7 compatibility
460 $resultInt = is_numeric($result) ? (int)$result : 0;
461 $create = $result !== null ? dechex( $resultInt ) : '0';
462 Log::trace("win32_create_service result code: " . $create);
463
464 // Retry once if the SCM has the service marked for deletion from a recent delete()
465 if ( $create == self::WIN32_ERROR_SERVICE_MARKED_FOR_DELETE ) {
466 Log::trace("Service marked for delete, waiting 2s before retry: " . $this->getName());
467 usleep( 2000000 );
468 $result = $this->callWin32Service( 'win32_create_service', $serviceParams, true );
469 $resultInt = is_numeric($result) ? (int)$result : 0;
470 $create = $result !== null ? dechex( $resultInt ) : '0';
471 Log::trace("win32_create_service retry result code: " . $create);
472 }
473
474 $this->writeLog( 'Create service: ' . $create . ' (status: ' . $this->status() . ')' );
475 $this->writeLog( '-> service: ' . $this->getName() );
476 $this->writeLog( '-> display: ' . $this->getDisplayName() );
477 $this->writeLog( '-> description: ' . $this->getDisplayName() );
478 $this->writeLog( '-> path: ' . $this->getBinPath() );
479 $this->writeLog( '-> params: ' . $this->getParams() );
480 $this->writeLog( '-> start_type: ' . ($this->getStartType() != null ? $this->getStartType() : self::SERVICE_DEMAND_START) );
481 $this->writeLog( '-> service: ' . ($this->getErrorControl() != null ? $this->getErrorControl() : self::SERVER_ERROR_NORMAL) );
482
483 if ( $create != self::WIN32_NO_ERROR ) {
484 Log::trace("Service creation failed with error code: " . $create);
485 return false;
486 }
487 elseif ( !$this->isInstalled() ) {
488 Log::trace("Service created but not found as installed");
489 $this->latestError = self::WIN32_NO_ERROR;
490 return false;
491 }
492
493 Log::trace("Service created successfully: " . $this->getName());
494 return true;
495 }
496
502 public function delete(): bool
503 {
504 Log::trace("Starting Win32Service::delete for service: " . $this->getName());
505 Log::trace("Checking if service is installed: " . $this->getName());
506
507 if ( !$this->isInstalled() ) {
508 Log::trace("Service is not installed, skipping deletion: " . $this->getName());
509 return true;
510 }
511
512 Log::trace("Stopping service before deletion: " . $this->getName());
513 $this->stop();
514
515 if ( $this->getNssm() instanceof Nssm ) {
516 $childExe = basename( $this->getNssm()->getBinPath() );
517 Log::trace("Killing NSSM child process after stop: " . $childExe);
518 Win32Ps::killBins( [$childExe] );
519 }
520
521 if ( $this->getName() == BinPostgresql::SERVICE_NAME ) {
522 Log::trace("PostgreSQL service detected - using specialized uninstallation");
524 Log::trace("PostgreSQL service uninstallation " . ($result ? "succeeded" : "failed"));
525 return $result;
526 }
527
528 Log::trace("Calling win32_delete_service for service: " . $this->getName());
529 $result = $this->callWin32Service( 'win32_delete_service', $this->getName(), true );
530 // Ensure proper type conversion for PHP 8.5.7 compatibility
531 $resultInt = is_numeric($result) ? (int)$result : 0;
532 $delete = $result !== null ? dechex( $resultInt ) : '0';
533 Log::trace("Delete service result code: " . $delete);
534 $this->writeLog( 'Delete service ' . $this->getName() . ': ' . $delete . ' (status: ' . $this->status() . ')' );
535
536 if ( $delete != self::WIN32_NO_ERROR && $delete != self::WIN32_ERROR_SERVICE_DOES_NOT_EXIST ) {
537 return false;
538 }
539 elseif ( $this->isInstalled() ) {
540 $this->latestError = self::WIN32_NO_ERROR;
541
542 return false;
543 }
544
545 return true;
546 }
547
553 public function reset(): bool
554 {
555 if ( $this->delete() ) {
556 usleep( self::SLEEP_TIME );
557
558 return $this->create();
559 }
560
561 return false;
562 }
563
569 public function start(): bool
570 {
571 global $bearsamppBins;
572
573 Log::info('Attempting to start service: ' . $this->getName());
574
575 if ( $this->getName() == BinMysql::SERVICE_NAME ) {
576 $bearsamppBins->getMysql()->initData();
577 }
578 elseif ( $this->getName() == BinMariadb::SERVICE_NAME ) {
579 $bearsamppBins->getMariadb()->initData();
580 }
581 elseif ( $this->getName() == BinMailpit::SERVICE_NAME ) {
582 $bearsamppBins->getMailpit()->rebuildConf();
583 }
584 elseif ( $this->getName() == BinMemcached::SERVICE_NAME ) {
585 $bearsamppBins->getMemcached()->rebuildConf();
586 }
587 elseif ( $this->getName() == BinPostgresql::SERVICE_NAME ) {
588 $bearsamppBins->getPostgresql()->rebuildConf();
589 $bearsamppBins->getPostgresql()->initData();
590 }
591 elseif ( $this->getName() == BinXlight::SERVICE_NAME ) {
592 $bearsamppBins->getXlight()->rebuildConf();
593 }
594
595
596 $result = $this->callWin32Service( 'win32_start_service', $this->getName(), true );
597 // Ensure proper type conversion for PHP 8.5.7 compatibility
598 $resultInt = is_numeric($result) ? (int)$result : 0;
599 $start = $result !== null ? dechex( $resultInt ) : '0';
600 Log::debug( 'Start service ' . $this->getName() . ': ' . $start . ' (status: ' . $this->status() . ')' );
601
602 if ( $start != self::WIN32_NO_ERROR && $start != self::WIN32_ERROR_SERVICE_ALREADY_RUNNING ) {
603
604 // Write error to log
605 Log::error('Failed to start service: ' . $this->getName() . ' with error code: ' . $start);
606
607 if ( $this->getName() == BinApache::SERVICE_NAME ) {
608 $cmdOutput = $bearsamppBins->getApache()->getCmdLineOutput( BinApache::CMD_SYNTAX_CHECK );
609 if ( !$cmdOutput['syntaxOk'] ) {
610 file_put_contents(
611 $bearsamppBins->getApache()->getErrorLog(),
612 '[' . date( 'Y-m-d H:i:s', time() ) . '] [error] ' . $cmdOutput['content'] . PHP_EOL,
613 FILE_APPEND
614 );
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 }
627 elseif ( $this->getName() == BinMariadb::SERVICE_NAME ) {
628 $cmdOutput = $bearsamppBins->getMariadb()->getCmdLineOutput( BinMariadb::CMD_SYNTAX_CHECK );
629 if ( !$cmdOutput['syntaxOk'] ) {
630 file_put_contents(
631 $bearsamppBins->getMariadb()->getErrorLog(),
632 '[' . date( 'Y-m-d H:i:s', time() ) . '] [error] ' . $cmdOutput['content'] . PHP_EOL,
633 FILE_APPEND
634 );
635 }
636 }
637
638 return false;
639 }
640
641 // Wait for the service to actually start before checking if it's running
642 // We use a timeout to avoid hanging if the service fails to start properly
643 $maxtime = time() + self::PENDING_TIMEOUT;
644 while ($this->isPending($this->status(false)) && time() < $maxtime) {
645 usleep(self::SLEEP_TIME);
646 }
647
648 if ( !$this->isRunning() ) {
649 $this->latestError = self::WIN32_NO_ERROR;
650 Log::error('Service ' . $this->getName() . ' is not running after start attempt (status: ' . $this->status() . ').');
651 $this->latestError = null;
652 return false;
653 }
654
655 Log::info('Service ' . $this->getName() . ' started successfully.');
656 return true;
657 }
658
664 public function stop(): bool
665 {
666 Log::trace("Starting Win32Service::stop for service: " . $this->getName());
667
668 Log::trace("Calling win32_stop_service for service: " . $this->getName());
669 $result = $this->callWin32Service( 'win32_stop_service', $this->getName(), true );
670
671 // Ensure proper type conversion for PHP 8.5.7 compatibility
672 $resultInt = is_numeric($result) ? (int)$result : 0;
673 $stop = $result !== null ? dechex( $resultInt ) : '0';
674 Log::trace("Stop service result code: " . $stop);
675
676 Log::trace("Checking current status after stop attempt");
677 $currentStatus = $this->status();
678 Log::trace("Current status: " . $currentStatus);
679
680 $this->writeLog( 'Stop service ' . $this->getName() . ': ' . $stop . ' (status: ' . $currentStatus . ')' );
681
682 if ( $stop != self::WIN32_NO_ERROR ) {
683 return false;
684 }
685
686 // Wait for the service to actually stop before checking if it's stopped
687 $maxtime = time() + self::PENDING_TIMEOUT;
688 while ($this->isPending($this->status(false)) && time() < $maxtime) {
689 usleep(self::SLEEP_TIME);
690 }
691
692 if ( !$this->isStopped() ) {
693 $this->latestError = self::WIN32_NO_ERROR;
694 Log::error('Service ' . $this->getName() . ' is still running after stop attempt (status: ' . $this->status() . ').');
695 $this->latestError = null;
696 return false;
697 }
698
699 return true;
700 }
701
707 public function restart(): bool
708 {
709 if ( $this->stop() ) {
710 return $this->start();
711 }
712
713 return false;
714 }
715
722 public function fastServiceCheck()
723 {
724 Log::trace("Starting fastServiceCheck for service: " . $this->getName());
725
726 $startTime = microtime(true);
727
728 // Use sc.exe to query service - this is very fast and reliable
729 // Execute with hidden window to prevent command prompt flash
730 Log::trace("Executing: sc query " . $this->getName());
731
732 $output = CommandRunner::execCombined('sc', ['query', $this->getName()]);
733 $duration = round(microtime(true) - $startTime, 3);
734
735 Log::trace("sc.exe query completed in " . $duration . "s");
736
737 if ($output === null || $output === false) {
738 Log::trace("sc.exe returned null/false, service likely doesn't exist");
739 return false;
740 }
741
742 // Check if service doesn't exist
743 if (stripos($output, 'does not exist') !== false ||
744 stripos($output, 'FAILED') !== false ||
745 stripos($output, '1060') !== false) { // Error code 1060 = service doesn't exist
746 Log::trace("Service doesn't exist: " . $this->getName());
747 return false;
748 }
749
750 // Service exists - parse basic info
751 $serviceInfo = [];
752
753 // Extract service name
754 if (preg_match('/SERVICE_NAME:\s*(.+)/i', $output, $matches)) {
755 $serviceInfo[self::VBS_NAME] = trim($matches[1]);
756 }
757
758 // Extract display name
759 if (preg_match('/DISPLAY_NAME:\s*(.+)/i', $output, $matches)) {
760 $serviceInfo[self::VBS_DISPLAY_NAME] = trim($matches[1]);
761 }
762
763 // Extract state
764 if (preg_match('/STATE\s*:\s*\d+\s+(\w+)/i', $output, $matches)) {
765 $state = trim($matches[1]);
766 $serviceInfo[self::SERVICE_STATE] = $state;
767 Log::trace("Service state: " . $state);
768 }
769
770 // If we have basic info, service exists - get full details if needed
771 if (!empty($serviceInfo)) {
772 Log::trace("Service exists, getting full details");
773
774 // Use sc qc to get configuration details (including path)
775 $configOutput = CommandRunner::execCombined('sc', ['qc', $this->getName()]);
776
777 if ($configOutput !== null && $configOutput !== false && preg_match('/BINARY_PATH_NAME\s*:\s*(.+)/i', $configOutput, $matches)) {
778 $serviceInfo[self::VBS_PATH_NAME] = trim($matches[1]);
779 Log::trace("Service path: " . $serviceInfo[self::VBS_PATH_NAME]);
780 }
781
782 // Get description if available
783 if ($configOutput !== null && $configOutput !== false && preg_match('/DISPLAY_NAME\s*:\s*(.+)/i', $configOutput, $matches)) {
784 $serviceInfo[self::VBS_DESCRIPTION] = trim($matches[1]);
785 }
786
787 Log::trace("Fast service check successful for: " . $this->getName());
788 return $serviceInfo;
789 }
790
791 Log::trace("Could not parse service info from sc.exe output");
792 return false;
793 }
794
801 public function infos()
802 {
803 Log::trace("Starting Win32Service::infos for service: " . $this->getName());
804
805 try {
806 // Set a timeout for the entire operation
807 $startTime = microtime(true);
808 $timeout = 10; // 10 seconds timeout for the entire operation
809
810 if ($this->getNssm() instanceof Nssm) {
811 Log::trace("Using NSSM to get service info");
812 $result = $this->getNssm()->infos();
813 Log::trace("NSSM info retrieval completed in " . round(microtime(true) - $startTime, 2) . " seconds");
814 return $result;
815 }
816
817 // Performance optimization: Try fast sc.exe check first
818 Log::trace("Attempting fast service check using sc.exe");
819 $fastResult = $this->fastServiceCheck();
820
821 if ($fastResult !== false) {
822 $duration = round(microtime(true) - $startTime, 3);
823 Log::trace("Fast service check succeeded in " . $duration . "s (saved 5-10s)");
824 Log::debug("Performance: Fast service check used for " . $this->getName() . ", saved 5-10 seconds");
825 return $fastResult;
826 }
827
828 // Fast check returned false - service doesn't exist
829 if ($fastResult === false) {
830 $duration = round(microtime(true) - $startTime, 3);
831 Log::trace("Fast service check determined service doesn't exist in " . $duration . "s");
832 return false;
833 }
834
835 // Fallback to VBS (should rarely be needed now)
836 Log::trace("Falling back to VBS for service info");
837
838 // Use set_time_limit to prevent PHP script timeout
839 $originalTimeout = ini_get('max_execution_time');
840 set_time_limit(15); // 15 seconds timeout
841
842 // Create a separate process to get service info with a timeout
844
845 // Reset the timeout
846 set_time_limit($originalTimeout);
847
848 // Check if we've exceeded our timeout
849 if (microtime(true) - $startTime > $timeout) {
850 Log::trace("Timeout exceeded in infos() method, returning false");
851 return false;
852 }
853
854 Log::trace("VBS info retrieval completed in " . round(microtime(true) - $startTime, 2) . " seconds");
855 return $result;
856 } catch (\Exception $e) {
857 Log::trace("Exception in infos() method: " . $e->getMessage() . ", returning false");
858 return false;
859 } catch (\Throwable $e) {
860 Log::trace("Throwable in infos() method: " . $e->getMessage() . ", returning false");
861 return false;
862 }
863 }
864
870 public function isInstalled(): bool
871 {
872 Log::trace("Checking if service is installed: " . $this->getName());
873
874 try {
875 // Set a timeout for the entire operation
876 $startTime = microtime(true);
877 $timeout = 15; // 15 seconds timeout for the entire operation
878
879 // Call status() with a try-catch to ensure we don't get stuck
880 $status = $this->status();
881
882 // Check if we've exceeded our timeout
883 if (microtime(true) - $startTime > $timeout) {
884 Log::trace("Timeout exceeded in isInstalled() method, assuming service is not installed");
885 $this->writeLog('isInstalled ' . $this->getName() . ': NO (timeout exceeded)');
886 return false;
887 }
888
889 $isInstalled = $status != self::WIN32_SERVICE_NA;
890
891 Log::trace("Service " . $this->getName() . " installation status: " . ($isInstalled ? "YES" : "NO") . " (status code: " . $status . ")");
892 $this->writeLog('isInstalled ' . $this->getName() . ': ' . ($isInstalled ? 'YES' : 'NO') . ' (status: ' . $status . ')');
893
894 return $isInstalled;
895 } catch (\Exception $e) {
896 Log::trace("Exception in isInstalled() method: " . $e->getMessage() . ", assuming service is not installed");
897 $this->writeLog('isInstalled ' . $this->getName() . ': NO (exception: ' . $e->getMessage() . ')');
898 return false;
899 } catch (\Throwable $e) {
900 Log::trace("Throwable in isInstalled() method: " . $e->getMessage() . ", assuming service is not installed");
901 $this->writeLog('isInstalled ' . $this->getName() . ': NO (throwable: ' . $e->getMessage() . ')');
902 return false;
903 }
904 }
905
911 public function isRunning(): bool
912 {
913 Log::trace("Checking if service is running: " . $this->getName());
914
915 $status = $this->status();
916 $isRunning = $status == self::WIN32_SERVICE_RUNNING;
917
918 Log::trace("Service " . $this->getName() . " running status: " . ($isRunning ? "YES" : "NO") . " (status code: " . $status . ")");
919 $this->writeLog( 'isRunning ' . $this->getName() . ': ' . ($isRunning ? 'YES' : 'NO') . ' (status: ' . $status . ')' );
920
921 return $isRunning;
922 }
923
929 public function isStopped(): bool
930 {
931 Log::trace("Checking if service is stopped: " . $this->getName());
932
933 $status = $this->status();
934 $isStopped = $status == self::WIN32_SERVICE_STOPPED;
935
936 Log::trace("Service " . $this->getName() . " stopped status: " . ($isStopped ? "YES" : "NO") . " (status code: " . $status . ")");
937 $this->writeLog( 'isStopped ' . $this->getName() . ': ' . ($isStopped ? 'YES' : 'NO') . ' (status: ' . $status . ')' );
938
939 return $isStopped;
940 }
941
947 public function isPaused(): bool
948 {
949 Log::trace("Checking if service is paused: " . $this->getName());
950
951 $status = $this->status();
952 $isPaused = $status == self::WIN32_SERVICE_PAUSED;
953
954 Log::trace("Service " . $this->getName() . " paused status: " . ($isPaused ? "YES" : "NO") . " (status code: " . $status . ")");
955 $this->writeLog( 'isPaused ' . $this->getName() . ': ' . ($isPaused ? 'YES' : 'NO') . ' (status: ' . $status . ')' );
956
957 return $isPaused;
958 }
959
967 public function isPending($status): bool
968 {
969 $isPending = $status == self::WIN32_SERVICE_START_PENDING || $status == self::WIN32_SERVICE_STOP_PENDING
970 || $status == self::WIN32_SERVICE_CONTINUE_PENDING || $status == self::WIN32_SERVICE_PAUSE_PENDING;
971
972 Log::trace("Checking if status is pending: " . $status . " - Result: " . ($isPending ? "YES" : "NO"));
973
974 if ($isPending) {
975 if ($status == self::WIN32_SERVICE_START_PENDING) {
976 Log::trace("Service is in START_PENDING state");
977 } else if ($status == self::WIN32_SERVICE_STOP_PENDING) {
978 Log::trace("Service is in STOP_PENDING state");
979 } else if ($status == self::WIN32_SERVICE_CONTINUE_PENDING) {
980 Log::trace("Service is in CONTINUE_PENDING state");
981 } else if ($status == self::WIN32_SERVICE_PAUSE_PENDING) {
982 Log::trace("Service is in PAUSE_PENDING state");
983 }
984 }
985
986 return $isPending;
987 }
988
996 private function getWin32ServiceStatusDesc($status): ?string
997 {
998 switch ( $status ) {
999 case self::WIN32_SERVICE_CONTINUE_PENDING:
1000 return 'The service continue is pending.';
1001
1002 case self::WIN32_SERVICE_PAUSE_PENDING:
1003 return 'The service pause is pending.';
1004
1005 case self::WIN32_SERVICE_PAUSED:
1006 return 'The service is paused.';
1007
1008 case self::WIN32_SERVICE_RUNNING:
1009 return 'The service is running.';
1010
1011 case self::WIN32_SERVICE_START_PENDING:
1012 return 'The service is starting.';
1013
1014 case self::WIN32_SERVICE_STOP_PENDING:
1015 return 'The service is stopping.';
1016
1017 case self::WIN32_SERVICE_STOPPED:
1018 return 'The service is not running.';
1019
1020 case self::WIN32_SERVICE_NA:
1021 return 'Cannot retrieve service status.';
1022
1023 default:
1024 return null;
1025 }
1026 }
1027
1035 private function getWin32ErrorCodeDesc($code): ?string
1036 {
1037 switch ( $code ) {
1038 case self::WIN32_ERROR_ACCESS_DENIED:
1039 return 'The handle to the SCM database does not have the appropriate access rights.';
1040 // ... other cases ...
1041 default:
1042 return null;
1043 }
1044 }
1045
1051 public function getName(): string
1052 {
1053 return $this->name;
1054 }
1055
1061 public function setName($name): void
1062 {
1063 $this->name = $name;
1064 }
1065
1071 public function getDisplayName(): string
1072 {
1073 return $this->displayName;
1074 }
1075
1081 public function setDisplayName($displayName): void
1082 {
1083 $this->displayName = $displayName;
1084 }
1085
1091 public function getBinPath(): string
1092 {
1093 return $this->binPath;
1094 }
1095
1101 public function setBinPath($binPath): void
1102 {
1103 $this->binPath = str_replace( '"', '', Path::formatWindowsPath( $binPath ) );
1104 }
1105
1111 public function getParams(): string
1112 {
1113 return $this->params;
1114 }
1115
1121 public function setParams($params): void
1122 {
1123 $this->params = $params;
1124 }
1125
1131 public function getStartType(): string
1132 {
1133 return $this->startType;
1134 }
1135
1141 public function setStartType($startType): void
1142 {
1143 $this->startType = $startType;
1144 }
1145
1151 public function getErrorControl(): string
1152 {
1153 return $this->errorControl;
1154 }
1155
1161 public function setErrorControl($errorControl): void
1162 {
1163 $this->errorControl = $errorControl;
1164 }
1165
1171 public function getNssm()
1172 {
1173 return $this->nssm;
1174 }
1175
1181 public function setNssm($nssm)
1182 {
1183 if ( $nssm instanceof Nssm ) {
1184 $this->setDisplayName( $nssm->getDisplayName() );
1185 $this->setBinPath( $nssm->getBinPath() );
1186 $this->setParams( $nssm->getParams() );
1187 $this->setStartType( $nssm->getStart() );
1188 $this->nssm = $nssm;
1189 }
1190 }
1191
1197 public function getLatestStatus()
1198 {
1199 return $this->latestStatus;
1200 }
1201
1207 public function getLatestError()
1208 {
1209 return $this->latestError;
1210 }
1211
1217 public function getError()
1218 {
1219 global $bearsamppLang;
1220 if ( $this->latestError != self::WIN32_NO_ERROR ) {
1221 // Ensure proper type conversion for PHP 8.5.7 compatibility
1222 $errorInt = is_numeric($this->latestError) ? hexdec( $this->latestError ) : 0;
1223 return $bearsamppLang->getValue( Lang::ERROR ) . ' ' .
1224 $this->latestError . ' (' . $errorInt . ' : ' . $this->getWin32ErrorCodeDesc( $this->latestError ) . ')';
1225 }
1226 elseif ( $this->latestStatus != self::WIN32_SERVICE_NA ) {
1227 // Ensure proper type conversion for PHP 8.5.7 compatibility
1228 $statusInt = is_numeric($this->latestStatus) ? hexdec( $this->latestStatus ) : 0;
1229 return $bearsamppLang->getValue( Lang::STATUS ) . ' ' .
1230 $this->latestStatus . ' (' . $statusInt . ' : ' . $this->getWin32ServiceStatusDesc( $this->latestStatus ) . ')';
1231 }
1232
1233 return null;
1234 }
1235
1245 public function waitForServiceDeletion($maxWaitTime = 30): bool
1246 {
1247 $startTime = time();
1248 $maxTime = $startTime + $maxWaitTime;
1249 $checkCount = 0;
1250
1251 Log::trace("Waiting for service deletion: " . $this->getName() . " (max wait: " . $maxWaitTime . "s)");
1252
1253 while (time() < $maxTime) {
1254 $checkCount++;
1255 $status = $this->status(false);
1256 Log::trace("Service deletion check #" . $checkCount . " - Status: " . $status . " at " . date('Y-m-d H:i:s'));
1257
1258 // Service doesn't exist or is definitely not there
1259 if ($status == self::WIN32_SERVICE_NA ||
1260 $status == self::WIN32_ERROR_SERVICE_DOES_NOT_EXIST) {
1261 $elapsedTime = time() - $startTime;
1262 Log::trace("Service deletion confirmed after " . $elapsedTime . " seconds");
1263 return true;
1264 }
1265
1266 // Wait a bit before checking again
1267 usleep(500000); // 0.5 seconds
1268 }
1269
1270 $totalWaitTime = time() - $startTime;
1271 Log::trace("Service deletion timeout after " . $totalWaitTime . " seconds - service still exists: " . $this->getName());
1272 return false;
1273 }
1274
1281 public function ensureReset(): bool
1282 {
1283 Log::trace("Starting ensureReset for service: " . $this->getName());
1284
1285 // First, make sure service is stopped
1286 if ($this->isRunning()) {
1287 Log::trace("Service is still running, stopping it first");
1288 if (!$this->stop()) {
1289 Log::trace("Failed to stop service during ensureReset");
1290 return false;
1291 }
1292 usleep(1000000); // 1 second wait after stop
1293 }
1294
1295 // Delete the service
1296 Log::trace("Deleting service");
1297 if (!$this->delete()) {
1298 Log::trace("Service deletion failed, but continuing with wait");
1299 }
1300
1301 // Wait for the service to be completely removed
1302 if (!$this->waitForServiceDeletion(30)) {
1303 Log::trace("Service deletion did not complete within timeout, but continuing");
1304 // Even if we timeout, give it a moment and try to create anyway
1305 usleep(2000000); // 2 seconds
1306 }
1307
1308 Log::trace("ensureReset completed for service: " . $this->getName());
1309 return true;
1310 }
1311}
$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