Bearsampp 2026.3.26
API documentation
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 = 500000;
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 VBS_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 // Track which functions have been logged to avoid duplicate log entries
89 private static $loggedFunctions = array();
90
96 public function __construct($name)
97 {
98 Util::logInitClass( $this );
99 $this->name = $name;
100 }
101
107 private function writeLog($log): void
108 {
109 global $bearsamppRoot;
110 Util::logDebug( $log, $bearsamppRoot->getServicesLogFilePath() );
111 }
112
118 public static function getVbsKeys(): array
119 {
120 return array(
121 self::VBS_NAME,
122 self::VBS_DISPLAY_NAME,
123 self::VBS_DESCRIPTION,
124 self::VBS_PATH_NAME,
125 self::VBS_STATE
126 );
127 }
128
138 private function callWin32Service($function, $param, $checkError = false): mixed
139 {
140 $result = false;
141 if ( function_exists( $function ) ) {
142 if (!isset(self::$loggedFunctions[$function])) {
143 Util::logTrace('Win32 function: ' . $function . ' exists');
144 self::$loggedFunctions[$function] = true;
145 }
146
147 // Special handling for win32_query_service_status to prevent hanging
148 if ($function === 'win32_query_service_status') {
149 Util::logTrace("Using enhanced handling for win32_query_service_status");
150
151 // Set a shorter timeout for this specific function
152 $originalTimeout = ini_get('max_execution_time');
153 set_time_limit(5); // 5 seconds timeout
154
155 try {
156 // Ensure proper parameter handling for PHP 8.2.3 compatibility
157 $result = call_user_func($function, $param);
158
159 // Reset the timeout
160 set_time_limit($originalTimeout);
161
162 if ($checkError && $result !== null) {
163 // Convert to int before using dechex for PHP 8.2.3 compatibility
164 $resultInt = is_numeric($result) ? (int)$result : 0;
165 if (dechex($resultInt) != self::WIN32_NO_ERROR) {
166 $this->latestError = dechex($resultInt);
167 }
168 }
169 } catch (\Win32ServiceException $e) {
170 // Reset the timeout
171 set_time_limit($originalTimeout);
172
173 Util::logTrace("Win32ServiceException caught: " . $e->getMessage());
174
175 // Handle "service does not exist" exception
176 if (strpos($e->getMessage(), 'service does not exist') !== false) {
177 Util::logTrace("Service does not exist exception handled for: " . $param);
178 // Return the appropriate error code for "service does not exist"
179 $result = hexdec(self::WIN32_ERROR_SERVICE_DOES_NOT_EXIST);
180 } else {
181 // For other exceptions, log and return false
182 Util::logTrace("Unhandled Win32ServiceException: " . $e->getMessage());
183 $result = false;
184 }
185 } catch (\Exception $e) {
186 // Reset the timeout
187 set_time_limit($originalTimeout);
188
189 // Catch any other exceptions to prevent application freeze
190 Util::logTrace("Exception caught in callWin32Service: " . $e->getMessage());
191 $result = false;
192 } catch (\Throwable $e) {
193 // Reset the timeout
194 set_time_limit($originalTimeout);
195
196 // Catch any other throwable (PHP 7+) to prevent application freeze
197 Util::logTrace("Throwable caught in callWin32Service: " . $e->getMessage());
198 $result = false;
199 }
200 } else {
201 // Standard handling for other functions
202 try {
203 // Ensure proper parameter handling for PHP 8.2.3 compatibility
204 $result = call_user_func($function, $param);
205 if ($checkError && $result !== null) {
206 // Convert to int before using dechex for PHP 8.2.3 compatibility
207 $resultInt = is_numeric($result) ? (int)$result : 0;
208 if (dechex($resultInt) != self::WIN32_NO_ERROR) {
209 $this->latestError = dechex($resultInt);
210 }
211 }
212 } catch (\Win32ServiceException $e) {
213 Util::logTrace("Win32ServiceException caught: " . $e->getMessage());
214
215 // Handle "service does not exist" exception
216 if (strpos($e->getMessage(), 'service does not exist') !== false) {
217 Util::logTrace("Service does not exist exception handled for: " . $param);
218 // Return the appropriate error code for "service does not exist"
219 $result = hexdec(self::WIN32_ERROR_SERVICE_DOES_NOT_EXIST);
220 } else {
221 // For other exceptions, log and return false
222 Util::logTrace("Unhandled Win32ServiceException: " . $e->getMessage());
223 $result = false;
224 }
225 } catch (\Exception $e) {
226 // Catch any other exceptions to prevent application freeze
227 Util::logTrace("Exception caught in callWin32Service: " . $e->getMessage());
228 $result = false;
229 } catch (\Throwable $e) {
230 // Catch any other throwable (PHP 7+) to prevent application freeze
231 Util::logTrace("Throwable caught in callWin32Service: " . $e->getMessage());
232 $result = false;
233 }
234 }
235 } else {
236 if (!isset(self::$loggedFunctions[$function])) {
237 Util::logTrace('Win32 function: ' . $function . ' missing');
238 self::$loggedFunctions[$function] = true;
239 }
240 }
241 return $result;
242 }
243
251 public function status($timeout = true): string
252 {
253 usleep( self::SLEEP_TIME );
254
255 $this->latestStatus = self::WIN32_SERVICE_NA;
256 $maxtime = time() + self::PENDING_TIMEOUT;
257
258 Util::logTrace("Querying status for service: " . $this->getName() . " (timeout: " . ($timeout ? "enabled" : "disabled") . ")");
259 if ($timeout) {
260 Util::logTrace("Max timeout time set to: " . date('Y-m-d H:i:s', $maxtime));
261 }
262
263 // Add a safety counter to prevent infinite loops
264 $loopCount = 0;
265 $maxLoops = 5; // Maximum number of attempts
266 $startTime = microtime(true);
267
268 try {
269 while ( ($this->latestStatus == self::WIN32_SERVICE_NA || $this->isPending( $this->latestStatus )) && $loopCount < $maxLoops ) {
270 $loopCount++;
271 Util::logTrace("Calling win32_query_service_status for service: " . $this->getName() . " (attempt " . $loopCount . " of " . $maxLoops . ")");
272
273 // Add a timeout check before making the call
274 if (microtime(true) - $startTime > 10) { // 10 seconds overall timeout
275 Util::logTrace("Overall timeout reached before making service status call");
276 break;
277 }
278
279 $this->latestStatus = $this->callWin32Service( 'win32_query_service_status', $this->getName() );
280
281 if ( is_array( $this->latestStatus ) && isset( $this->latestStatus['CurrentState'] ) ) {
282 // Ensure proper type conversion for PHP 8.2.3 compatibility
283 $stateInt = is_numeric($this->latestStatus['CurrentState']) ? (int)$this->latestStatus['CurrentState'] : 0;
284 $this->latestStatus = dechex( $stateInt );
285 Util::logTrace("Service status returned as array, CurrentState: " . $this->latestStatus);
286 }
287 elseif ( $this->latestStatus !== null ) {
288 // Ensure proper type conversion for PHP 8.2.3 compatibility
289 $statusInt = is_numeric($this->latestStatus) ? (int)$this->latestStatus : 0;
290 $statusHex = dechex( $statusInt );
291 Util::logTrace("Service status returned as value: " . $statusHex);
292
293 if ( $statusHex == self::WIN32_ERROR_SERVICE_DOES_NOT_EXIST ) {
294 $this->latestStatus = $statusHex;
295 Util::logTrace("Service does not exist, breaking loop");
296 break; // Exit the loop immediately if service doesn't exist
297 }
298 } else {
299 Util::logTrace("Service status query returned null");
300 // If we get a null result, assume service does not exist to avoid hanging
301 if ($loopCount >= 2) { // Only do this after at least one retry
302 Util::logTrace("Multiple null results, assuming service does not exist");
303 $this->latestStatus = self::WIN32_ERROR_SERVICE_DOES_NOT_EXIST;
304 break;
305 }
306 }
307
308 if ( $timeout && $maxtime < time() ) {
309 Util::logTrace("Timeout reached while querying service status");
310 break;
311 }
312
313 // Only sleep if we're going to loop again
314 if ($loopCount < $maxLoops && ($this->latestStatus == self::WIN32_SERVICE_NA || $this->isPending($this->latestStatus))) {
315 Util::logTrace("Sleeping before next status check attempt");
316 usleep(self::SLEEP_TIME);
317 }
318 }
319 } catch (\Exception $e) {
320 Util::logTrace("Exception in status method: " . $e->getMessage());
321 // If an exception occurs, assume service does not exist
322 $this->latestStatus = self::WIN32_ERROR_SERVICE_DOES_NOT_EXIST;
323 } catch (\Throwable $e) {
324 Util::logTrace("Throwable in status method: " . $e->getMessage());
325 // If a throwable occurs, assume service does not exist
326 $this->latestStatus = self::WIN32_ERROR_SERVICE_DOES_NOT_EXIST;
327 }
328
329 if ($loopCount >= $maxLoops) {
330 Util::logTrace("Maximum query attempts reached for service: " . $this->getName());
331 }
332
333 $elapsedTime = microtime(true) - $startTime;
334 Util::logTrace("Status check completed in " . round($elapsedTime, 2) . " seconds after " . $loopCount . " attempts");
335
336 if ( $this->latestStatus == self::WIN32_ERROR_SERVICE_DOES_NOT_EXIST ) {
337 $this->latestError = $this->latestStatus;
338 $this->latestStatus = self::WIN32_SERVICE_NA;
339 Util::logTrace("Service does not exist, setting status to NA");
340 }
341
342 Util::logTrace("Final status for service " . $this->getName() . ": " . $this->latestStatus);
343 return $this->latestStatus;
344 }
345
351 public function create(): bool
352 {
353 global $bearsamppBins;
354
355 Util::logTrace("Starting Win32Service::create for service: " . $this->getName());
356
357 if ( $this->getName() == BinPostgresql::SERVICE_NAME ) {
358 Util::logTrace("PostgreSQL service detected - using specialized installation");
359 $bearsamppBins->getPostgresql()->rebuildConf();
360 Util::logTrace("PostgreSQL configuration rebuilt");
361
362 $bearsamppBins->getPostgresql()->initData();
363 Util::logTrace("PostgreSQL data initialized");
364
366 Util::logTrace("PostgreSQL service installation " . ($result ? "succeeded" : "failed"));
367 return $result;
368 }
369
370 if ( $this->getNssm() instanceof Nssm ) {
371 Util::logTrace("Using NSSM for service installation");
372
373 $nssmEnvPath = Util::getAppBinsRegKey( false );
374 Util::logTrace("NSSM environment path (bins): " . $nssmEnvPath);
375
376 $nssmEnvPath .= Util::getNssmEnvPaths();
377 Util::logTrace("NSSM environment path (with additional paths): " . $nssmEnvPath);
378
379 $nssmEnvPath .= '%SystemRoot%/system32;';
380 $nssmEnvPath .= '%SystemRoot%;';
381 $nssmEnvPath .= '%SystemRoot%/system32/Wbem;';
382 $nssmEnvPath .= '%SystemRoot%/system32/WindowsPowerShell/v1.0';
383 Util::logTrace("NSSM final environment PATH: " . $nssmEnvPath);
384
385 $this->getNssm()->setEnvironmentExtra( 'PATH=' . $nssmEnvPath );
386 Util::logTrace("NSSM service parameters:");
387 Util::logTrace("-> Name: " . $this->getNssm()->getName());
388 Util::logTrace("-> DisplayName: " . $this->getNssm()->getDisplayName());
389 Util::logTrace("-> BinPath: " . $this->getNssm()->getBinPath());
390 Util::logTrace("-> Params: " . $this->getNssm()->getParams());
391 Util::logTrace("-> Start: " . $this->getNssm()->getStart());
392 Util::logTrace("-> Stdout: " . $this->getNssm()->getStdout());
393 Util::logTrace("-> Stderr: " . $this->getNssm()->getStderr());
394
395 $result = $this->getNssm()->create();
396 Util::logTrace("NSSM service creation " . ($result ? "succeeded" : "failed"));
397 if (!$result) {
398 Util::logTrace("NSSM error: " . $this->getNssm()->getLatestError());
399 }
400 return $result;
401 }
402
403 Util::logTrace("Using win32_create_service for service installation");
404 $serviceParams = array(
405 'service' => $this->getName(),
406 'display' => $this->getDisplayName(),
407 'description' => $this->getDisplayName(),
408 'path' => $this->getBinPath(),
409 'params' => $this->getParams(),
410 'start_type' => $this->getStartType() != null ? $this->getStartType() : self::SERVICE_DEMAND_START,
411 'error_control' => $this->getErrorControl() != null ? $this->getErrorControl() : self::SERVER_ERROR_NORMAL,
412 );
413
414 Util::logTrace("win32_create_service parameters:");
415 foreach ($serviceParams as $key => $value) {
416 Util::logTrace("-> $key: $value");
417 }
418
419 $result = $this->callWin32Service( 'win32_create_service', $serviceParams, true );
420 // Ensure proper type conversion for PHP 8.2.3 compatibility
421 $resultInt = is_numeric($result) ? (int)$result : 0;
422 $create = $result !== null ? dechex( $resultInt ) : '0';
423 Util::logTrace("win32_create_service result code: " . $create);
424
425 $this->writeLog( 'Create service: ' . $create . ' (status: ' . $this->status() . ')' );
426 $this->writeLog( '-> service: ' . $this->getName() );
427 $this->writeLog( '-> display: ' . $this->getDisplayName() );
428 $this->writeLog( '-> description: ' . $this->getDisplayName() );
429 $this->writeLog( '-> path: ' . $this->getBinPath() );
430 $this->writeLog( '-> params: ' . $this->getParams() );
431 $this->writeLog( '-> start_type: ' . ($this->getStartType() != null ? $this->getStartType() : self::SERVICE_DEMAND_START) );
432 $this->writeLog( '-> service: ' . ($this->getErrorControl() != null ? $this->getErrorControl() : self::SERVER_ERROR_NORMAL) );
433
434 if ( $create != self::WIN32_NO_ERROR ) {
435 Util::logTrace("Service creation failed with error code: " . $create);
436 return false;
437 }
438 elseif ( !$this->isInstalled() ) {
439 Util::logTrace("Service created but not found as installed");
440 $this->latestError = self::WIN32_NO_ERROR;
441 return false;
442 }
443
444 Util::logTrace("Service created successfully: " . $this->getName());
445 return true;
446 }
447
453 public function delete(): bool
454 {
455 Util::logTrace("Starting Win32Service::delete for service: " . $this->getName());
456 Util::logTrace("Checking if service is installed: " . $this->getName());
457
458 if ( !$this->isInstalled() ) {
459 Util::logTrace("Service is not installed, skipping deletion: " . $this->getName());
460 return true;
461 }
462
463 Util::logTrace("Stopping service before deletion: " . $this->getName());
464 $this->stop();
465
466 if ( $this->getName() == BinPostgresql::SERVICE_NAME ) {
467 Util::logTrace("PostgreSQL service detected - using specialized uninstallation");
469 Util::logTrace("PostgreSQL service uninstallation " . ($result ? "succeeded" : "failed"));
470 return $result;
471 }
472
473 Util::logTrace("Calling win32_delete_service for service: " . $this->getName());
474 $result = $this->callWin32Service( 'win32_delete_service', $this->getName(), true );
475 // Ensure proper type conversion for PHP 8.2.3 compatibility
476 $resultInt = is_numeric($result) ? (int)$result : 0;
477 $delete = $result !== null ? dechex( $resultInt ) : '0';
478 Util::logTrace("Delete service result code: " . $delete);
479 $this->writeLog( 'Delete service ' . $this->getName() . ': ' . $delete . ' (status: ' . $this->status() . ')' );
480
481 if ( $delete != self::WIN32_NO_ERROR && $delete != self::WIN32_ERROR_SERVICE_DOES_NOT_EXIST ) {
482 return false;
483 }
484 elseif ( $this->isInstalled() ) {
485 $this->latestError = self::WIN32_NO_ERROR;
486
487 return false;
488 }
489
490 return true;
491 }
492
498 public function reset(): bool
499 {
500 if ( $this->delete() ) {
501 usleep( self::SLEEP_TIME );
502
503 return $this->create();
504 }
505
506 return false;
507 }
508
514 public function start(): bool
515 {
516 global $bearsamppBins;
517
518 Util::logInfo('Attempting to start service: ' . $this->getName());
519
520 if ( $this->getName() == BinMysql::SERVICE_NAME ) {
521 $bearsamppBins->getMysql()->initData();
522 }
523 elseif ( $this->getName() == BinMariadb::SERVICE_NAME ) {
524 $bearsamppBins->getMariadb()->initData();
525 }
526 elseif ( $this->getName() == BinMailpit::SERVICE_NAME ) {
527 $bearsamppBins->getMailpit()->rebuildConf();
528 }
529 elseif ( $this->getName() == BinMemcached::SERVICE_NAME ) {
530 $bearsamppBins->getMemcached()->rebuildConf();
531 }
532 elseif ( $this->getName() == BinPostgresql::SERVICE_NAME ) {
533 $bearsamppBins->getPostgresql()->rebuildConf();
534 $bearsamppBins->getPostgresql()->initData();
535 }
536 elseif ( $this->getName() == BinXlight::SERVICE_NAME ) {
537 $bearsamppBins->getXlight()->rebuildConf();
538 }
539
540
541 $result = $this->callWin32Service( 'win32_start_service', $this->getName(), true );
542 // Ensure proper type conversion for PHP 8.2.3 compatibility
543 $resultInt = is_numeric($result) ? (int)$result : 0;
544 $start = $result !== null ? dechex( $resultInt ) : '0';
545 Util::logDebug( 'Start service ' . $this->getName() . ': ' . $start . ' (status: ' . $this->status() . ')' );
546
547 if ( $start != self::WIN32_NO_ERROR && $start != self::WIN32_ERROR_SERVICE_ALREADY_RUNNING ) {
548
549 // Write error to log
550 Util::logError('Failed to start service: ' . $this->getName() . ' with error code: ' . $start);
551
552 if ( $this->getName() == BinApache::SERVICE_NAME ) {
553 $cmdOutput = $bearsamppBins->getApache()->getCmdLineOutput( BinApache::CMD_SYNTAX_CHECK );
554 if ( !$cmdOutput['syntaxOk'] ) {
555 file_put_contents(
556 $bearsamppBins->getApache()->getErrorLog(),
557 '[' . date( 'Y-m-d H:i:s', time() ) . '] [error] ' . $cmdOutput['content'] . PHP_EOL,
558 FILE_APPEND
559 );
560 }
561 }
562 elseif ( $this->getName() == BinMysql::SERVICE_NAME ) {
563 $cmdOutput = $bearsamppBins->getMysql()->getCmdLineOutput( BinMysql::CMD_SYNTAX_CHECK );
564 if ( !$cmdOutput['syntaxOk'] ) {
565 file_put_contents(
566 $bearsamppBins->getMysql()->getErrorLog(),
567 '[' . date( 'Y-m-d H:i:s', time() ) . '] [error] ' . $cmdOutput['content'] . PHP_EOL,
568 FILE_APPEND
569 );
570 }
571 }
572 elseif ( $this->getName() == BinMariadb::SERVICE_NAME ) {
573 $cmdOutput = $bearsamppBins->getMariadb()->getCmdLineOutput( BinMariadb::CMD_SYNTAX_CHECK );
574 if ( !$cmdOutput['syntaxOk'] ) {
575 file_put_contents(
576 $bearsamppBins->getMariadb()->getErrorLog(),
577 '[' . date( 'Y-m-d H:i:s', time() ) . '] [error] ' . $cmdOutput['content'] . PHP_EOL,
578 FILE_APPEND
579 );
580 }
581 }
582
583 return false;
584 }
585 elseif ( !$this->isRunning() ) {
586 $this->latestError = self::WIN32_NO_ERROR;
587 Util::logError('Service ' . $this->getName() . ' is not running after start attempt.');
588 $this->latestError = null;
589 return false;
590 }
591
592 Util::logInfo('Service ' . $this->getName() . ' started successfully.');
593 return true;
594 }
595
601 public function stop(): bool
602 {
603 Util::logTrace("Starting Win32Service::stop for service: " . $this->getName());
604
605 Util::logTrace("Calling win32_stop_service for service: " . $this->getName());
606 $result = $this->callWin32Service( 'win32_stop_service', $this->getName(), true );
607
608 // Ensure proper type conversion for PHP 8.2.3 compatibility
609 $resultInt = is_numeric($result) ? (int)$result : 0;
610 $stop = $result !== null ? dechex( $resultInt ) : '0';
611 Util::logTrace("Stop service result code: " . $stop);
612
613 Util::logTrace("Checking current status after stop attempt");
614 $currentStatus = $this->status();
615 Util::logTrace("Current status: " . $currentStatus);
616
617 $this->writeLog( 'Stop service ' . $this->getName() . ': ' . $stop . ' (status: ' . $currentStatus . ')' );
618
619 if ( $stop != self::WIN32_NO_ERROR ) {
620 return false;
621 }
622 elseif ( !$this->isStopped() ) {
623 $this->latestError = self::WIN32_NO_ERROR;
624
625 return false;
626 }
627
628 return true;
629 }
630
636 public function restart(): bool
637 {
638 if ( $this->stop() ) {
639 return $this->start();
640 }
641
642 return false;
643 }
644
658 private function execHidden($command)
659 {
660 // Use proc_open with hidden window flags
661 $descriptorspec = [
662 0 => ['pipe', 'r'], // stdin
663 1 => ['pipe', 'w'], // stdout
664 2 => ['pipe', 'w'] // stderr
665 ];
666
667 // On Windows, use CREATE_NO_WINDOW flag to hide console
668 $options = ['bypass_shell' => true];
669
670 $process = @proc_open($command, $descriptorspec, $pipes, null, null, $options);
671
672 if (!is_resource($process)) {
673 return false;
674 }
675
676 // Close stdin
677 fclose($pipes[0]);
678
679 // Read stdout
680 $output = stream_get_contents($pipes[1]);
681 fclose($pipes[1]);
682
683 // Read stderr
684 $errors = stream_get_contents($pipes[2]);
685 fclose($pipes[2]);
686
687 // Close process
688 proc_close($process);
689
690 // Combine output and errors for sc.exe
691 if (!empty($errors)) {
692 $output .= "\n" . $errors;
693 }
694
695 return $output;
696 }
697
698 public function fastServiceCheck()
699 {
700 Util::logTrace("Starting fastServiceCheck for service: " . $this->getName());
701
702 $startTime = microtime(true);
703
704 // Use sc.exe to query service - this is very fast and reliable
705 // Execute with hidden window to prevent command prompt flash
706 $command = 'sc query "' . $this->getName() . '"';
707 Util::logTrace("Executing command: " . $command);
708
709 $output = $this->execHidden($command);
710 $duration = round(microtime(true) - $startTime, 3);
711
712 Util::logTrace("sc.exe query completed in " . $duration . "s");
713
714 if ($output === null || $output === false) {
715 Util::logTrace("sc.exe returned null/false, service likely doesn't exist");
716 return false;
717 }
718
719 // Check if service doesn't exist
720 if (stripos($output, 'does not exist') !== false ||
721 stripos($output, 'FAILED') !== false ||
722 stripos($output, '1060') !== false) { // Error code 1060 = service doesn't exist
723 Util::logTrace("Service doesn't exist: " . $this->getName());
724 return false;
725 }
726
727 // Service exists - parse basic info
728 $serviceInfo = [];
729
730 // Extract service name
731 if (preg_match('/SERVICE_NAME:\s*(.+)/i', $output, $matches)) {
732 $serviceInfo[self::VBS_NAME] = trim($matches[1]);
733 }
734
735 // Extract display name
736 if (preg_match('/DISPLAY_NAME:\s*(.+)/i', $output, $matches)) {
737 $serviceInfo[self::VBS_DISPLAY_NAME] = trim($matches[1]);
738 }
739
740 // Extract state
741 if (preg_match('/STATE\s*:\s*\d+\s+(\w+)/i', $output, $matches)) {
742 $state = trim($matches[1]);
743 $serviceInfo[self::VBS_STATE] = $state;
744 Util::logTrace("Service state: " . $state);
745 }
746
747 // If we have basic info, service exists - get full details if needed
748 if (!empty($serviceInfo)) {
749 Util::logTrace("Service exists, getting full details");
750
751 // Use sc qc to get configuration details (including path)
752 $configCommand = 'sc qc "' . $this->getName() . '"';
753 $configOutput = $this->execHidden($configCommand);
754
755 if ($configOutput !== null && $configOutput !== false && preg_match('/BINARY_PATH_NAME\s*:\s*(.+)/i', $configOutput, $matches)) {
756 $serviceInfo[self::VBS_PATH_NAME] = trim($matches[1]);
757 Util::logTrace("Service path: " . $serviceInfo[self::VBS_PATH_NAME]);
758 }
759
760 // Get description if available
761 if ($configOutput !== null && $configOutput !== false && preg_match('/DISPLAY_NAME\s*:\s*(.+)/i', $configOutput, $matches)) {
762 $serviceInfo[self::VBS_DESCRIPTION] = trim($matches[1]);
763 }
764
765 Util::logTrace("Fast service check successful for: " . $this->getName());
766 return $serviceInfo;
767 }
768
769 Util::logTrace("Could not parse service info from sc.exe output");
770 return false;
771 }
772
779 public function infos()
780 {
781 Util::logTrace("Starting Win32Service::infos for service: " . $this->getName());
782
783 try {
784 // Set a timeout for the entire operation
785 $startTime = microtime(true);
786 $timeout = 10; // 10 seconds timeout for the entire operation
787
788 if ($this->getNssm() instanceof Nssm) {
789 Util::logTrace("Using NSSM to get service info");
790 $result = $this->getNssm()->infos();
791 Util::logTrace("NSSM info retrieval completed in " . round(microtime(true) - $startTime, 2) . " seconds");
792 return $result;
793 }
794
795 // Performance optimization: Try fast sc.exe check first
796 Util::logTrace("Attempting fast service check using sc.exe");
797 $fastResult = $this->fastServiceCheck();
798
799 if ($fastResult !== false) {
800 $duration = round(microtime(true) - $startTime, 3);
801 Util::logTrace("Fast service check succeeded in " . $duration . "s (saved 5-10s)");
802 Util::logDebug("Performance: Fast service check used for " . $this->getName() . ", saved 5-10 seconds");
803 return $fastResult;
804 }
805
806 // Fast check returned false - service doesn't exist
807 if ($fastResult === false) {
808 $duration = round(microtime(true) - $startTime, 3);
809 Util::logTrace("Fast service check determined service doesn't exist in " . $duration . "s");
810 return false;
811 }
812
813 // Fallback to VBS (should rarely be needed now)
814 Util::logTrace("Falling back to VBS for service info");
815
816 // Use set_time_limit to prevent PHP script timeout
817 $originalTimeout = ini_get('max_execution_time');
818 set_time_limit(15); // 15 seconds timeout
819
820 // Create a separate process to get service info with a timeout
822
823 // Reset the timeout
824 set_time_limit($originalTimeout);
825
826 // Check if we've exceeded our timeout
827 if (microtime(true) - $startTime > $timeout) {
828 Util::logTrace("Timeout exceeded in infos() method, returning false");
829 return false;
830 }
831
832 Util::logTrace("VBS info retrieval completed in " . round(microtime(true) - $startTime, 2) . " seconds");
833 return $result;
834 } catch (\Exception $e) {
835 Util::logTrace("Exception in infos() method: " . $e->getMessage() . ", returning false");
836 return false;
837 } catch (\Throwable $e) {
838 Util::logTrace("Throwable in infos() method: " . $e->getMessage() . ", returning false");
839 return false;
840 }
841 }
842
848 public function isInstalled(): bool
849 {
850 Util::logTrace("Checking if service is installed: " . $this->getName());
851
852 try {
853 // Set a timeout for the entire operation
854 $startTime = microtime(true);
855 $timeout = 15; // 15 seconds timeout for the entire operation
856
857 // Call status() with a try-catch to ensure we don't get stuck
858 $status = $this->status();
859
860 // Check if we've exceeded our timeout
861 if (microtime(true) - $startTime > $timeout) {
862 Util::logTrace("Timeout exceeded in isInstalled() method, assuming service is not installed");
863 $this->writeLog('isInstalled ' . $this->getName() . ': NO (timeout exceeded)');
864 return false;
865 }
866
867 $isInstalled = $status != self::WIN32_SERVICE_NA;
868
869 Util::logTrace("Service " . $this->getName() . " installation status: " . ($isInstalled ? "YES" : "NO") . " (status code: " . $status . ")");
870 $this->writeLog('isInstalled ' . $this->getName() . ': ' . ($isInstalled ? 'YES' : 'NO') . ' (status: ' . $status . ')');
871
872 return $isInstalled;
873 } catch (\Exception $e) {
874 Util::logTrace("Exception in isInstalled() method: " . $e->getMessage() . ", assuming service is not installed");
875 $this->writeLog('isInstalled ' . $this->getName() . ': NO (exception: ' . $e->getMessage() . ')');
876 return false;
877 } catch (\Throwable $e) {
878 Util::logTrace("Throwable in isInstalled() method: " . $e->getMessage() . ", assuming service is not installed");
879 $this->writeLog('isInstalled ' . $this->getName() . ': NO (throwable: ' . $e->getMessage() . ')');
880 return false;
881 }
882 }
883
889 public function isRunning(): bool
890 {
891 Util::logTrace("Checking if service is running: " . $this->getName());
892
893 $status = $this->status();
894 $isRunning = $status == self::WIN32_SERVICE_RUNNING;
895
896 Util::logTrace("Service " . $this->getName() . " running status: " . ($isRunning ? "YES" : "NO") . " (status code: " . $status . ")");
897 $this->writeLog( 'isRunning ' . $this->getName() . ': ' . ($isRunning ? 'YES' : 'NO') . ' (status: ' . $status . ')' );
898
899 return $isRunning;
900 }
901
907 public function isStopped(): bool
908 {
909 Util::logTrace("Checking if service is stopped: " . $this->getName());
910
911 $status = $this->status();
912 $isStopped = $status == self::WIN32_SERVICE_STOPPED;
913
914 Util::logTrace("Service " . $this->getName() . " stopped status: " . ($isStopped ? "YES" : "NO") . " (status code: " . $status . ")");
915 $this->writeLog( 'isStopped ' . $this->getName() . ': ' . ($isStopped ? 'YES' : 'NO') . ' (status: ' . $status . ')' );
916
917 return $isStopped;
918 }
919
925 public function isPaused(): bool
926 {
927 Util::logTrace("Checking if service is paused: " . $this->getName());
928
929 $status = $this->status();
930 $isPaused = $status == self::WIN32_SERVICE_PAUSED;
931
932 Util::logTrace("Service " . $this->getName() . " paused status: " . ($isPaused ? "YES" : "NO") . " (status code: " . $status . ")");
933 $this->writeLog( 'isPaused ' . $this->getName() . ': ' . ($isPaused ? 'YES' : 'NO') . ' (status: ' . $status . ')' );
934
935 return $isPaused;
936 }
937
945 public function isPending($status): bool
946 {
947 $isPending = $status == self::WIN32_SERVICE_START_PENDING || $status == self::WIN32_SERVICE_STOP_PENDING
948 || $status == self::WIN32_SERVICE_CONTINUE_PENDING || $status == self::WIN32_SERVICE_PAUSE_PENDING;
949
950 Util::logTrace("Checking if status is pending: " . $status . " - Result: " . ($isPending ? "YES" : "NO"));
951
952 if ($isPending) {
953 if ($status == self::WIN32_SERVICE_START_PENDING) {
954 Util::logTrace("Service is in START_PENDING state");
955 } else if ($status == self::WIN32_SERVICE_STOP_PENDING) {
956 Util::logTrace("Service is in STOP_PENDING state");
957 } else if ($status == self::WIN32_SERVICE_CONTINUE_PENDING) {
958 Util::logTrace("Service is in CONTINUE_PENDING state");
959 } else if ($status == self::WIN32_SERVICE_PAUSE_PENDING) {
960 Util::logTrace("Service is in PAUSE_PENDING state");
961 }
962 }
963
964 return $isPending;
965 }
966
974 private function getWin32ServiceStatusDesc($status): ?string
975 {
976 switch ( $status ) {
977 case self::WIN32_SERVICE_CONTINUE_PENDING:
978 return 'The service continue is pending.';
979
980 case self::WIN32_SERVICE_PAUSE_PENDING:
981 return 'The service pause is pending.';
982
983 case self::WIN32_SERVICE_PAUSED:
984 return 'The service is paused.';
985
986 case self::WIN32_SERVICE_RUNNING:
987 return 'The service is running.';
988
989 case self::WIN32_SERVICE_START_PENDING:
990 return 'The service is starting.';
991
992 case self::WIN32_SERVICE_STOP_PENDING:
993 return 'The service is stopping.';
994
995 case self::WIN32_SERVICE_STOPPED:
996 return 'The service is not running.';
997
998 case self::WIN32_SERVICE_NA:
999 return 'Cannot retrieve service status.';
1000
1001 default:
1002 return null;
1003 }
1004 }
1005
1013 private function getWin32ErrorCodeDesc($code): ?string
1014 {
1015 switch ( $code ) {
1016 case self::WIN32_ERROR_ACCESS_DENIED:
1017 return 'The handle to the SCM database does not have the appropriate access rights.';
1018 // ... other cases ...
1019 default:
1020 return null;
1021 }
1022 }
1023
1029 public function getName(): string
1030 {
1031 return $this->name;
1032 }
1033
1039 public function setName($name): void
1040 {
1041 $this->name = $name;
1042 }
1043
1049 public function getDisplayName(): string
1050 {
1051 return $this->displayName;
1052 }
1053
1059 public function setDisplayName($displayName): void
1060 {
1061 $this->displayName = $displayName;
1062 }
1063
1069 public function getBinPath(): string
1070 {
1071 return $this->binPath;
1072 }
1073
1079 public function setBinPath($binPath): void
1080 {
1081 $this->binPath = str_replace( '"', '', Util::formatWindowsPath( $binPath ) );
1082 }
1083
1089 public function getParams(): string
1090 {
1091 return $this->params;
1092 }
1093
1099 public function setParams($params): void
1100 {
1101 $this->params = $params;
1102 }
1103
1109 public function getStartType(): string
1110 {
1111 return $this->startType;
1112 }
1113
1119 public function setStartType($startType): void
1120 {
1121 $this->startType = $startType;
1122 }
1123
1129 public function getErrorControl(): string
1130 {
1131 return $this->errorControl;
1132 }
1133
1139 public function setErrorControl($errorControl): void
1140 {
1141 $this->errorControl = $errorControl;
1142 }
1143
1149 public function getNssm()
1150 {
1151 return $this->nssm;
1152 }
1153
1159 public function setNssm($nssm)
1160 {
1161 if ( $nssm instanceof Nssm ) {
1162 $this->setDisplayName( $nssm->getDisplayName() );
1163 $this->setBinPath( $nssm->getBinPath() );
1164 $this->setParams( $nssm->getParams() );
1165 $this->setStartType( $nssm->getStart() );
1166 $this->nssm = $nssm;
1167 }
1168 }
1169
1175 public function getLatestStatus()
1176 {
1177 return $this->latestStatus;
1178 }
1179
1185 public function getLatestError()
1186 {
1187 return $this->latestError;
1188 }
1189
1195 public function getError()
1196 {
1197 global $bearsamppLang;
1198 if ( $this->latestError != self::WIN32_NO_ERROR ) {
1199 // Ensure proper type conversion for PHP 8.2.3 compatibility
1200 $errorInt = is_numeric($this->latestError) ? hexdec( $this->latestError ) : 0;
1201 return $bearsamppLang->getValue( Lang::ERROR ) . ' ' .
1202 $this->latestError . ' (' . $errorInt . ' : ' . $this->getWin32ErrorCodeDesc( $this->latestError ) . ')';
1203 }
1204 elseif ( $this->latestStatus != self::WIN32_SERVICE_NA ) {
1205 // Ensure proper type conversion for PHP 8.2.3 compatibility
1206 $statusInt = is_numeric($this->latestStatus) ? hexdec( $this->latestStatus ) : 0;
1207 return $bearsamppLang->getValue( Lang::STATUS ) . ' ' .
1208 $this->latestStatus . ' (' . $statusInt . ' : ' . $this->getWin32ServiceStatusDesc( $this->latestStatus ) . ')';
1209 }
1210
1211 return null;
1212 }
1213}
$result
global $bearsamppBins
global $bearsamppLang
global $bearsamppRoot
static installPostgresqlService()
static uninstallPostgresqlService()
const CMD_SYNTAX_CHECK
const SERVICE_NAME
const CMD_SYNTAX_CHECK
const ERROR
const STATUS
static logError($data, $file=null)
static logInitClass($classInstance)
static logTrace($data, $file=null)
static logInfo($data, $file=null)
static getAppBinsRegKey($fromRegistry=true)
static logDebug($data, $file=null)
static getNssmEnvPaths()
static formatWindowsPath($path)
static getServiceInfos($serviceName)
setErrorControl($errorControl)
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
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