125 self::VBS_DISPLAY_NAME,
126 self::VBS_DESCRIPTION,
142 if (self::$serviceListCache ===
null || $forceRefresh) {
143 Log::trace(
'Fetching service list from Windows (COM/WMI)');
144 $startTime = microtime(
true);
147 self::$serviceListCache = [];
148 if (is_array($services)) {
149 foreach ($services as $service) {
150 if (isset($service[self::VBS_NAME])) {
151 self::$serviceListCache[$service[self::VBS_NAME]] = $service;
155 $duration = round(microtime(
true) - $startTime, 3);
156 Log::trace(
'Service list fetched in ' . $duration .
's');
159 return self::$serviceListCache;
174 if (function_exists($function)) {
175 if (!isset(self::$loggedFunctions[$function])) {
176 Log::trace(
'Win32 function: ' . $function .
' exists');
177 self::$loggedFunctions[$function] =
true;
181 if ($function ===
'win32_query_service_status') {
182 Log::trace(
"Using enhanced handling for win32_query_service_status");
185 $originalTimeout = ini_get(
'max_execution_time');
190 $result = call_user_func($function, $param);
193 set_time_limit($originalTimeout);
195 if ($checkError &&
$result !==
null) {
198 if (dechex($resultInt) != self::WIN32_NO_ERROR) {
199 $this->latestError = dechex($resultInt);
204 set_time_limit($originalTimeout);
206 Log::trace(
"Win32ServiceException caught: " . $e->getMessage());
209 if (strpos($e->getMessage(),
'service does not exist') !==
false) {
210 Log::trace(
"Service does not exist exception handled for: " . $param);
212 $result = hexdec(self::WIN32_ERROR_SERVICE_DOES_NOT_EXIST);
215 Log::trace(
"Unhandled Win32ServiceException: " . $e->getMessage());
218 }
catch (\Exception $e) {
220 set_time_limit($originalTimeout);
223 Log::trace(
"Exception caught in callWin32Service: " . $e->getMessage());
225 }
catch (\Throwable $e) {
227 set_time_limit($originalTimeout);
230 Log::trace(
"Throwable caught in callWin32Service: " . $e->getMessage());
237 $result = call_user_func($function, $param);
238 if ($checkError &&
$result !==
null) {
241 if (dechex($resultInt) != self::WIN32_NO_ERROR) {
242 $this->latestError = dechex($resultInt);
246 Log::trace(
"Win32ServiceException caught: " . $e->getMessage());
249 if (strpos($e->getMessage(),
'service does not exist') !==
false) {
250 Log::trace(
"Service does not exist exception handled for: " . $param);
252 $result = hexdec(self::WIN32_ERROR_SERVICE_DOES_NOT_EXIST);
255 Log::trace(
"Unhandled Win32ServiceException: " . $e->getMessage());
258 }
catch (\Exception $e) {
260 Log::trace(
"Exception caught in callWin32Service: " . $e->getMessage());
262 }
catch (\Throwable $e) {
264 Log::trace(
"Throwable caught in callWin32Service: " . $e->getMessage());
269 if (!isset(self::$loggedFunctions[$function])) {
270 Log::trace(
'Win32 function: ' . $function .
' missing');
271 self::$loggedFunctions[$function] =
true;
285 public function status($timeout =
true): string
287 $this->latestStatus = self::WIN32_SERVICE_NA;
288 $maxtime = time() + self::PENDING_TIMEOUT;
290 Log::trace(
"Querying status for service: " . $this->
getName() .
" (timeout: " . ($timeout ?
"enabled" :
"disabled") .
")");
292 Log::trace(
"Max timeout time set to: " . date(
'Y-m-d H:i:s', $maxtime));
298 $startTime = microtime(
true);
301 while (($this->latestStatus == self::WIN32_SERVICE_NA || $this->
isPending($this->latestStatus)) && $loopCount < $maxLoops) {
303 Log::trace(
"Calling win32_query_service_status for service: " . $this->
getName() .
" (attempt " . $loopCount .
" of " . $maxLoops .
")");
306 if (microtime(
true) - $startTime > 10) {
307 Log::trace(
"Overall timeout reached before making service status call");
313 if (is_array($this->latestStatus) && isset($this->latestStatus[
'CurrentState'])) {
315 $stateInt = is_numeric($this->latestStatus[
'CurrentState']) ? (int)$this->latestStatus[
'CurrentState'] : 0;
316 $this->latestStatus = dechex($stateInt);
317 Log::trace(
"Service status returned as array, CurrentState: " . $this->latestStatus);
318 } elseif ($this->latestStatus !==
null) {
320 $statusInt = is_numeric($this->latestStatus) ? (int)$this->latestStatus : 0;
321 $statusHex = dechex($statusInt);
322 Log::trace(
"Service status returned as value: " . $statusHex);
324 if ($statusHex == self::WIN32_ERROR_SERVICE_DOES_NOT_EXIST) {
325 $this->latestStatus = $statusHex;
326 Log::trace(
"Service does not exist, breaking loop");
330 Log::trace(
"Service status query returned null");
334 Log::trace(
"Multiple null results, assuming service does not exist");
335 $this->latestStatus = self::WIN32_ERROR_SERVICE_DOES_NOT_EXIST;
340 if ($timeout && $maxtime < time()) {
341 Log::trace(
"Timeout reached while querying service status");
346 if ($loopCount < $maxLoops && ($this->latestStatus == self::WIN32_SERVICE_NA || $this->
isPending($this->latestStatus))) {
347 Log::trace(
"Sleeping before next status check attempt");
348 usleep(self::SLEEP_TIME);
351 }
catch (\Exception $e) {
352 Log::trace(
"Exception in status method: " . $e->getMessage());
354 $this->latestStatus = self::WIN32_ERROR_SERVICE_DOES_NOT_EXIST;
355 }
catch (\Throwable $e) {
356 Log::trace(
"Throwable in status method: " . $e->getMessage());
358 $this->latestStatus = self::WIN32_ERROR_SERVICE_DOES_NOT_EXIST;
361 if ($loopCount >= $maxLoops) {
365 $elapsedTime = microtime(
true) - $startTime;
366 Log::trace(
"Status check completed in " . round($elapsedTime, 2) .
" seconds after " . $loopCount .
" attempts");
368 if ($this->latestStatus == self::WIN32_ERROR_SERVICE_DOES_NOT_EXIST) {
370 $this->latestStatus = self::WIN32_SERVICE_NA;
371 Log::trace(
"Service does not exist, setting status to NA");
374 Log::trace(
"Final status for service " . $this->
getName() .
": " . $this->latestStatus);
391 Log::trace(
"PostgreSQL service detected - using specialized installation");
393 Log::trace(
"PostgreSQL configuration rebuilt");
399 Log::trace(
"PostgreSQL service installation " . (
$result ?
"succeeded" :
"failed"));
405 Log::trace(
"Using NSSM for service installation");
408 global $bearsamppTools;
409 if (!isset($bearsamppTools)) {
410 Log::trace(
"Tools not loaded, forcing synchronous load");
414 global $bearsamppRegistry;
415 $nssmEnvPath = $bearsamppRegistry->getAppBinsRegKey(
false);
416 Log::trace(
"NSSM environment path (bins): " . $nssmEnvPath);
419 Log::trace(
"NSSM environment path (with additional paths): " . $nssmEnvPath);
421 $nssmEnvPath .=
'%SystemRoot%/system32;';
422 $nssmEnvPath .=
'%SystemRoot%;';
423 $nssmEnvPath .=
'%SystemRoot%/system32/Wbem;';
424 $nssmEnvPath .=
'%SystemRoot%/system32/WindowsPowerShell/v1.0';
425 Log::trace(
"NSSM final environment PATH: " . $nssmEnvPath);
427 $this->
getNssm()->setEnvironmentExtra(
'PATH=' . $nssmEnvPath);
446 Log::trace(
"Using win32_create_service for service installation");
447 $serviceParams = array(
457 Log::trace(
"win32_create_service parameters:");
458 foreach ($serviceParams as $key => $value) {
465 $create =
$result !==
null ? dechex($resultInt) :
'0';
466 Log::trace(
"win32_create_service result code: " . $create);
469 if ($create == self::WIN32_ERROR_SERVICE_MARKED_FOR_DELETE) {
470 Log::trace(
"Service marked for delete, waiting 2s before retry: " . $this->
getName());
474 $create =
$result !==
null ? dechex($resultInt) :
'0';
475 Log::trace(
"win32_create_service retry result code: " . $create);
478 $this->
writeLog(
'Create service: ' . $create .
' (status: ' . $this->
status() .
')');
487 if ($create != self::WIN32_NO_ERROR) {
488 Log::trace(
"Service creation failed with error code: " . $create);
492 Log::trace(
"Service created but not found as installed");
493 $this->latestError = self::WIN32_NO_ERROR;
508 public function delete(): bool
524 Log::trace(
"Killing NSSM child process after stop: " . $childExe);
529 Log::trace(
"PostgreSQL service detected - using specialized uninstallation");
531 Log::trace(
"PostgreSQL service uninstallation " . (
$result ?
"succeeded" :
"failed"));
540 $delete =
$result !==
null ? dechex($resultInt) :
'0';
541 Log::trace(
"Delete service result code: " . $delete);
542 $this->
writeLog(
'Delete service ' . $this->
getName() .
': ' . $delete .
' (status: ' . $this->
status() .
')');
544 if ($delete != self::WIN32_NO_ERROR && $delete != self::WIN32_ERROR_SERVICE_DOES_NOT_EXIST) {
547 $this->latestError = self::WIN32_NO_ERROR;
562 if ($this->
delete()) {
563 usleep(self::SLEEP_TIME);
601 $start =
$result !==
null ? dechex($resultInt) :
'0';
604 if ($start != self::WIN32_NO_ERROR && $start != self::WIN32_ERROR_SERVICE_ALREADY_RUNNING) {
606 Log::error(
'Failed to start service: ' . $this->
getName() .
' with error code: ' . $start);
610 if (!$cmdOutput[
'syntaxOk']) {
613 '[' . 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,
628 if (!$cmdOutput[
'syntaxOk']) {
631 '[' . date(
'Y-m-d H:i:s', time()) .
'] [error] ' . $cmdOutput[
'content'] . PHP_EOL,
642 $maxtime = time() + self::PENDING_TIMEOUT;
644 usleep(self::SLEEP_TIME);
648 $this->latestError = self::WIN32_NO_ERROR;
649 Log::error(
'Service ' . $this->
getName() .
' is not running after start attempt (status: ' . $this->
status() .
').');
650 $this->latestError =
null;
674 $stop =
$result !==
null ? dechex($resultInt) :
'0';
675 Log::trace(
"Stop service result code: " . $stop);
677 Log::trace(
"Checking current status after stop attempt");
678 $currentStatus = $this->
status();
679 Log::trace(
"Current status: " . $currentStatus);
681 $this->
writeLog(
'Stop service ' . $this->
getName() .
': ' . $stop .
' (status: ' . $currentStatus .
')');
683 if ($stop != self::WIN32_NO_ERROR) {
688 $maxtime = time() + self::PENDING_TIMEOUT;
690 usleep(self::SLEEP_TIME);
694 $this->latestError = self::WIN32_NO_ERROR;
695 Log::error(
'Service ' . $this->
getName() .
' is still running after stop attempt (status: ' . $this->
status() .
').');
696 $this->latestError =
null;
712 return $this->
start();
728 $startTime = microtime(
true);
735 $duration = round(microtime(
true) - $startTime, 3);
737 Log::trace(
"sc.exe query completed in " . $duration .
"s");
739 if ($output ===
null || $output ===
false) {
740 Log::trace(
"sc.exe returned null/false, service likely doesn't exist");
746 if (stripos($output,
'does not exist') !==
false ||
747 stripos($output,
'FAILED') !==
false ||
748 stripos($output,
'1060') !==
false) {
758 if (preg_match(
'/SERVICE_NAME:\s*(.+)/i', $output, $matches)) {
759 $serviceInfo[self::VBS_NAME] = trim($matches[1]);
763 if (preg_match(
'/DISPLAY_NAME:\s*(.+)/i', $output, $matches)) {
764 $serviceInfo[self::VBS_DISPLAY_NAME] = trim($matches[1]);
768 if (preg_match(
'/STATE\s*:\s*\d+\s+(\w+)/i', $output, $matches)) {
769 $state = trim($matches[1]);
770 $serviceInfo[self::SERVICE_STATE] = $state;
775 if (!empty($serviceInfo)) {
776 Log::trace(
"Service exists, getting full details");
781 if ($configOutput !==
null && $configOutput !==
false && preg_match(
'/BINARY_PATH_NAME\s*:\s*(.+)/i', $configOutput, $matches)) {
782 $serviceInfo[self::VBS_PATH_NAME] = trim($matches[1]);
783 Log::trace(
"Service path: " . $serviceInfo[self::VBS_PATH_NAME]);
787 if ($configOutput !==
null && $configOutput !==
false && preg_match(
'/DISPLAY_NAME\s*:\s*(.+)/i', $configOutput, $matches)) {
788 $serviceInfo[self::VBS_DESCRIPTION] = trim($matches[1]);
796 Log::trace(
"Could not parse service info from sc.exe output");
813 $startTime = microtime(
true);
819 Log::trace(
"NSSM info retrieval completed in " . round(microtime(
true) - $startTime, 2) .
" seconds");
825 Log::trace(
"Attempting fast service check using sc.exe");
828 if ($fastResult !==
false) {
829 $duration = round(microtime(
true) - $startTime, 3);
830 Log::trace(
"Fast service check succeeded in " . $duration .
"s (saved 5-10s)");
831 Log::debug(
"Performance: Fast service check used for " . $this->
getName() .
", saved 5-10 seconds");
837 if ($fastResult ===
false) {
838 $duration = round(microtime(
true) - $startTime, 3);
839 Log::trace(
"Fast service check determined service doesn't exist in " . $duration .
"s");
845 Log::trace(
"Falling back to VBS for service info");
848 $originalTimeout = ini_get(
'max_execution_time');
855 set_time_limit($originalTimeout);
858 if (microtime(
true) - $startTime > $timeout) {
859 Log::trace(
"Timeout exceeded in infos() method, returning false");
864 Log::trace(
"VBS info retrieval completed in " . round(microtime(
true) - $startTime, 2) .
" seconds");
867 }
catch (\Exception $e) {
868 Log::trace(
"Exception in infos() method: " . $e->getMessage() .
", returning false");
871 }
catch (\Throwable $e) {
872 Log::trace(
"Throwable in infos() method: " . $e->getMessage() .
", returning false");
889 $startTime = microtime(
true);
893 $status = $this->
status();
896 if (microtime(
true) - $startTime > $timeout) {
897 Log::trace(
"Timeout exceeded in isInstalled() method, assuming service is not installed");
898 $this->
writeLog(
'isInstalled ' . $this->
getName() .
': NO (timeout exceeded)');
903 $isInstalled = $status != self::WIN32_SERVICE_NA;
905 Log::trace(
"Service " . $this->
getName() .
" installation status: " . ($isInstalled ?
"YES" :
"NO") .
" (status code: " . $status .
")");
906 $this->
writeLog(
'isInstalled ' . $this->
getName() .
': ' . ($isInstalled ?
'YES' :
'NO') .
' (status: ' . $status .
')');
909 }
catch (\Exception $e) {
910 Log::trace(
"Exception in isInstalled() method: " . $e->getMessage() .
", assuming service is not installed");
911 $this->
writeLog(
'isInstalled ' . $this->
getName() .
': NO (exception: ' . $e->getMessage() .
')');
914 }
catch (\Throwable $e) {
915 Log::trace(
"Throwable in isInstalled() method: " . $e->getMessage() .
", assuming service is not installed");
916 $this->
writeLog(
'isInstalled ' . $this->
getName() .
': NO (throwable: ' . $e->getMessage() .
')');
931 $status = $this->
status();
932 $isRunning = $status == self::WIN32_SERVICE_RUNNING;
934 Log::trace(
"Service " . $this->
getName() .
" running status: " . ($isRunning ?
"YES" :
"NO") .
" (status code: " . $status .
")");
935 $this->
writeLog(
'isRunning ' . $this->
getName() .
': ' . ($isRunning ?
'YES' :
'NO') .
' (status: ' . $status .
')');
949 $status = $this->
status();
950 $isStopped = $status == self::WIN32_SERVICE_STOPPED;
952 Log::trace(
"Service " . $this->
getName() .
" stopped status: " . ($isStopped ?
"YES" :
"NO") .
" (status code: " . $status .
")");
953 $this->
writeLog(
'isStopped ' . $this->
getName() .
': ' . ($isStopped ?
'YES' :
'NO') .
' (status: ' . $status .
')');
967 $status = $this->
status();
968 $isPaused = $status == self::WIN32_SERVICE_PAUSED;
970 Log::trace(
"Service " . $this->
getName() .
" paused status: " . ($isPaused ?
"YES" :
"NO") .
" (status code: " . $status .
")");
971 $this->
writeLog(
'isPaused ' . $this->
getName() .
': ' . ($isPaused ?
'YES' :
'NO') .
' (status: ' . $status .
')');
985 $isPending = $status == self::WIN32_SERVICE_START_PENDING || $status == self::WIN32_SERVICE_STOP_PENDING
986 || $status == self::WIN32_SERVICE_CONTINUE_PENDING || $status == self::WIN32_SERVICE_PAUSE_PENDING;
988 Log::trace(
"Checking if status is pending: " . $status .
" - Result: " . ($isPending ?
"YES" :
"NO"));
991 if ($status == self::WIN32_SERVICE_START_PENDING) {
992 Log::trace(
"Service is in START_PENDING state");
994 if ($status == self::WIN32_SERVICE_STOP_PENDING) {
995 Log::trace(
"Service is in STOP_PENDING state");
997 if ($status == self::WIN32_SERVICE_CONTINUE_PENDING) {
998 Log::trace(
"Service is in CONTINUE_PENDING state");
1000 if ($status == self::WIN32_SERVICE_PAUSE_PENDING) {
1001 Log::trace(
"Service is in PAUSE_PENDING state");
1021 case self::WIN32_SERVICE_CONTINUE_PENDING:
1022 return 'The service continue is pending.';
1024 case self::WIN32_SERVICE_PAUSE_PENDING:
1025 return 'The service pause is pending.';
1027 case self::WIN32_SERVICE_PAUSED:
1028 return 'The service is paused.';
1030 case self::WIN32_SERVICE_RUNNING:
1031 return 'The service is running.';
1033 case self::WIN32_SERVICE_START_PENDING:
1034 return 'The service is starting.';
1036 case self::WIN32_SERVICE_STOP_PENDING:
1037 return 'The service is stopping.';
1039 case self::WIN32_SERVICE_STOPPED:
1040 return 'The service is not running.';
1042 case self::WIN32_SERVICE_NA:
1043 return 'Cannot retrieve service status.';
1060 case self::WIN32_ERROR_ACCESS_DENIED:
1061 return 'The handle to the SCM database does not have the appropriate access rights.';
1085 $this->name =
$name;
1210 $this->nssm =
$nssm;
1242 if ($this->latestError != self::WIN32_NO_ERROR) {
1244 $errorInt = is_numeric($this->latestError) ? hexdec($this->latestError) : 0;
1247 $this->latestError .
' (' . $errorInt .
' : ' . $this->
getWin32ErrorCodeDesc($this->latestError) .
')';
1248 } elseif ($this->latestStatus != self::WIN32_SERVICE_NA) {
1250 $statusInt = is_numeric($this->latestStatus) ? hexdec($this->latestStatus) : 0;
1270 $startTime = time();
1271 $maxTime = $startTime + $maxWaitTime;
1274 Log::trace(
"Waiting for service deletion: " . $this->
getName() .
" (max wait: " . $maxWaitTime .
"s)");
1276 while (time() < $maxTime) {
1278 $status = $this->
status(
false);
1279 Log::trace(
"Service deletion check #" . $checkCount .
" - Status: " . $status .
" at " . date(
'Y-m-d H:i:s'));
1282 if ($status == self::WIN32_SERVICE_NA ||
1283 $status == self::WIN32_ERROR_SERVICE_DOES_NOT_EXIST) {
1284 $elapsedTime = time() - $startTime;
1285 Log::trace(
"Service deletion confirmed after " . $elapsedTime .
" seconds");
1294 $totalWaitTime = time() - $startTime;
1295 Log::trace(
"Service deletion timeout after " . $totalWaitTime .
" seconds - service still exists: " . $this->
getName());
1312 Log::trace(
"Service is still running, stopping it first");
1313 if (!$this->
stop()) {
1314 Log::trace(
"Failed to stop service during ensureReset");
1323 if (!$this->
delete()) {
1324 Log::trace(
"Service deletion failed, but continuing with wait");
1329 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