122 self::VBS_DISPLAY_NAME,
123 self::VBS_DESCRIPTION,
141 if ( function_exists( $function ) ) {
142 if (!isset(self::$loggedFunctions[$function])) {
143 Log::trace(
'Win32 function: ' . $function .
' exists');
144 self::$loggedFunctions[$function] =
true;
148 if ($function ===
'win32_query_service_status') {
149 Log::trace(
"Using enhanced handling for win32_query_service_status");
152 $originalTimeout = ini_get(
'max_execution_time');
157 $result = call_user_func($function, $param);
160 set_time_limit($originalTimeout);
162 if ($checkError &&
$result !==
null) {
165 if (dechex($resultInt) != self::WIN32_NO_ERROR) {
166 $this->latestError = dechex($resultInt);
171 set_time_limit($originalTimeout);
173 Log::trace(
"Win32ServiceException caught: " . $e->getMessage());
176 if (strpos($e->getMessage(),
'service does not exist') !==
false) {
177 Log::trace(
"Service does not exist exception handled for: " . $param);
179 $result = hexdec(self::WIN32_ERROR_SERVICE_DOES_NOT_EXIST);
182 Log::trace(
"Unhandled Win32ServiceException: " . $e->getMessage());
185 }
catch (\Exception $e) {
187 set_time_limit($originalTimeout);
190 Log::trace(
"Exception caught in callWin32Service: " . $e->getMessage());
192 }
catch (\Throwable $e) {
194 set_time_limit($originalTimeout);
197 Log::trace(
"Throwable caught in callWin32Service: " . $e->getMessage());
204 $result = call_user_func($function, $param);
205 if ($checkError &&
$result !==
null) {
208 if (dechex($resultInt) != self::WIN32_NO_ERROR) {
209 $this->latestError = dechex($resultInt);
213 Log::trace(
"Win32ServiceException caught: " . $e->getMessage());
216 if (strpos($e->getMessage(),
'service does not exist') !==
false) {
217 Log::trace(
"Service does not exist exception handled for: " . $param);
219 $result = hexdec(self::WIN32_ERROR_SERVICE_DOES_NOT_EXIST);
222 Log::trace(
"Unhandled Win32ServiceException: " . $e->getMessage());
225 }
catch (\Exception $e) {
227 Log::trace(
"Exception caught in callWin32Service: " . $e->getMessage());
229 }
catch (\Throwable $e) {
231 Log::trace(
"Throwable caught in callWin32Service: " . $e->getMessage());
236 if (!isset(self::$loggedFunctions[$function])) {
237 Log::trace(
'Win32 function: ' . $function .
' missing');
238 self::$loggedFunctions[$function] =
true;
251 public function status($timeout =
true): string
253 usleep( self::SLEEP_TIME );
255 $this->latestStatus = self::WIN32_SERVICE_NA;
256 $maxtime = time() + self::PENDING_TIMEOUT;
258 Log::trace(
"Querying status for service: " . $this->
getName() .
" (timeout: " . ($timeout ?
"enabled" :
"disabled") .
")");
260 Log::trace(
"Max timeout time set to: " . date(
'Y-m-d H:i:s', $maxtime));
266 $startTime = microtime(
true);
269 while ( ($this->latestStatus == self::WIN32_SERVICE_NA || $this->
isPending( $this->latestStatus )) && $loopCount < $maxLoops ) {
271 Log::trace(
"Calling win32_query_service_status for service: " . $this->
getName() .
" (attempt " . $loopCount .
" of " . $maxLoops .
")");
274 if (microtime(
true) - $startTime > 10) {
275 Log::trace(
"Overall timeout reached before making service status call");
281 if ( is_array( $this->latestStatus ) && isset( $this->latestStatus[
'CurrentState'] ) ) {
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);
287 elseif ( $this->latestStatus !==
null ) {
289 $statusInt = is_numeric($this->latestStatus) ? (int)$this->latestStatus : 0;
290 $statusHex = dechex( $statusInt );
291 Log::trace(
"Service status returned as value: " . $statusHex);
293 if ( $statusHex == self::WIN32_ERROR_SERVICE_DOES_NOT_EXIST ) {
294 $this->latestStatus = $statusHex;
295 Log::trace(
"Service does not exist, breaking loop");
299 Log::trace(
"Service status query returned null");
303 Log::trace(
"Multiple null results, assuming service does not exist");
304 $this->latestStatus = self::WIN32_ERROR_SERVICE_DOES_NOT_EXIST;
309 if ( $timeout && $maxtime < time() ) {
310 Log::trace(
"Timeout reached while querying service status");
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);
320 }
catch (\Exception $e) {
321 Log::trace(
"Exception in status method: " . $e->getMessage());
323 $this->latestStatus = self::WIN32_ERROR_SERVICE_DOES_NOT_EXIST;
324 }
catch (\Throwable $e) {
325 Log::trace(
"Throwable in status method: " . $e->getMessage());
327 $this->latestStatus = self::WIN32_ERROR_SERVICE_DOES_NOT_EXIST;
330 if ($loopCount >= $maxLoops) {
334 $elapsedTime = microtime(
true) - $startTime;
335 Log::trace(
"Status check completed in " . round($elapsedTime, 2) .
" seconds after " . $loopCount .
" attempts");
337 if ( $this->latestStatus == self::WIN32_ERROR_SERVICE_DOES_NOT_EXIST ) {
339 $this->latestStatus = self::WIN32_SERVICE_NA;
340 Log::trace(
"Service does not exist, setting status to NA");
343 Log::trace(
"Final status for service " . $this->
getName() .
": " . $this->latestStatus);
359 Log::trace(
"PostgreSQL service detected - using specialized installation");
361 Log::trace(
"PostgreSQL configuration rebuilt");
367 Log::trace(
"PostgreSQL service installation " . (
$result ?
"succeeded" :
"failed"));
372 Log::trace(
"Using NSSM for service installation");
375 Log::trace(
"NSSM environment path (bins): " . $nssmEnvPath);
378 Log::trace(
"NSSM environment path (with additional paths): " . $nssmEnvPath);
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);
386 $this->
getNssm()->setEnvironmentExtra(
'PATH=' . $nssmEnvPath );
404 Log::trace(
"Using win32_create_service for service installation");
405 $serviceParams = array(
415 Log::trace(
"win32_create_service parameters:");
416 foreach ($serviceParams as $key => $value) {
423 $create =
$result !==
null ? dechex( $resultInt ) :
'0';
424 Log::trace(
"win32_create_service result code: " . $create);
427 if ( $create == self::WIN32_ERROR_SERVICE_MARKED_FOR_DELETE ) {
428 Log::trace(
"Service marked for delete, waiting 2s before retry: " . $this->
getName());
432 $create =
$result !==
null ? dechex( $resultInt ) :
'0';
433 Log::trace(
"win32_create_service retry result code: " . $create);
436 $this->
writeLog(
'Create service: ' . $create .
' (status: ' . $this->
status() .
')' );
445 if ( $create != self::WIN32_NO_ERROR ) {
446 Log::trace(
"Service creation failed with error code: " . $create);
450 Log::trace(
"Service created but not found as installed");
451 $this->latestError = self::WIN32_NO_ERROR;
464 public function delete(): bool
479 Log::trace(
"Killing NSSM child process after stop: " . $childExe);
484 Log::trace(
"PostgreSQL service detected - using specialized uninstallation");
486 Log::trace(
"PostgreSQL service uninstallation " . (
$result ?
"succeeded" :
"failed"));
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() .
')' );
498 if ( $delete != self::WIN32_NO_ERROR && $delete != self::WIN32_ERROR_SERVICE_DOES_NOT_EXIST ) {
502 $this->latestError = self::WIN32_NO_ERROR;
517 if ( $this->
delete() ) {
518 usleep( self::SLEEP_TIME );
561 $start =
$result !==
null ? dechex( $resultInt ) :
'0';
564 if ( $start != self::WIN32_NO_ERROR && $start != self::WIN32_ERROR_SERVICE_ALREADY_RUNNING ) {
567 Log::error(
'Failed to start service: ' . $this->
getName() .
' with error code: ' . $start);
571 if ( !$cmdOutput[
'syntaxOk'] ) {
574 '[' . date(
'Y-m-d H:i:s', time() ) .
'] [error] ' . $cmdOutput[
'content'] . PHP_EOL,
581 if ( !$cmdOutput[
'syntaxOk'] ) {
584 '[' . date(
'Y-m-d H:i:s', time() ) .
'] [error] ' . $cmdOutput[
'content'] . PHP_EOL,
591 if ( !$cmdOutput[
'syntaxOk'] ) {
594 '[' . date(
'Y-m-d H:i:s', time() ) .
'] [error] ' . $cmdOutput[
'content'] . PHP_EOL,
603 $this->latestError = self::WIN32_NO_ERROR;
604 Log::error(
'Service ' . $this->
getName() .
' is not running after start attempt.');
605 $this->latestError =
null;
627 $stop =
$result !==
null ? dechex( $resultInt ) :
'0';
628 Log::trace(
"Stop service result code: " . $stop);
630 Log::trace(
"Checking current status after stop attempt");
631 $currentStatus = $this->
status();
632 Log::trace(
"Current status: " . $currentStatus);
634 $this->
writeLog(
'Stop service ' . $this->
getName() .
': ' . $stop .
' (status: ' . $currentStatus .
')' );
636 if ( $stop != self::WIN32_NO_ERROR ) {
640 $this->latestError = self::WIN32_NO_ERROR;
655 if ( $this->
stop() ) {
656 return $this->
start();
672 $startTime = microtime(
true);
679 $duration = round(microtime(
true) - $startTime, 3);
681 Log::trace(
"sc.exe query completed in " . $duration .
"s");
683 if ($output ===
null || $output ===
false) {
684 Log::trace(
"sc.exe returned null/false, service likely doesn't exist");
689 if (stripos($output,
'does not exist') !==
false ||
690 stripos($output,
'FAILED') !==
false ||
691 stripos($output,
'1060') !==
false) {
700 if (preg_match(
'/SERVICE_NAME:\s*(.+)/i', $output, $matches)) {
701 $serviceInfo[self::VBS_NAME] = trim($matches[1]);
705 if (preg_match(
'/DISPLAY_NAME:\s*(.+)/i', $output, $matches)) {
706 $serviceInfo[self::VBS_DISPLAY_NAME] = trim($matches[1]);
710 if (preg_match(
'/STATE\s*:\s*\d+\s+(\w+)/i', $output, $matches)) {
711 $state = trim($matches[1]);
712 $serviceInfo[self::VBS_STATE] = $state;
717 if (!empty($serviceInfo)) {
718 Log::trace(
"Service exists, getting full details");
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]);
729 if ($configOutput !==
null && $configOutput !==
false && preg_match(
'/DISPLAY_NAME\s*:\s*(.+)/i', $configOutput, $matches)) {
730 $serviceInfo[self::VBS_DESCRIPTION] = trim($matches[1]);
737 Log::trace(
"Could not parse service info from sc.exe output");
753 $startTime = microtime(
true);
759 Log::trace(
"NSSM info retrieval completed in " . round(microtime(
true) - $startTime, 2) .
" seconds");
764 Log::trace(
"Attempting fast service check using sc.exe");
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");
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");
782 Log::trace(
"Falling back to VBS for service info");
785 $originalTimeout = ini_get(
'max_execution_time');
792 set_time_limit($originalTimeout);
795 if (microtime(
true) - $startTime > $timeout) {
796 Log::trace(
"Timeout exceeded in infos() method, returning false");
800 Log::trace(
"VBS info retrieval completed in " . round(microtime(
true) - $startTime, 2) .
" seconds");
802 }
catch (\Exception $e) {
803 Log::trace(
"Exception in infos() method: " . $e->getMessage() .
", returning false");
805 }
catch (\Throwable $e) {
806 Log::trace(
"Throwable in infos() method: " . $e->getMessage() .
", returning false");
822 $startTime = microtime(
true);
826 $status = $this->
status();
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)');
835 $isInstalled = $status != self::WIN32_SERVICE_NA;
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 .
')');
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() .
')');
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() .
')');
861 $status = $this->
status();
862 $isRunning = $status == self::WIN32_SERVICE_RUNNING;
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 .
')' );
879 $status = $this->
status();
880 $isStopped = $status == self::WIN32_SERVICE_STOPPED;
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 .
')' );
897 $status = $this->
status();
898 $isPaused = $status == self::WIN32_SERVICE_PAUSED;
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 .
')' );
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;
918 Log::trace(
"Checking if status is pending: " . $status .
" - Result: " . ($isPending ?
"YES" :
"NO"));
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");
945 case self::WIN32_SERVICE_CONTINUE_PENDING:
946 return 'The service continue is pending.';
948 case self::WIN32_SERVICE_PAUSE_PENDING:
949 return 'The service pause is pending.';
951 case self::WIN32_SERVICE_PAUSED:
952 return 'The service is paused.';
954 case self::WIN32_SERVICE_RUNNING:
955 return 'The service is running.';
957 case self::WIN32_SERVICE_START_PENDING:
958 return 'The service is starting.';
960 case self::WIN32_SERVICE_STOP_PENDING:
961 return 'The service is stopping.';
963 case self::WIN32_SERVICE_STOPPED:
964 return 'The service is not running.';
966 case self::WIN32_SERVICE_NA:
967 return 'Cannot retrieve service status.';
984 case self::WIN32_ERROR_ACCESS_DENIED:
985 return 'The handle to the SCM database does not have the appropriate access rights.';
1009 $this->name =
$name;
1134 $this->nssm =
$nssm;
1166 if ( $this->latestError != self::WIN32_NO_ERROR ) {
1168 $errorInt = is_numeric($this->latestError) ? hexdec( $this->latestError ) : 0;
1170 $this->latestError .
' (' . $errorInt .
' : ' . $this->
getWin32ErrorCodeDesc( $this->latestError ) .
')';
1172 elseif ( $this->latestStatus != self::WIN32_SERVICE_NA ) {
1174 $statusInt = is_numeric($this->latestStatus) ? hexdec( $this->latestStatus ) : 0;
1193 $startTime = time();
1194 $maxTime = $startTime + $maxWaitTime;
1197 Log::trace(
"Waiting for service deletion: " . $this->
getName() .
" (max wait: " . $maxWaitTime .
"s)");
1199 while (time() < $maxTime) {
1201 $status = $this->
status(
false);
1202 Log::trace(
"Service deletion check #" . $checkCount .
" - Status: " . $status .
" at " . date(
'Y-m-d H:i:s'));
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");
1216 $totalWaitTime = time() - $startTime;
1217 Log::trace(
"Service deletion timeout after " . $totalWaitTime .
" seconds - service still exists: " . $this->
getName());
1233 Log::trace(
"Service is still running, stopping it first");
1234 if (!$this->
stop()) {
1235 Log::trace(
"Failed to stop service during ensureReset");
1243 if (!$this->
delete()) {
1244 Log::trace(
"Service deletion failed, but continuing with wait");
1249 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 getAppBinsRegKey($fromRegistry=true)
static formatWindowsPath($path)
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
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