125 self::VBS_DISPLAY_NAME,
126 self::VBS_DESCRIPTION,
141 if (self::$serviceListCache ===
null || $forceRefresh) {
142 Log::trace(
'Fetching service list from Windows (COM/WMI)');
143 $startTime = microtime(
true);
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;
154 $duration = round(microtime(
true) - $startTime, 3);
155 Log::trace(
'Service list fetched in ' . $duration .
's');
158 return self::$serviceListCache;
173 if ( function_exists( $function ) ) {
174 if (!isset(self::$loggedFunctions[$function])) {
175 Log::trace(
'Win32 function: ' . $function .
' exists');
176 self::$loggedFunctions[$function] =
true;
180 if ($function ===
'win32_query_service_status') {
181 Log::trace(
"Using enhanced handling for win32_query_service_status");
184 $originalTimeout = ini_get(
'max_execution_time');
189 $result = call_user_func($function, $param);
192 set_time_limit($originalTimeout);
194 if ($checkError &&
$result !==
null) {
197 if (dechex($resultInt) != self::WIN32_NO_ERROR) {
198 $this->latestError = dechex($resultInt);
203 set_time_limit($originalTimeout);
205 Log::trace(
"Win32ServiceException caught: " . $e->getMessage());
208 if (strpos($e->getMessage(),
'service does not exist') !==
false) {
209 Log::trace(
"Service does not exist exception handled for: " . $param);
211 $result = hexdec(self::WIN32_ERROR_SERVICE_DOES_NOT_EXIST);
214 Log::trace(
"Unhandled Win32ServiceException: " . $e->getMessage());
217 }
catch (\Exception $e) {
219 set_time_limit($originalTimeout);
222 Log::trace(
"Exception caught in callWin32Service: " . $e->getMessage());
224 }
catch (\Throwable $e) {
226 set_time_limit($originalTimeout);
229 Log::trace(
"Throwable caught in callWin32Service: " . $e->getMessage());
236 $result = call_user_func($function, $param);
237 if ($checkError &&
$result !==
null) {
240 if (dechex($resultInt) != self::WIN32_NO_ERROR) {
241 $this->latestError = dechex($resultInt);
245 Log::trace(
"Win32ServiceException caught: " . $e->getMessage());
248 if (strpos($e->getMessage(),
'service does not exist') !==
false) {
249 Log::trace(
"Service does not exist exception handled for: " . $param);
251 $result = hexdec(self::WIN32_ERROR_SERVICE_DOES_NOT_EXIST);
254 Log::trace(
"Unhandled Win32ServiceException: " . $e->getMessage());
257 }
catch (\Exception $e) {
259 Log::trace(
"Exception caught in callWin32Service: " . $e->getMessage());
261 }
catch (\Throwable $e) {
263 Log::trace(
"Throwable caught in callWin32Service: " . $e->getMessage());
268 if (!isset(self::$loggedFunctions[$function])) {
269 Log::trace(
'Win32 function: ' . $function .
' missing');
270 self::$loggedFunctions[$function] =
true;
283 public function status($timeout =
true): string
285 $this->latestStatus = self::WIN32_SERVICE_NA;
286 $maxtime = time() + self::PENDING_TIMEOUT;
288 Log::trace(
"Querying status for service: " . $this->
getName() .
" (timeout: " . ($timeout ?
"enabled" :
"disabled") .
")");
290 Log::trace(
"Max timeout time set to: " . date(
'Y-m-d H:i:s', $maxtime));
296 $startTime = microtime(
true);
299 while ( ($this->latestStatus == self::WIN32_SERVICE_NA || $this->
isPending( $this->latestStatus )) && $loopCount < $maxLoops ) {
301 Log::trace(
"Calling win32_query_service_status for service: " . $this->
getName() .
" (attempt " . $loopCount .
" of " . $maxLoops .
")");
304 if (microtime(
true) - $startTime > 10) {
305 Log::trace(
"Overall timeout reached before making service status call");
311 if ( is_array( $this->latestStatus ) && isset( $this->latestStatus[
'CurrentState'] ) ) {
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);
317 elseif ( $this->latestStatus !==
null ) {
319 $statusInt = is_numeric($this->latestStatus) ? (int)$this->latestStatus : 0;
320 $statusHex = dechex( $statusInt );
321 Log::trace(
"Service status returned as value: " . $statusHex);
323 if ( $statusHex == self::WIN32_ERROR_SERVICE_DOES_NOT_EXIST ) {
324 $this->latestStatus = $statusHex;
325 Log::trace(
"Service does not exist, breaking loop");
329 Log::trace(
"Service status query returned null");
333 Log::trace(
"Multiple null results, assuming service does not exist");
334 $this->latestStatus = self::WIN32_ERROR_SERVICE_DOES_NOT_EXIST;
339 if ( $timeout && $maxtime < time() ) {
340 Log::trace(
"Timeout reached while querying service status");
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);
350 }
catch (\Exception $e) {
351 Log::trace(
"Exception in status method: " . $e->getMessage());
353 $this->latestStatus = self::WIN32_ERROR_SERVICE_DOES_NOT_EXIST;
354 }
catch (\Throwable $e) {
355 Log::trace(
"Throwable in status method: " . $e->getMessage());
357 $this->latestStatus = self::WIN32_ERROR_SERVICE_DOES_NOT_EXIST;
360 if ($loopCount >= $maxLoops) {
364 $elapsedTime = microtime(
true) - $startTime;
365 Log::trace(
"Status check completed in " . round($elapsedTime, 2) .
" seconds after " . $loopCount .
" attempts");
367 if ( $this->latestStatus == self::WIN32_ERROR_SERVICE_DOES_NOT_EXIST ) {
369 $this->latestStatus = self::WIN32_SERVICE_NA;
370 Log::trace(
"Service does not exist, setting status to NA");
373 Log::trace(
"Final status for service " . $this->
getName() .
": " . $this->latestStatus);
389 Log::trace(
"PostgreSQL service detected - using specialized installation");
391 Log::trace(
"PostgreSQL configuration rebuilt");
397 Log::trace(
"PostgreSQL service installation " . (
$result ?
"succeeded" :
"failed"));
402 Log::trace(
"Using NSSM for service installation");
405 global $bearsamppTools;
406 if (!isset($bearsamppTools)) {
407 Log::trace(
"Tools not loaded, forcing synchronous load");
411 global $bearsamppRegistry;
412 $nssmEnvPath = $bearsamppRegistry->getAppBinsRegKey(
false );
413 Log::trace(
"NSSM environment path (bins): " . $nssmEnvPath);
416 Log::trace(
"NSSM environment path (with additional paths): " . $nssmEnvPath);
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);
424 $this->
getNssm()->setEnvironmentExtra(
'PATH=' . $nssmEnvPath );
442 Log::trace(
"Using win32_create_service for service installation");
443 $serviceParams = array(
453 Log::trace(
"win32_create_service parameters:");
454 foreach ($serviceParams as $key => $value) {
461 $create =
$result !==
null ? dechex( $resultInt ) :
'0';
462 Log::trace(
"win32_create_service result code: " . $create);
465 if ( $create == self::WIN32_ERROR_SERVICE_MARKED_FOR_DELETE ) {
466 Log::trace(
"Service marked for delete, waiting 2s before retry: " . $this->
getName());
470 $create =
$result !==
null ? dechex( $resultInt ) :
'0';
471 Log::trace(
"win32_create_service retry result code: " . $create);
474 $this->
writeLog(
'Create service: ' . $create .
' (status: ' . $this->
status() .
')' );
483 if ( $create != self::WIN32_NO_ERROR ) {
484 Log::trace(
"Service creation failed with error code: " . $create);
488 Log::trace(
"Service created but not found as installed");
489 $this->latestError = self::WIN32_NO_ERROR;
502 public function delete(): bool
517 Log::trace(
"Killing NSSM child process after stop: " . $childExe);
522 Log::trace(
"PostgreSQL service detected - using specialized uninstallation");
524 Log::trace(
"PostgreSQL service uninstallation " . (
$result ?
"succeeded" :
"failed"));
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() .
')' );
536 if ( $delete != self::WIN32_NO_ERROR && $delete != self::WIN32_ERROR_SERVICE_DOES_NOT_EXIST ) {
540 $this->latestError = self::WIN32_NO_ERROR;
555 if ( $this->
delete() ) {
556 usleep( self::SLEEP_TIME );
599 $start =
$result !==
null ? dechex( $resultInt ) :
'0';
602 if ( $start != self::WIN32_NO_ERROR && $start != self::WIN32_ERROR_SERVICE_ALREADY_RUNNING ) {
605 Log::error(
'Failed to start service: ' . $this->
getName() .
' with error code: ' . $start);
609 if ( !$cmdOutput[
'syntaxOk'] ) {
612 '[' . date(
'Y-m-d H:i:s', time() ) .
'] [error] ' . $cmdOutput[
'content'] . PHP_EOL,
619 if ( !$cmdOutput[
'syntaxOk'] ) {
622 '[' . date(
'Y-m-d H:i:s', time() ) .
'] [error] ' . $cmdOutput[
'content'] . PHP_EOL,
629 if ( !$cmdOutput[
'syntaxOk'] ) {
632 '[' . date(
'Y-m-d H:i:s', time() ) .
'] [error] ' . $cmdOutput[
'content'] . PHP_EOL,
643 $maxtime = time() + self::PENDING_TIMEOUT;
645 usleep(self::SLEEP_TIME);
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;
673 $stop =
$result !==
null ? dechex( $resultInt ) :
'0';
674 Log::trace(
"Stop service result code: " . $stop);
676 Log::trace(
"Checking current status after stop attempt");
677 $currentStatus = $this->
status();
678 Log::trace(
"Current status: " . $currentStatus);
680 $this->
writeLog(
'Stop service ' . $this->
getName() .
': ' . $stop .
' (status: ' . $currentStatus .
')' );
682 if ( $stop != self::WIN32_NO_ERROR ) {
687 $maxtime = time() + self::PENDING_TIMEOUT;
689 usleep(self::SLEEP_TIME);
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;
709 if ( $this->
stop() ) {
710 return $this->
start();
726 $startTime = microtime(
true);
733 $duration = round(microtime(
true) - $startTime, 3);
735 Log::trace(
"sc.exe query completed in " . $duration .
"s");
737 if ($output ===
null || $output ===
false) {
738 Log::trace(
"sc.exe returned null/false, service likely doesn't exist");
743 if (stripos($output,
'does not exist') !==
false ||
744 stripos($output,
'FAILED') !==
false ||
745 stripos($output,
'1060') !==
false) {
754 if (preg_match(
'/SERVICE_NAME:\s*(.+)/i', $output, $matches)) {
755 $serviceInfo[self::VBS_NAME] = trim($matches[1]);
759 if (preg_match(
'/DISPLAY_NAME:\s*(.+)/i', $output, $matches)) {
760 $serviceInfo[self::VBS_DISPLAY_NAME] = trim($matches[1]);
764 if (preg_match(
'/STATE\s*:\s*\d+\s+(\w+)/i', $output, $matches)) {
765 $state = trim($matches[1]);
766 $serviceInfo[self::SERVICE_STATE] = $state;
771 if (!empty($serviceInfo)) {
772 Log::trace(
"Service exists, getting full details");
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]);
783 if ($configOutput !==
null && $configOutput !==
false && preg_match(
'/DISPLAY_NAME\s*:\s*(.+)/i', $configOutput, $matches)) {
784 $serviceInfo[self::VBS_DESCRIPTION] = trim($matches[1]);
791 Log::trace(
"Could not parse service info from sc.exe output");
807 $startTime = microtime(
true);
813 Log::trace(
"NSSM info retrieval completed in " . round(microtime(
true) - $startTime, 2) .
" seconds");
818 Log::trace(
"Attempting fast service check using sc.exe");
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");
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");
836 Log::trace(
"Falling back to VBS for service info");
839 $originalTimeout = ini_get(
'max_execution_time');
846 set_time_limit($originalTimeout);
849 if (microtime(
true) - $startTime > $timeout) {
850 Log::trace(
"Timeout exceeded in infos() method, returning false");
854 Log::trace(
"VBS info retrieval completed in " . round(microtime(
true) - $startTime, 2) .
" seconds");
856 }
catch (\Exception $e) {
857 Log::trace(
"Exception in infos() method: " . $e->getMessage() .
", returning false");
859 }
catch (\Throwable $e) {
860 Log::trace(
"Throwable in infos() method: " . $e->getMessage() .
", returning false");
876 $startTime = microtime(
true);
880 $status = $this->
status();
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)');
889 $isInstalled = $status != self::WIN32_SERVICE_NA;
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 .
')');
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() .
')');
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() .
')');
915 $status = $this->
status();
916 $isRunning = $status == self::WIN32_SERVICE_RUNNING;
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 .
')' );
933 $status = $this->
status();
934 $isStopped = $status == self::WIN32_SERVICE_STOPPED;
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 .
')' );
951 $status = $this->
status();
952 $isPaused = $status == self::WIN32_SERVICE_PAUSED;
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 .
')' );
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;
972 Log::trace(
"Checking if status is pending: " . $status .
" - Result: " . ($isPending ?
"YES" :
"NO"));
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");
999 case self::WIN32_SERVICE_CONTINUE_PENDING:
1000 return 'The service continue is pending.';
1002 case self::WIN32_SERVICE_PAUSE_PENDING:
1003 return 'The service pause is pending.';
1005 case self::WIN32_SERVICE_PAUSED:
1006 return 'The service is paused.';
1008 case self::WIN32_SERVICE_RUNNING:
1009 return 'The service is running.';
1011 case self::WIN32_SERVICE_START_PENDING:
1012 return 'The service is starting.';
1014 case self::WIN32_SERVICE_STOP_PENDING:
1015 return 'The service is stopping.';
1017 case self::WIN32_SERVICE_STOPPED:
1018 return 'The service is not running.';
1020 case self::WIN32_SERVICE_NA:
1021 return 'Cannot retrieve service status.';
1038 case self::WIN32_ERROR_ACCESS_DENIED:
1039 return 'The handle to the SCM database does not have the appropriate access rights.';
1063 $this->name =
$name;
1188 $this->nssm =
$nssm;
1220 if ( $this->latestError != self::WIN32_NO_ERROR ) {
1222 $errorInt = is_numeric($this->latestError) ? hexdec( $this->latestError ) : 0;
1224 $this->latestError .
' (' . $errorInt .
' : ' . $this->
getWin32ErrorCodeDesc( $this->latestError ) .
')';
1226 elseif ( $this->latestStatus != self::WIN32_SERVICE_NA ) {
1228 $statusInt = is_numeric($this->latestStatus) ? hexdec( $this->latestStatus ) : 0;
1247 $startTime = time();
1248 $maxTime = $startTime + $maxWaitTime;
1251 Log::trace(
"Waiting for service deletion: " . $this->
getName() .
" (max wait: " . $maxWaitTime .
"s)");
1253 while (time() < $maxTime) {
1255 $status = $this->
status(
false);
1256 Log::trace(
"Service deletion check #" . $checkCount .
" - Status: " . $status .
" at " . date(
'Y-m-d H:i:s'));
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");
1270 $totalWaitTime = time() - $startTime;
1271 Log::trace(
"Service deletion timeout after " . $totalWaitTime .
" seconds - service still exists: " . $this->
getName());
1287 Log::trace(
"Service is still running, stopping it first");
1288 if (!$this->
stop()) {
1289 Log::trace(
"Failed to stop service during ensureReset");
1297 if (!$this->
delete()) {
1298 Log::trace(
"Service deletion failed, but continuing with wait");
1303 Log::trace(
"Service deletion did not complete within timeout, but continuing");
static installPostgresqlService()
static uninstallPostgresqlService()
static execCombined(string $executable, array $args=[])
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 formatWindowsPath($path)
static listServices($properties=[])
static getServiceInfo($serviceName, $properties=[])
static killBins($refreshProcs=false)
setErrorControl($errorControl)
const WIN32_SERVICE_PAUSED
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_INVALID_NAME
const SERVER_ERROR_NORMAL
const WIN32_ERROR_INVALID_DATA
const SERVER_ERROR_IGNORE
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
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
const WIN32_ERROR_SERVICE_DOES_NOT_EXIST
const WIN32_SERVICE_RUNNING
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_SERVICE_STOPPED
const WIN32_ERROR_SERVICE_EXISTS
const WIN32_SERVICE_PAUSE_PENDING
const WIN32_ERROR_SERVICE_LOGON_FAILED
getWin32ErrorCodeDesc($code)
const SERVICE_DEMAND_START