Bearsampp 2026.5.5
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 Log::initClass( $this );
99 $this->name = $name;
100 }
101
107 private function writeLog($log): void
108 {
109 global $bearsamppRoot;
110 Log::debug( $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 Log::trace('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 Log::trace("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 Log::trace("Win32ServiceException caught: " . $e->getMessage());
174
175 // Handle "service does not exist" exception
176 if (strpos($e->getMessage(), 'service does not exist') !== false) {
177 Log::trace("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 Log::trace("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 Log::trace("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 Log::trace("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 Log::trace("Win32ServiceException caught: " . $e->getMessage());
214
215 // Handle "service does not exist" exception
216 if (strpos($e->getMessage(), 'service does not exist') !== false) {
217 Log::trace("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 Log::trace("Unhandled Win32ServiceException: " . $e->getMessage());
223 $result = false;
224 }
225 } catch (\Exception $e) {
226 // Catch any other exceptions to prevent application freeze
227 Log::trace("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 Log::trace("Throwable caught in callWin32Service: " . $e->getMessage());
232 $result = false;
233 }
234 }
235 } else {
236 if (!isset(self::$loggedFunctions[$function])) {
237 Log::trace('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 Log::trace("Querying status for service: " . $this->getName() . " (timeout: " . ($timeout ? "enabled" : "disabled") . ")");
259 if ($timeout) {
260 Log::trace("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 Log::trace("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 Log::trace("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 Log::trace("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 Log::trace("Service status returned as value: " . $statusHex);
292
293 if ( $statusHex == self::WIN32_ERROR_SERVICE_DOES_NOT_EXIST ) {
294 $this->latestStatus = $statusHex;
295 Log::trace("Service does not exist, breaking loop");
296 break; // Exit the loop immediately if service doesn't exist
297 }
298 } else {
299 Log::trace("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 {
303 Log::trace("Multiple null results, assuming service does not exist");
304 $this->latestStatus = self::WIN32_ERROR_SERVICE_DOES_NOT_EXIST;
305 break;
306 }
307 }
308
309 if ( $timeout && $maxtime < time() ) {
310 Log::trace("Timeout reached while querying service status");
311 break;
312 }
313
314 // Only sleep if we're going to loop again
315 if ($loopCount < $maxLoops && ($this->latestStatus == self::WIN32_SERVICE_NA || $this->isPending($this->latestStatus))) {
316 Log::trace("Sleeping before next status check attempt");
317 usleep(self::SLEEP_TIME);
318 }
319 }
320 } catch (\Exception $e) {
321 Log::trace("Exception in status method: " . $e->getMessage());
322 // If an exception occurs, assume service does not exist
323 $this->latestStatus = self::WIN32_ERROR_SERVICE_DOES_NOT_EXIST;
324 } catch (\Throwable $e) {
325 Log::trace("Throwable in status method: " . $e->getMessage());
326 // If a throwable occurs, assume service does not exist
327 $this->latestStatus = self::WIN32_ERROR_SERVICE_DOES_NOT_EXIST;
328 }
329
330 if ($loopCount >= $maxLoops) {
331 Log::trace("Maximum query attempts reached for service: " . $this->getName());
332 }
333
334 $elapsedTime = microtime(true) - $startTime;
335 Log::trace("Status check completed in " . round($elapsedTime, 2) . " seconds after " . $loopCount . " attempts");
336
337 if ( $this->latestStatus == self::WIN32_ERROR_SERVICE_DOES_NOT_EXIST ) {
338 $this->latestError = $this->latestStatus;
339 $this->latestStatus = self::WIN32_SERVICE_NA;
340 Log::trace("Service does not exist, setting status to NA");
341 }
342
343 Log::trace("Final status for service " . $this->getName() . ": " . $this->latestStatus);
344 return $this->latestStatus;
345 }
346
352 public function create(): bool
353 {
354 global $bearsamppBins;
355
356 Log::trace("Starting Win32Service::create for service: " . $this->getName());
357
358 if ( $this->getName() == BinPostgresql::SERVICE_NAME ) {
359 Log::trace("PostgreSQL service detected - using specialized installation");
360 $bearsamppBins->getPostgresql()->rebuildConf();
361 Log::trace("PostgreSQL configuration rebuilt");
362
363 $bearsamppBins->getPostgresql()->initData();
364 Log::trace("PostgreSQL data initialized");
365
367 Log::trace("PostgreSQL service installation " . ($result ? "succeeded" : "failed"));
368 return $result;
369 }
370
371 if ( $this->getNssm() instanceof Nssm ) {
372 Log::trace("Using NSSM for service installation");
373
374 $nssmEnvPath = Util::getAppBinsRegKey( false );
375 Log::trace("NSSM environment path (bins): " . $nssmEnvPath);
376
377 $nssmEnvPath .= Nssm::getNssmEnvPaths();
378 Log::trace("NSSM environment path (with additional paths): " . $nssmEnvPath);
379
380 $nssmEnvPath .= '%SystemRoot%/system32;';
381 $nssmEnvPath .= '%SystemRoot%;';
382 $nssmEnvPath .= '%SystemRoot%/system32/Wbem;';
383 $nssmEnvPath .= '%SystemRoot%/system32/WindowsPowerShell/v1.0';
384 Log::trace("NSSM final environment PATH: " . $nssmEnvPath);
385
386 $this->getNssm()->setEnvironmentExtra( 'PATH=' . $nssmEnvPath );
387 Log::trace("NSSM service parameters:");
388 Log::trace("-> Name: " . $this->getNssm()->getName());
389 Log::trace("-> DisplayName: " . $this->getNssm()->getDisplayName());
390 Log::trace("-> BinPath: " . $this->getNssm()->getBinPath());
391 Log::trace("-> Params: " . $this->getNssm()->getParams());
392 Log::trace("-> Start: " . $this->getNssm()->getStart());
393 Log::trace("-> Stdout: " . $this->getNssm()->getStdout());
394 Log::trace("-> Stderr: " . $this->getNssm()->getStderr());
395
396 $result = $this->getNssm()->create();
397 Log::trace("NSSM service creation " . ($result ? "succeeded" : "failed"));
398 if (!$result) {
399 Log::trace("NSSM error: " . $this->getNssm()->getLatestError());
400 }
401 return $result;
402 }
403
404 Log::trace("Using win32_create_service for service installation");
405 $serviceParams = array(
406 'service' => $this->getName(),
407 'display' => $this->getDisplayName(),
408 'description' => $this->getDisplayName(),
409 'path' => $this->getBinPath(),
410 'params' => $this->getParams(),
411 'start_type' => $this->getStartType() != null ? $this->getStartType() : self::SERVICE_DEMAND_START,
412 'error_control' => $this->getErrorControl() != null ? $this->getErrorControl() : self::SERVER_ERROR_NORMAL,
413 );
414
415 Log::trace("win32_create_service parameters:");
416 foreach ($serviceParams as $key => $value) {
417 Log::trace("-> $key: $value");
418 }
419
420 $result = $this->callWin32Service( 'win32_create_service', $serviceParams, true );
421 // Ensure proper type conversion for PHP 8.2.3 compatibility
422 $resultInt = is_numeric($result) ? (int)$result : 0;
423 $create = $result !== null ? dechex( $resultInt ) : '0';
424 Log::trace("win32_create_service result code: " . $create);
425
426 // Retry once if the SCM has the service marked for deletion from a recent delete()
427 if ( $create == self::WIN32_ERROR_SERVICE_MARKED_FOR_DELETE ) {
428 Log::trace("Service marked for delete, waiting 2s before retry: " . $this->getName());
429 usleep( 2000000 );
430 $result = $this->callWin32Service( 'win32_create_service', $serviceParams, true );
431 $resultInt = is_numeric($result) ? (int)$result : 0;
432 $create = $result !== null ? dechex( $resultInt ) : '0';
433 Log::trace("win32_create_service retry result code: " . $create);
434 }
435
436 $this->writeLog( 'Create service: ' . $create . ' (status: ' . $this->status() . ')' );
437 $this->writeLog( '-> service: ' . $this->getName() );
438 $this->writeLog( '-> display: ' . $this->getDisplayName() );
439 $this->writeLog( '-> description: ' . $this->getDisplayName() );
440 $this->writeLog( '-> path: ' . $this->getBinPath() );
441 $this->writeLog( '-> params: ' . $this->getParams() );
442 $this->writeLog( '-> start_type: ' . ($this->getStartType() != null ? $this->getStartType() : self::SERVICE_DEMAND_START) );
443 $this->writeLog( '-> service: ' . ($this->getErrorControl() != null ? $this->getErrorControl() : self::SERVER_ERROR_NORMAL) );
444
445 if ( $create != self::WIN32_NO_ERROR ) {
446 Log::trace("Service creation failed with error code: " . $create);
447 return false;
448 }
449 elseif ( !$this->isInstalled() ) {
450 Log::trace("Service created but not found as installed");
451 $this->latestError = self::WIN32_NO_ERROR;
452 return false;
453 }
454
455 Log::trace("Service created successfully: " . $this->getName());
456 return true;
457 }
458
464 public function delete(): bool
465 {
466 Log::trace("Starting Win32Service::delete for service: " . $this->getName());
467 Log::trace("Checking if service is installed: " . $this->getName());
468
469 if ( !$this->isInstalled() ) {
470 Log::trace("Service is not installed, skipping deletion: " . $this->getName());
471 return true;
472 }
473
474 Log::trace("Stopping service before deletion: " . $this->getName());
475 $this->stop();
476
477 if ( $this->getNssm() instanceof Nssm ) {
478 $childExe = basename( $this->getNssm()->getBinPath() );
479 Log::trace("Killing NSSM child process after stop: " . $childExe);
480 Win32Ps::killBins( [$childExe] );
481 }
482
483 if ( $this->getName() == BinPostgresql::SERVICE_NAME ) {
484 Log::trace("PostgreSQL service detected - using specialized uninstallation");
486 Log::trace("PostgreSQL service uninstallation " . ($result ? "succeeded" : "failed"));
487 return $result;
488 }
489
490 Log::trace("Calling win32_delete_service for service: " . $this->getName());
491 $result = $this->callWin32Service( 'win32_delete_service', $this->getName(), true );
492 // Ensure proper type conversion for PHP 8.2.3 compatibility
493 $resultInt = is_numeric($result) ? (int)$result : 0;
494 $delete = $result !== null ? dechex( $resultInt ) : '0';
495 Log::trace("Delete service result code: " . $delete);
496 $this->writeLog( 'Delete service ' . $this->getName() . ': ' . $delete . ' (status: ' . $this->status() . ')' );
497
498 if ( $delete != self::WIN32_NO_ERROR && $delete != self::WIN32_ERROR_SERVICE_DOES_NOT_EXIST ) {
499 return false;
500 }
501 elseif ( $this->isInstalled() ) {
502 $this->latestError = self::WIN32_NO_ERROR;
503
504 return false;
505 }
506
507 return true;
508 }
509
515 public function reset(): bool
516 {
517 if ( $this->delete() ) {
518 usleep( self::SLEEP_TIME );
519
520 return $this->create();
521 }
522
523 return false;
524 }
525
531 public function start(): bool
532 {
533 global $bearsamppBins;
534
535 Log::info('Attempting to start service: ' . $this->getName());
536
537 if ( $this->getName() == BinMysql::SERVICE_NAME ) {
538 $bearsamppBins->getMysql()->initData();
539 }
540 elseif ( $this->getName() == BinMariadb::SERVICE_NAME ) {
541 $bearsamppBins->getMariadb()->initData();
542 }
543 elseif ( $this->getName() == BinMailpit::SERVICE_NAME ) {
544 $bearsamppBins->getMailpit()->rebuildConf();
545 }
546 elseif ( $this->getName() == BinMemcached::SERVICE_NAME ) {
547 $bearsamppBins->getMemcached()->rebuildConf();
548 }
549 elseif ( $this->getName() == BinPostgresql::SERVICE_NAME ) {
550 $bearsamppBins->getPostgresql()->rebuildConf();
551 $bearsamppBins->getPostgresql()->initData();
552 }
553 elseif ( $this->getName() == BinXlight::SERVICE_NAME ) {
554 $bearsamppBins->getXlight()->rebuildConf();
555 }
556
557
558 $result = $this->callWin32Service( 'win32_start_service', $this->getName(), true );
559 // Ensure proper type conversion for PHP 8.2.3 compatibility
560 $resultInt = is_numeric($result) ? (int)$result : 0;
561 $start = $result !== null ? dechex( $resultInt ) : '0';
562 Log::debug( 'Start service ' . $this->getName() . ': ' . $start . ' (status: ' . $this->status() . ')' );
563
564 if ( $start != self::WIN32_NO_ERROR && $start != self::WIN32_ERROR_SERVICE_ALREADY_RUNNING ) {
565
566 // Write error to log
567 Log::error('Failed to start service: ' . $this->getName() . ' with error code: ' . $start);
568
569 if ( $this->getName() == BinApache::SERVICE_NAME ) {
570 $cmdOutput = $bearsamppBins->getApache()->getCmdLineOutput( BinApache::CMD_SYNTAX_CHECK );
571 if ( !$cmdOutput['syntaxOk'] ) {
572 file_put_contents(
573 $bearsamppBins->getApache()->getErrorLog(),
574 '[' . date( 'Y-m-d H:i:s', time() ) . '] [error] ' . $cmdOutput['content'] . PHP_EOL,
575 FILE_APPEND
576 );
577 }
578 }
579 elseif ( $this->getName() == BinMysql::SERVICE_NAME ) {
580 $cmdOutput = $bearsamppBins->getMysql()->getCmdLineOutput( BinMysql::CMD_SYNTAX_CHECK );
581 if ( !$cmdOutput['syntaxOk'] ) {
582 file_put_contents(
583 $bearsamppBins->getMysql()->getErrorLog(),
584 '[' . date( 'Y-m-d H:i:s', time() ) . '] [error] ' . $cmdOutput['content'] . PHP_EOL,
585 FILE_APPEND
586 );
587 }
588 }
589 elseif ( $this->getName() == BinMariadb::SERVICE_NAME ) {
590 $cmdOutput = $bearsamppBins->getMariadb()->getCmdLineOutput( BinMariadb::CMD_SYNTAX_CHECK );
591 if ( !$cmdOutput['syntaxOk'] ) {
592 file_put_contents(
593 $bearsamppBins->getMariadb()->getErrorLog(),
594 '[' . date( 'Y-m-d H:i:s', time() ) . '] [error] ' . $cmdOutput['content'] . PHP_EOL,
595 FILE_APPEND
596 );
597 }
598 }
599
600 return false;
601 }
602 elseif ( !$this->isRunning() ) {
603 $this->latestError = self::WIN32_NO_ERROR;
604 Log::error('Service ' . $this->getName() . ' is not running after start attempt.');
605 $this->latestError = null;
606 return false;
607 }
608
609 Log::info('Service ' . $this->getName() . ' started successfully.');
610 return true;
611 }
612
618 public function stop(): bool
619 {
620 Log::trace("Starting Win32Service::stop for service: " . $this->getName());
621
622 Log::trace("Calling win32_stop_service for service: " . $this->getName());
623 $result = $this->callWin32Service( 'win32_stop_service', $this->getName(), true );
624
625 // Ensure proper type conversion for PHP 8.2.3 compatibility
626 $resultInt = is_numeric($result) ? (int)$result : 0;
627 $stop = $result !== null ? dechex( $resultInt ) : '0';
628 Log::trace("Stop service result code: " . $stop);
629
630 Log::trace("Checking current status after stop attempt");
631 $currentStatus = $this->status();
632 Log::trace("Current status: " . $currentStatus);
633
634 $this->writeLog( 'Stop service ' . $this->getName() . ': ' . $stop . ' (status: ' . $currentStatus . ')' );
635
636 if ( $stop != self::WIN32_NO_ERROR ) {
637 return false;
638 }
639 elseif ( !$this->isStopped() ) {
640 $this->latestError = self::WIN32_NO_ERROR;
641
642 return false;
643 }
644
645 return true;
646 }
647
653 public function restart(): bool
654 {
655 if ( $this->stop() ) {
656 return $this->start();
657 }
658
659 return false;
660 }
661
668 public function fastServiceCheck()
669 {
670 Log::trace("Starting fastServiceCheck for service: " . $this->getName());
671
672 $startTime = microtime(true);
673
674 // Use sc.exe to query service - this is very fast and reliable
675 // Execute with hidden window to prevent command prompt flash
676 Log::trace("Executing: sc query " . $this->getName());
677
678 $output = CommandRunner::execCombined('sc', ['query', $this->getName()]);
679 $duration = round(microtime(true) - $startTime, 3);
680
681 Log::trace("sc.exe query completed in " . $duration . "s");
682
683 if ($output === null || $output === false) {
684 Log::trace("sc.exe returned null/false, service likely doesn't exist");
685 return false;
686 }
687
688 // Check if service doesn't exist
689 if (stripos($output, 'does not exist') !== false ||
690 stripos($output, 'FAILED') !== false ||
691 stripos($output, '1060') !== false) { // Error code 1060 = service doesn't exist
692 Log::trace("Service doesn't exist: " . $this->getName());
693 return false;
694 }
695
696 // Service exists - parse basic info
697 $serviceInfo = [];
698
699 // Extract service name
700 if (preg_match('/SERVICE_NAME:\s*(.+)/i', $output, $matches)) {
701 $serviceInfo[self::VBS_NAME] = trim($matches[1]);
702 }
703
704 // Extract display name
705 if (preg_match('/DISPLAY_NAME:\s*(.+)/i', $output, $matches)) {
706 $serviceInfo[self::VBS_DISPLAY_NAME] = trim($matches[1]);
707 }
708
709 // Extract state
710 if (preg_match('/STATE\s*:\s*\d+\s+(\w+)/i', $output, $matches)) {
711 $state = trim($matches[1]);
712 $serviceInfo[self::VBS_STATE] = $state;
713 Log::trace("Service state: " . $state);
714 }
715
716 // If we have basic info, service exists - get full details if needed
717 if (!empty($serviceInfo)) {
718 Log::trace("Service exists, getting full details");
719
720 // Use sc qc to get configuration details (including path)
721 $configOutput = CommandRunner::execCombined('sc', ['qc', $this->getName()]);
722
723 if ($configOutput !== null && $configOutput !== false && preg_match('/BINARY_PATH_NAME\s*:\s*(.+)/i', $configOutput, $matches)) {
724 $serviceInfo[self::VBS_PATH_NAME] = trim($matches[1]);
725 Log::trace("Service path: " . $serviceInfo[self::VBS_PATH_NAME]);
726 }
727
728 // Get description if available
729 if ($configOutput !== null && $configOutput !== false && preg_match('/DISPLAY_NAME\s*:\s*(.+)/i', $configOutput, $matches)) {
730 $serviceInfo[self::VBS_DESCRIPTION] = trim($matches[1]);
731 }
732
733 Log::trace("Fast service check successful for: " . $this->getName());
734 return $serviceInfo;
735 }
736
737 Log::trace("Could not parse service info from sc.exe output");
738 return false;
739 }
740
747 public function infos()
748 {
749 Log::trace("Starting Win32Service::infos for service: " . $this->getName());
750
751 try {
752 // Set a timeout for the entire operation
753 $startTime = microtime(true);
754 $timeout = 10; // 10 seconds timeout for the entire operation
755
756 if ($this->getNssm() instanceof Nssm) {
757 Log::trace("Using NSSM to get service info");
758 $result = $this->getNssm()->infos();
759 Log::trace("NSSM info retrieval completed in " . round(microtime(true) - $startTime, 2) . " seconds");
760 return $result;
761 }
762
763 // Performance optimization: Try fast sc.exe check first
764 Log::trace("Attempting fast service check using sc.exe");
765 $fastResult = $this->fastServiceCheck();
766
767 if ($fastResult !== false) {
768 $duration = round(microtime(true) - $startTime, 3);
769 Log::trace("Fast service check succeeded in " . $duration . "s (saved 5-10s)");
770 Log::debug("Performance: Fast service check used for " . $this->getName() . ", saved 5-10 seconds");
771 return $fastResult;
772 }
773
774 // Fast check returned false - service doesn't exist
775 if ($fastResult === false) {
776 $duration = round(microtime(true) - $startTime, 3);
777 Log::trace("Fast service check determined service doesn't exist in " . $duration . "s");
778 return false;
779 }
780
781 // Fallback to VBS (should rarely be needed now)
782 Log::trace("Falling back to VBS for service info");
783
784 // Use set_time_limit to prevent PHP script timeout
785 $originalTimeout = ini_get('max_execution_time');
786 set_time_limit(15); // 15 seconds timeout
787
788 // Create a separate process to get service info with a timeout
790
791 // Reset the timeout
792 set_time_limit($originalTimeout);
793
794 // Check if we've exceeded our timeout
795 if (microtime(true) - $startTime > $timeout) {
796 Log::trace("Timeout exceeded in infos() method, returning false");
797 return false;
798 }
799
800 Log::trace("VBS info retrieval completed in " . round(microtime(true) - $startTime, 2) . " seconds");
801 return $result;
802 } catch (\Exception $e) {
803 Log::trace("Exception in infos() method: " . $e->getMessage() . ", returning false");
804 return false;
805 } catch (\Throwable $e) {
806 Log::trace("Throwable in infos() method: " . $e->getMessage() . ", returning false");
807 return false;
808 }
809 }
810
816 public function isInstalled(): bool
817 {
818 Log::trace("Checking if service is installed: " . $this->getName());
819
820 try {
821 // Set a timeout for the entire operation
822 $startTime = microtime(true);
823 $timeout = 15; // 15 seconds timeout for the entire operation
824
825 // Call status() with a try-catch to ensure we don't get stuck
826 $status = $this->status();
827
828 // Check if we've exceeded our timeout
829 if (microtime(true) - $startTime > $timeout) {
830 Log::trace("Timeout exceeded in isInstalled() method, assuming service is not installed");
831 $this->writeLog('isInstalled ' . $this->getName() . ': NO (timeout exceeded)');
832 return false;
833 }
834
835 $isInstalled = $status != self::WIN32_SERVICE_NA;
836
837 Log::trace("Service " . $this->getName() . " installation status: " . ($isInstalled ? "YES" : "NO") . " (status code: " . $status . ")");
838 $this->writeLog('isInstalled ' . $this->getName() . ': ' . ($isInstalled ? 'YES' : 'NO') . ' (status: ' . $status . ')');
839
840 return $isInstalled;
841 } catch (\Exception $e) {
842 Log::trace("Exception in isInstalled() method: " . $e->getMessage() . ", assuming service is not installed");
843 $this->writeLog('isInstalled ' . $this->getName() . ': NO (exception: ' . $e->getMessage() . ')');
844 return false;
845 } catch (\Throwable $e) {
846 Log::trace("Throwable in isInstalled() method: " . $e->getMessage() . ", assuming service is not installed");
847 $this->writeLog('isInstalled ' . $this->getName() . ': NO (throwable: ' . $e->getMessage() . ')');
848 return false;
849 }
850 }
851
857 public function isRunning(): bool
858 {
859 Log::trace("Checking if service is running: " . $this->getName());
860
861 $status = $this->status();
862 $isRunning = $status == self::WIN32_SERVICE_RUNNING;
863
864 Log::trace("Service " . $this->getName() . " running status: " . ($isRunning ? "YES" : "NO") . " (status code: " . $status . ")");
865 $this->writeLog( 'isRunning ' . $this->getName() . ': ' . ($isRunning ? 'YES' : 'NO') . ' (status: ' . $status . ')' );
866
867 return $isRunning;
868 }
869
875 public function isStopped(): bool
876 {
877 Log::trace("Checking if service is stopped: " . $this->getName());
878
879 $status = $this->status();
880 $isStopped = $status == self::WIN32_SERVICE_STOPPED;
881
882 Log::trace("Service " . $this->getName() . " stopped status: " . ($isStopped ? "YES" : "NO") . " (status code: " . $status . ")");
883 $this->writeLog( 'isStopped ' . $this->getName() . ': ' . ($isStopped ? 'YES' : 'NO') . ' (status: ' . $status . ')' );
884
885 return $isStopped;
886 }
887
893 public function isPaused(): bool
894 {
895 Log::trace("Checking if service is paused: " . $this->getName());
896
897 $status = $this->status();
898 $isPaused = $status == self::WIN32_SERVICE_PAUSED;
899
900 Log::trace("Service " . $this->getName() . " paused status: " . ($isPaused ? "YES" : "NO") . " (status code: " . $status . ")");
901 $this->writeLog( 'isPaused ' . $this->getName() . ': ' . ($isPaused ? 'YES' : 'NO') . ' (status: ' . $status . ')' );
902
903 return $isPaused;
904 }
905
913 public function isPending($status): bool
914 {
915 $isPending = $status == self::WIN32_SERVICE_START_PENDING || $status == self::WIN32_SERVICE_STOP_PENDING
916 || $status == self::WIN32_SERVICE_CONTINUE_PENDING || $status == self::WIN32_SERVICE_PAUSE_PENDING;
917
918 Log::trace("Checking if status is pending: " . $status . " - Result: " . ($isPending ? "YES" : "NO"));
919
920 if ($isPending) {
921 if ($status == self::WIN32_SERVICE_START_PENDING) {
922 Log::trace("Service is in START_PENDING state");
923 } else if ($status == self::WIN32_SERVICE_STOP_PENDING) {
924 Log::trace("Service is in STOP_PENDING state");
925 } else if ($status == self::WIN32_SERVICE_CONTINUE_PENDING) {
926 Log::trace("Service is in CONTINUE_PENDING state");
927 } else if ($status == self::WIN32_SERVICE_PAUSE_PENDING) {
928 Log::trace("Service is in PAUSE_PENDING state");
929 }
930 }
931
932 return $isPending;
933 }
934
942 private function getWin32ServiceStatusDesc($status): ?string
943 {
944 switch ( $status ) {
945 case self::WIN32_SERVICE_CONTINUE_PENDING:
946 return 'The service continue is pending.';
947
948 case self::WIN32_SERVICE_PAUSE_PENDING:
949 return 'The service pause is pending.';
950
951 case self::WIN32_SERVICE_PAUSED:
952 return 'The service is paused.';
953
954 case self::WIN32_SERVICE_RUNNING:
955 return 'The service is running.';
956
957 case self::WIN32_SERVICE_START_PENDING:
958 return 'The service is starting.';
959
960 case self::WIN32_SERVICE_STOP_PENDING:
961 return 'The service is stopping.';
962
963 case self::WIN32_SERVICE_STOPPED:
964 return 'The service is not running.';
965
966 case self::WIN32_SERVICE_NA:
967 return 'Cannot retrieve service status.';
968
969 default:
970 return null;
971 }
972 }
973
981 private function getWin32ErrorCodeDesc($code): ?string
982 {
983 switch ( $code ) {
984 case self::WIN32_ERROR_ACCESS_DENIED:
985 return 'The handle to the SCM database does not have the appropriate access rights.';
986 // ... other cases ...
987 default:
988 return null;
989 }
990 }
991
997 public function getName(): string
998 {
999 return $this->name;
1000 }
1001
1007 public function setName($name): void
1008 {
1009 $this->name = $name;
1010 }
1011
1017 public function getDisplayName(): string
1018 {
1019 return $this->displayName;
1020 }
1021
1027 public function setDisplayName($displayName): void
1028 {
1029 $this->displayName = $displayName;
1030 }
1031
1037 public function getBinPath(): string
1038 {
1039 return $this->binPath;
1040 }
1041
1047 public function setBinPath($binPath): void
1048 {
1049 $this->binPath = str_replace( '"', '', UtilPath::formatWindowsPath( $binPath ) );
1050 }
1051
1057 public function getParams(): string
1058 {
1059 return $this->params;
1060 }
1061
1067 public function setParams($params): void
1068 {
1069 $this->params = $params;
1070 }
1071
1077 public function getStartType(): string
1078 {
1079 return $this->startType;
1080 }
1081
1087 public function setStartType($startType): void
1088 {
1089 $this->startType = $startType;
1090 }
1091
1097 public function getErrorControl(): string
1098 {
1099 return $this->errorControl;
1100 }
1101
1107 public function setErrorControl($errorControl): void
1108 {
1109 $this->errorControl = $errorControl;
1110 }
1111
1117 public function getNssm()
1118 {
1119 return $this->nssm;
1120 }
1121
1127 public function setNssm($nssm)
1128 {
1129 if ( $nssm instanceof Nssm ) {
1130 $this->setDisplayName( $nssm->getDisplayName() );
1131 $this->setBinPath( $nssm->getBinPath() );
1132 $this->setParams( $nssm->getParams() );
1133 $this->setStartType( $nssm->getStart() );
1134 $this->nssm = $nssm;
1135 }
1136 }
1137
1143 public function getLatestStatus()
1144 {
1145 return $this->latestStatus;
1146 }
1147
1153 public function getLatestError()
1154 {
1155 return $this->latestError;
1156 }
1157
1163 public function getError()
1164 {
1165 global $bearsamppLang;
1166 if ( $this->latestError != self::WIN32_NO_ERROR ) {
1167 // Ensure proper type conversion for PHP 8.2.3 compatibility
1168 $errorInt = is_numeric($this->latestError) ? hexdec( $this->latestError ) : 0;
1169 return $bearsamppLang->getValue( Lang::ERROR ) . ' ' .
1170 $this->latestError . ' (' . $errorInt . ' : ' . $this->getWin32ErrorCodeDesc( $this->latestError ) . ')';
1171 }
1172 elseif ( $this->latestStatus != self::WIN32_SERVICE_NA ) {
1173 // Ensure proper type conversion for PHP 8.2.3 compatibility
1174 $statusInt = is_numeric($this->latestStatus) ? hexdec( $this->latestStatus ) : 0;
1175 return $bearsamppLang->getValue( Lang::STATUS ) . ' ' .
1176 $this->latestStatus . ' (' . $statusInt . ' : ' . $this->getWin32ServiceStatusDesc( $this->latestStatus ) . ')';
1177 }
1178
1179 return null;
1180 }
1181
1191 public function waitForServiceDeletion($maxWaitTime = 30): bool
1192 {
1193 $startTime = time();
1194 $maxTime = $startTime + $maxWaitTime;
1195 $checkCount = 0;
1196
1197 Log::trace("Waiting for service deletion: " . $this->getName() . " (max wait: " . $maxWaitTime . "s)");
1198
1199 while (time() < $maxTime) {
1200 $checkCount++;
1201 $status = $this->status(false);
1202 Log::trace("Service deletion check #" . $checkCount . " - Status: " . $status . " at " . date('Y-m-d H:i:s'));
1203
1204 // Service doesn't exist or is definitely not there
1205 if ($status == self::WIN32_SERVICE_NA ||
1206 $status == self::WIN32_ERROR_SERVICE_DOES_NOT_EXIST) {
1207 $elapsedTime = time() - $startTime;
1208 Log::trace("Service deletion confirmed after " . $elapsedTime . " seconds");
1209 return true;
1210 }
1211
1212 // Wait a bit before checking again
1213 usleep(500000); // 0.5 seconds
1214 }
1215
1216 $totalWaitTime = time() - $startTime;
1217 Log::trace("Service deletion timeout after " . $totalWaitTime . " seconds - service still exists: " . $this->getName());
1218 return false;
1219 }
1220
1227 public function ensureReset(): bool
1228 {
1229 Log::trace("Starting ensureReset for service: " . $this->getName());
1230
1231 // First, make sure service is stopped
1232 if ($this->isRunning()) {
1233 Log::trace("Service is still running, stopping it first");
1234 if (!$this->stop()) {
1235 Log::trace("Failed to stop service during ensureReset");
1236 return false;
1237 }
1238 usleep(1000000); // 1 second wait after stop
1239 }
1240
1241 // Delete the service
1242 Log::trace("Deleting service");
1243 if (!$this->delete()) {
1244 Log::trace("Service deletion failed, but continuing with wait");
1245 }
1246
1247 // Wait for the service to be completely removed
1248 if (!$this->waitForServiceDeletion(30)) {
1249 Log::trace("Service deletion did not complete within timeout, but continuing");
1250 // Even if we timeout, give it a moment and try to create anyway
1251 usleep(2000000); // 2 seconds
1252 }
1253
1254 Log::trace("ensureReset completed for service: " . $this->getName());
1255 return true;
1256 }
1257}
$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 getNssmEnvPaths()
static getAppBinsRegKey($fromRegistry=true)
static formatWindowsPath($path)
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
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