2024.8.23
Loading...
Searching...
No Matches
class.win32service.php
Go to the documentation of this file.
1<?php
2/*
3 * Copyright (c) 2021-2024 Bearsampp
4 * License: GNU General Public License version 3 or later; see LICENSE.txt
5 * Author: Bear
6 * Website: https://bearsampp.com
7 * Github: https://github.com/Bearsampp
8 */
9
10/**
11 * Class Win32Service
12 *
13 * This class provides an interface to manage Windows services. It includes methods to create, delete, start, stop, and query the status of services.
14 * It also handles logging and error reporting for service operations.
15 */
17{
18 // Win32Service Service Status Constants
26 const WIN32_SERVICE_NA = '0';
27
28 // Win32 Error Codes
58 const WIN32_NO_ERROR = '0';
59
62
63 const SERVICE_AUTO_START = '2';
65 const SERVICE_DISABLED = '4';
66
67 const PENDING_TIMEOUT = 20;
68 const SLEEP_TIME = 500000;
69
70 const VBS_NAME = 'Name';
71 const VBS_DISPLAY_NAME = 'DisplayName';
72 const VBS_DESCRIPTION = 'Description';
73 const VBS_PATH_NAME = 'PathName';
74 const VBS_STATE = 'State';
75
76 private $name;
77 private $displayName;
78 private $binPath;
79 private $params;
80 private $startType;
82 private $nssm;
83
85 private $latestError;
86
87 /**
88 * Constructor for the Win32Service class.
89 *
90 * @param string $name The name of the service.
91 */
92 public function __construct($name)
93 {
94 Util::logInitClass( $this );
95 $this->name = $name;
96 }
97
98 /**
99 * Writes a log entry.
100 *
101 * @param string $log The log message.
102 */
103 private function writeLog($log)
104 {
105 global $bearsamppRoot;
106 Util::logDebug( $log, $bearsamppRoot->getServicesLogFilePath() );
107 }
108
109 /**
110 * Returns an array of VBS keys used for service information.
111 *
112 * @return array The array of VBS keys.
113 */
114 public static function getVbsKeys()
115 {
116 return array(
117 self::VBS_NAME,
118 self::VBS_DISPLAY_NAME,
119 self::VBS_DESCRIPTION,
120 self::VBS_PATH_NAME,
121 self::VBS_STATE
122 );
123 }
124
125 /**
126 * Calls a Win32 service function.
127 *
128 * @param string $function The function name.
129 * @param mixed $param The parameter to pass to the function.
130 * @param bool $checkError Whether to check for errors.
131 *
132 * @return mixed The result of the function call.
133 */
134 private function callWin32Service($function, $param, $checkError = false)
135 {
136 $result = false;
137 if ( function_exists( $function ) ) {
138 $result = call_user_func( $function, $param );
139 if ( $checkError && dechex( $result ) != self::WIN32_NO_ERROR ) {
140 $this->latestError = dechex( $result );
141 }
142 }
143
144 return $result;
145 }
146
147 /**
148 * Queries the status of the service.
149 *
150 * @param bool $timeout Whether to use a timeout.
151 *
152 * @return string The status of the service.
153 */
154 public function status($timeout = true)
155 {
156 usleep( self::SLEEP_TIME );
157
158 $this->latestStatus = self::WIN32_SERVICE_NA;
159 $maxtime = time() + self::PENDING_TIMEOUT;
160
161 while ( $this->latestStatus == self::WIN32_SERVICE_NA || $this->isPending( $this->latestStatus ) ) {
162 $this->latestStatus = $this->callWin32Service( 'win32_query_service_status', $this->getName() );
163 if ( is_array( $this->latestStatus ) && isset( $this->latestStatus['CurrentState'] ) ) {
164 $this->latestStatus = dechex( $this->latestStatus['CurrentState'] );
165 }
166 elseif ( dechex( $this->latestStatus ) == self::WIN32_ERROR_SERVICE_DOES_NOT_EXIST ) {
167 $this->latestStatus = dechex( $this->latestStatus );
168 }
169 if ( $timeout && $maxtime < time() ) {
170 break;
171 }
172 }
173
174 if ( $this->latestStatus == self::WIN32_ERROR_SERVICE_DOES_NOT_EXIST ) {
175 $this->latestError = $this->latestStatus;
176 $this->latestStatus = self::WIN32_SERVICE_NA;
177 }
178
179 return $this->latestStatus;
180 }
181
182 /**
183 * Creates the service.
184 *
185 * @return bool True if the service was created successfully, false otherwise.
186 */
187 public function create()
188 {
189 global $bearsamppBins;
190
191 if ( $this->getName() == BinFilezilla::SERVICE_NAME ) {
192 $bearsamppBins->getFilezilla()->rebuildConf();
193
195 }
196 elseif ( $this->getName() == BinPostgresql::SERVICE_NAME ) {
197 $bearsamppBins->getPostgresql()->rebuildConf();
198 $bearsamppBins->getPostgresql()->initData();
199
201 }
202 if ( $this->getNssm() instanceof Nssm ) {
203 $nssmEnvPath = Util::getAppBinsRegKey( false );
204 $nssmEnvPath .= Util::getNssmEnvPaths();
205 $nssmEnvPath .= '%SystemRoot%/system32;';
206 $nssmEnvPath .= '%SystemRoot%;';
207 $nssmEnvPath .= '%SystemRoot%/system32/Wbem;';
208 $nssmEnvPath .= '%SystemRoot%/system32/WindowsPowerShell/v1.0';
209 $this->getNssm()->setEnvironmentExtra( 'PATH=' . $nssmEnvPath );
210
211 return $this->getNssm()->create();
212 }
213
214 $create = dechex( $this->callWin32Service( 'win32_create_service', array(
215 'service' => $this->getName(),
216 'display' => $this->getDisplayName(),
217 'description' => $this->getDisplayName(),
218 'path' => $this->getBinPath(),
219 'params' => $this->getParams(),
220 'start_type' => $this->getStartType() != null ? $this->getStartType() : self::SERVICE_DEMAND_START,
221 'error_control' => $this->getErrorControl() != null ? $this->getErrorControl() : self::SERVER_ERROR_NORMAL,
222 ), true ) );
223
224 $this->writeLog( 'Create service: ' . $create . ' (status: ' . $this->status() . ')' );
225 $this->writeLog( '-> service: ' . $this->getName() );
226 $this->writeLog( '-> display: ' . $this->getDisplayName() );
227 $this->writeLog( '-> description: ' . $this->getDisplayName() );
228 $this->writeLog( '-> path: ' . $this->getBinPath() );
229 $this->writeLog( '-> params: ' . $this->getParams() );
230 $this->writeLog( '-> start_type: ' . ($this->getStartType() != null ? $this->getStartType() : self::SERVICE_DEMAND_START) );
231 $this->writeLog( '-> service: ' . ($this->getErrorControl() != null ? $this->getErrorControl() : self::SERVER_ERROR_NORMAL) );
232
233 if ( $create != self::WIN32_NO_ERROR ) {
234 return false;
235 }
236 elseif ( !$this->isInstalled() ) {
237 $this->latestError = self::WIN32_NO_ERROR;
238
239 return false;
240 }
241
242 return true;
243 }
244
245 /**
246 * Deletes the service.
247 *
248 * @return bool True if the service was deleted successfully, false otherwise.
249 */
250 public function delete()
251 {
252 if ( !$this->isInstalled() ) {
253 return true;
254 }
255
256 $this->stop();
257
258 if ( $this->getName() == BinFilezilla::SERVICE_NAME ) {
260 }
261 elseif ( $this->getName() == BinPostgresql::SERVICE_NAME ) {
263 }
264
265 $delete = dechex( $this->callWin32Service( 'win32_delete_service', $this->getName(), true ) );
266 $this->writeLog( 'Delete service ' . $this->getName() . ': ' . $delete . ' (status: ' . $this->status() . ')' );
267
268 if ( $delete != self::WIN32_NO_ERROR && $delete != self::WIN32_ERROR_SERVICE_DOES_NOT_EXIST ) {
269 return false;
270 }
271 elseif ( $this->isInstalled() ) {
272 $this->latestError = self::WIN32_NO_ERROR;
273
274 return false;
275 }
276
277 return true;
278 }
279
280 /**
281 * Resets the service by deleting and recreating it.
282 *
283 * @return bool True if the service was reset successfully, false otherwise.
284 */
285 public function reset()
286 {
287 if ( $this->delete() ) {
288 usleep( self::SLEEP_TIME );
289
290 return $this->create();
291 }
292
293 return false;
294 }
295
296 /**
297 * Starts the service.
298 *
299 * @return bool True if the service was started successfully, false otherwise.
300 */
301 public function start()
302 {
303 global $bearsamppBins;
304
305 Util::logInfo('Attempting to start service: ' . $this->getName());
306
307 if ( $this->getName() == BinFilezilla::SERVICE_NAME ) {
308 $bearsamppBins->getFilezilla()->rebuildConf();
309 }
310 elseif ( $this->getName() == BinMysql::SERVICE_NAME ) {
311 $bearsamppBins->getMysql()->initData();
312 }
313 elseif ( $this->getName() == BinMailhog::SERVICE_NAME ) {
314 $bearsamppBins->getMailhog()->rebuildConf();
315 }
316 elseif ( $this->getName() == BinMailpit::SERVICE_NAME ) {
317 $bearsamppBins->getMailpit()->rebuildConf();
318 }
319 elseif ( $this->getName() == BinMemcached::SERVICE_NAME ) {
320 $bearsamppBins->getMemcached()->rebuildConf();
321 }
322 elseif ( $this->getName() == BinPostgresql::SERVICE_NAME ) {
323 $bearsamppBins->getPostgresql()->rebuildConf();
324 $bearsamppBins->getPostgresql()->initData();
325 }
326 elseif ( $this->getName() == BinXlight::SERVICE_NAME ) {
327 $bearsamppBins->getXlight()->rebuildConf();
328 }
329
330
331 $start = dechex( $this->callWin32Service( 'win32_start_service', $this->getName(), true ) );
332 Util::logDebug( 'Start service ' . $this->getName() . ': ' . $start . ' (status: ' . $this->status() . ')' );
333
334 if ( $start != self::WIN32_NO_ERROR && $start != self::WIN32_ERROR_SERVICE_ALREADY_RUNNING ) {
335
336 // Write error to log
337 Util::logError('Failed to start service: ' . $this->getName() . ' with error code: ' . $start);
338
339 if ( $this->getName() == BinApache::SERVICE_NAME ) {
340 $cmdOutput = $bearsamppBins->getApache()->getCmdLineOutput( BinApache::CMD_SYNTAX_CHECK );
341 if ( !$cmdOutput['syntaxOk'] ) {
342 file_put_contents(
343 $bearsamppBins->getApache()->getErrorLog(),
344 '[' . date( 'Y-m-d H:i:s', time() ) . '] [error] ' . $cmdOutput['content'] . PHP_EOL,
345 FILE_APPEND
346 );
347 }
348 }
349 elseif ( $this->getName() == BinMysql::SERVICE_NAME ) {
350 $cmdOutput = $bearsamppBins->getMysql()->getCmdLineOutput( BinMysql::CMD_SYNTAX_CHECK );
351 if ( !$cmdOutput['syntaxOk'] ) {
352 file_put_contents(
353 $bearsamppBins->getMysql()->getErrorLog(),
354 '[' . date( 'Y-m-d H:i:s', time() ) . '] [error] ' . $cmdOutput['content'] . PHP_EOL,
355 FILE_APPEND
356 );
357 }
358 }
359 elseif ( $this->getName() == BinMariadb::SERVICE_NAME ) {
360 $cmdOutput = $bearsamppBins->getMariadb()->getCmdLineOutput( BinMariadb::CMD_SYNTAX_CHECK );
361 if ( !$cmdOutput['syntaxOk'] ) {
362 file_put_contents(
363 $bearsamppBins->getMariadb()->getErrorLog(),
364 '[' . date( 'Y-m-d H:i:s', time() ) . '] [error] ' . $cmdOutput['content'] . PHP_EOL,
365 FILE_APPEND
366 );
367 }
368 }
369
370 return false;
371 }
372 elseif ( !$this->isRunning() ) {
373 $this->latestError = self::WIN32_NO_ERROR;
374 Util::logError('Service ' . $this->getName() . ' is not running after start attempt.');
375 $this->latestError = null;
376 return false;
377 }
378
379 Util::logInfo('Service ' . $this->getName() . ' started successfully.');
380 return true;
381 }
382
383 /**
384 * Stops the service.
385 *
386 * @return bool True if the service was stopped successfully, false otherwise.
387 */
388 public function stop()
389 {
390 $stop = dechex( $this->callWin32Service( 'win32_stop_service', $this->getName(), true ) );
391 $this->writeLog( 'Stop service ' . $this->getName() . ': ' . $stop . ' (status: ' . $this->status() . ')' );
392
393 if ( $stop != self::WIN32_NO_ERROR ) {
394 return false;
395 }
396 elseif ( !$this->isStopped() ) {
397 $this->latestError = self::WIN32_NO_ERROR;
398
399 return false;
400 }
401
402 return true;
403 }
404
405 /**
406 * Restarts the service by stopping and then starting it.
407 *
408 * @return bool True if the service was restarted successfully, false otherwise.
409 */
410 public function restart()
411 {
412 if ( $this->stop() ) {
413 return $this->start();
414 }
415
416 return false;
417 }
418
419 /**
420 * Retrieves information about the service.
421 *
422 * @return array The service information.
423 */
424 public function infos()
425 {
426 if ( $this->getNssm() instanceof Nssm ) {
427 return $this->getNssm()->infos();
428 }
429
430 return Vbs::getServiceInfos( $this->getName() );
431 }
432
433 /**
434 * Checks if the service is installed.
435 *
436 * @return bool True if the service is installed, false otherwise.
437 */
438 public function isInstalled()
439 {
440 $status = $this->status();
441 $this->writeLog( 'isInstalled ' . $this->getName() . ': ' . ($status != self::WIN32_SERVICE_NA ? 'YES' : 'NO') . ' (status: ' . $status . ')' );
442
443 return $status != self::WIN32_SERVICE_NA;
444 }
445
446 /**
447 * Checks if the service is running.
448 *
449 * @return bool True if the service is running, false otherwise.
450 */
451 public function isRunning()
452 {
453 $status = $this->status();
454 $this->writeLog( 'isRunning ' . $this->getName() . ': ' . ($status == self::WIN32_SERVICE_RUNNING ? 'YES' : 'NO') . ' (status: ' . $status . ')' );
455
456 return $status == self::WIN32_SERVICE_RUNNING;
457 }
458
459 /**
460 * Checks if the service is stopped.
461 *
462 * @return bool True if the service is stopped, false otherwise.
463 */
464 public function isStopped()
465 {
466 $status = $this->status();
467 $this->writeLog( 'isStopped ' . $this->getName() . ': ' . ($status == self::WIN32_SERVICE_STOPPED ? 'YES' : 'NO') . ' (status: ' . $status . ')' );
468
469 return $status == self::WIN32_SERVICE_STOPPED;
470 }
471
472 /**
473 * Checks if the service is paused.
474 *
475 * @return bool True if the service is paused, false otherwise.
476 */
477 public function isPaused()
478 {
479 $status = $this->status();
480 $this->writeLog( 'isPaused ' . $this->getName() . ': ' . ($status == self::WIN32_SERVICE_PAUSED ? 'YES' : 'NO') . ' (status: ' . $status . ')' );
481
482 return $status == self::WIN32_SERVICE_PAUSED;
483 }
484
485 /**
486 * Checks if the service is in a pending state.
487 *
488 * @param string $status The status to check.
489 *
490 * @return bool True if the service is in a pending state, false otherwise.
491 */
492 public function isPending($status)
493 {
494 return $status == self::WIN32_SERVICE_START_PENDING || $status == self::WIN32_SERVICE_STOP_PENDING
495 || $status == self::WIN32_SERVICE_CONTINUE_PENDING || $status == self::WIN32_SERVICE_PAUSE_PENDING;
496 }
497
498 /**
499 * Returns a description of the Win32 service status.
500 *
501 * @param string $status The status code.
502 *
503 * @return string|null The status description.
504 */
505 private function getWin32ServiceStatusDesc($status)
506 {
507 switch ( $status ) {
509 return 'The service continue is pending.';
510 break;
511
513 return 'The service pause is pending.';
514 break;
515
517 return 'The service is paused.';
518 break;
519
521 return 'The service is running.';
522 break;
523
525 return 'The service is starting.';
526 break;
527
529 return 'The service is stopping.';
530 break;
531
533 return 'The service is not running.';
534 break;
535
537 return 'Cannot retrieve service status.';
538 break;
539
540 default:
541 return null;
542 break;
543 }
544 }
545
546 /**
547 * Returns a description of the Win32 error code.
548 *
549 * @param string $code The error code.
550 *
551 * @return string|null The description of the error code, or null if the code is not recognized.
552 */
553 private function getWin32ErrorCodeDesc($code)
554 {
555 switch ( $code ) {
557 return 'The handle to the SCM database does not have the appropriate access rights.';
558 // ... other cases ...
559 default:
560 return null;
561 }
562 }
563
564 /**
565 * Gets the name of the service.
566 *
567 * @return string The name of the service.
568 */
569 public function getName()
570 {
571 return $this->name;
572 }
573
574 /**
575 * Sets the name of the service.
576 *
577 * @param string $name The name to set.
578 */
579 public function setName($name)
580 {
581 $this->name = $name;
582 }
583
584 /**
585 * Gets the display name of the service.
586 *
587 * @return string The display name of the service.
588 */
589 public function getDisplayName()
590 {
591 return $this->displayName;
592 }
593
594 /**
595 * Sets the display name of the service.
596 *
597 * @param string $displayName The display name to set.
598 */
600 {
601 $this->displayName = $displayName;
602 }
603
604 /**
605 * Gets the binary path of the service.
606 *
607 * @return string The binary path of the service.
608 */
609 public function getBinPath()
610 {
611 return $this->binPath;
612 }
613
614 /**
615 * Sets the binary path of the service.
616 *
617 * @param string $binPath The binary path to set.
618 */
619 public function setBinPath($binPath)
620 {
621 $this->binPath = str_replace( '"', '', Util::formatWindowsPath( $binPath ) );
622 }
623
624 /**
625 * Gets the parameters for the service.
626 *
627 * @return string The parameters for the service.
628 */
629 public function getParams()
630 {
631 return $this->params;
632 }
633
634 /**
635 * Sets the parameters for the service.
636 *
637 * @param string $params The parameters to set.
638 */
639 public function setParams($params)
640 {
641 $this->params = $params;
642 }
643
644 /**
645 * Gets the start type of the service.
646 *
647 * @return string The start type of the service.
648 */
649 public function getStartType()
650 {
651 return $this->startType;
652 }
653
654 /**
655 * Sets the start type of the service.
656 *
657 * @param string $startType The start type to set.
658 */
659 public function setStartType($startType)
660 {
661 $this->startType = $startType;
662 }
663
664 /**
665 * Gets the error control setting of the service.
666 *
667 * @return string The error control setting of the service.
668 */
669 public function getErrorControl()
670 {
671 return $this->errorControl;
672 }
673
674 /**
675 * Sets the error control setting of the service.
676 *
677 * @param string $errorControl The error control setting to set.
678 */
680 {
681 $this->errorControl = $errorControl;
682 }
683
684 /**
685 * Gets the NSSM instance associated with the service.
686 *
687 * @return Nssm The NSSM instance.
688 */
689 public function getNssm()
690 {
691 return $this->nssm;
692 }
693
694 /**
695 * Sets the NSSM instance associated with the service.
696 *
697 * @param Nssm $nssm The NSSM instance to set.
698 */
699 public function setNssm($nssm)
700 {
701 if ( $nssm instanceof Nssm ) {
702 $this->setDisplayName( $nssm->getDisplayName() );
703 $this->setBinPath( $nssm->getBinPath() );
704 $this->setParams( $nssm->getParams() );
705 $this->setStartType( $nssm->getStart() );
706 $this->nssm = $nssm;
707 }
708 }
709
710 /**
711 * Gets the latest status of the service.
712 *
713 * @return string The latest status of the service.
714 */
715 public function getLatestStatus()
716 {
717 return $this->latestStatus;
718 }
719
720 /**
721 * Gets the latest error encountered by the service.
722 *
723 * @return string The latest error encountered by the service.
724 */
725 public function getLatestError()
726 {
727 return $this->latestError;
728 }
729
730 /**
731 * Gets a detailed error message for the latest error encountered by the service.
732 *
733 * @return string|null The detailed error message, or null if no error.
734 */
735 public function getError()
736 {
737 global $bearsamppLang;
738 if ( $this->latestError != self::WIN32_NO_ERROR ) {
739 return $bearsamppLang->getValue( Lang::ERROR ) . ' ' .
740 $this->latestError . ' (' . hexdec( $this->latestError ) . ' : ' . $this->getWin32ErrorCodeDesc( $this->latestError ) . ')';
741 }
742 elseif ( $this->latestStatus != self::WIN32_SERVICE_NA ) {
743 return $bearsamppLang->getValue( Lang::STATUS ) . ' ' .
744 $this->latestStatus . ' (' . hexdec( $this->latestStatus ) . ' : ' . $this->getWin32ServiceStatusDesc( $this->latestStatus ) . ')';
745 }
746
747 return null;
748 }
749}
$result
global $bearsamppBins
global $bearsamppLang
global $bearsamppRoot
static installPostgresqlService()
static uninstallFilezillaService()
static uninstallPostgresqlService()
static installFilezillaService()
const CMD_SYNTAX_CHECK
const CMD_SYNTAX_CHECK
const SERVICE_NAME
const ERROR
const STATUS
static getNssmEnvPaths()
static logError($data, $file=null)
static logDebug($data, $file=null)
static logInitClass($classInstance)
static logInfo($data, $file=null)
static formatWindowsPath($path)
static getAppBinsRegKey($fromRegistry=true)
static getServiceInfos($serviceName)
const WIN32_ERROR_SERVICE_REQUEST_TIMEOUT
const WIN32_ERROR_SERVICE_EXISTS
const WIN32_ERROR_SERVICE_MARKED_FOR_DELETE
const WIN32_ERROR_SERVICE_DEPENDENCY_DELETED
const WIN32_ERROR_PATH_NOT_FOUND
const WIN32_ERROR_INVALID_HANDLE
const WIN32_ERROR_ACCESS_DENIED
const WIN32_ERROR_SHUTDOWN_IN_PROGRESS
const WIN32_ERROR_INVALID_LEVEL
const WIN32_SERVICE_STOP_PENDING
const WIN32_ERROR_SERVICE_CANNOT_ACCEPT_CTRL
const WIN32_ERROR_SERVICE_DISABLED
const WIN32_ERROR_CIRCULAR_DEPENDENCY
const WIN32_ERROR_INVALID_SERVICE_ACCOUNT
const WIN32_ERROR_SERVICE_LOGON_FAILED
setDisplayName($displayName)
const WIN32_ERROR_FAILED_SERVICE_CONTROLLER_CONNECT
const WIN32_ERROR_SERVICE_DATABASE_LOCKED
const WIN32_SERVICE_START_PENDING
setStartType($startType)
const WIN32_ERROR_SERVICE_ALREADY_RUNNING
const WIN32_SERVICE_CONTINUE_PENDING
callWin32Service($function, $param, $checkError=false)
const WIN32_ERROR_DATABASE_DOES_NOT_EXIST
const WIN32_ERROR_INSUFFICIENT_BUFFER
const WIN32_ERROR_DUPLICATE_SERVICE_NAME
const WIN32_ERROR_SERVICE_DOES_NOT_EXIST
const WIN32_SERVICE_PAUSE_PENDING
const WIN32_ERROR_SERVICE_DEPENDENCY_FAIL
setErrorControl($errorControl)
getWin32ServiceStatusDesc($status)
const WIN32_ERROR_DEPENDENT_SERVICES_RUNNING
const WIN32_ERROR_INVALID_SERVICE_CONTROL
const WIN32_ERROR_INVALID_PARAMETER
const WIN32_ERROR_SERVICE_NOT_ACTIVE
const WIN32_ERROR_SERVICE_NO_THREAD
status($timeout=true)