Kategorien
PHP Server Administration

PHP Skript als Windows-Dienst ausführen

Unter Windows lassen sich Skripte, die endlos laufen sollen mit PHP mittels eines Dienstes realisieren.

Dies hat den Vorteil, dass der Speicherverbrauch nicht mit der Zeit ins unendliche geht, bei endloser Skriptausführungen und eine Recovery und Restart Funktionalität implementiert werden kann, um den Dienst über lange Zeiträume am Laufen zu halten.

Außerdem erhält der Dienst vom Betriebsystem Events, wenn z.B. eine Shutdown ansteht, um sich rechtzeitig selber beenden zu können und keine korrupten Daten zu produzieren beim Abbruch in einer nicht atomaren Operation.

Um einen Windows Dienst anzulegen benötigt man die win32service PHP-Library.

Diese kann man hier downloaden und in der php.ini einbinden:

extension=php_win32service.dll

Dienst anlegen

Um einen Dienst erstellen und steuern zu können, kann man folgenden Code verwenden und in der Datei win32_service.php speichern:

<?php
//No timeouts, Flush Content immediatly
set_time_limit(0);
ob_implicit_flush();

$serviceName = 'FooCommand';

$serviceAction = $argv[1];

switch ($argv[1]) {
    case  'install': {
        $result = win32_create_service(array(
            'service' => $serviceName,
            'display' => $serviceName,
            'description' => $serviceName,
            'params' => sprintf('%s\win32_run.php', __DIR__),
            'recovery_delay'        => 1000,                                               // Recovery action is executed after 10s
            'recovery_action_1'     => WIN32_SC_ACTION_RESTART,
            'recovery_action_2'     => WIN32_SC_ACTION_RESTART,
            'recovery_action_3'     => WIN32_SC_ACTION_RESTART,
            'recovery_reset_period' => 3600,                                                // Reset the fail counter after 1 hour
        ));
        break;
    }
    case  'uninstall': {
        $result = win32_delete_service($serviceName);
        break;
    }
    case  'stop': {
        $result = win32_stop_service($serviceName);
        break;
    }
    case  'status': {
        $ServiceStatus = win32_query_service_status($serviceName);
        if ( $ServiceStatus['CurrentState'] == WIN32_SERVICE_STOPPED ) {
            echo "Service Stopped\n\n";
        } else if ( $ServiceStatus['CurrentState'] == WIN32_SERVICE_START_PENDING ) {
            echo "Service Start Pending\n\n";
        } else if ( $ServiceStatus['CurrentState'] == WIN32_SERVICE_STOP_PENDING ) {
            echo "Service Stop Pending\n\n";
        } else if ( $ServiceStatus['CurrentState'] == WIN32_SERVICE_RUNNING ) {
            echo "Service Running\n\n";
        } else if ( $ServiceStatus['CurrentState'] == WIN32_SERVICE_CONTINUE_PENDING ) {
            echo "Service Continue Pending\n\n";
        } else if ( $ServiceStatus['CurrentState'] == WIN32_SERVICE_PAUSE_PENDING ) {
            echo "Service Pause Pending\n\n";
        } else if ( $ServiceStatus['CurrentState'] == WIN32_SERVICE_PAUSED ) {
            echo "Service Paused\n\n";
        } else{
            echo "Service Unknown\n\n";
        }
        exit;
    }
    default: {
        throw new Exception('unknow argument');
    }
}

switch($result) {
    case WIN32_ERROR_SERVICE_DOES_NOT_EXIST:
        throw new Exception('service not found');
    case WIN32_SERVICE_ERROR_IGNORE:
        die('service successfully installed/uninstalled');
    case WIN32_ERROR_ACCESS_DENIED:
        throw new Exception('access denied, run command as administrator');
    case WIN32_ERROR_SERVICE_EXISTS:
        throw new Exception('service already exists, uninstall service prior');
    case WIN32_ERROR_SERVICE_MARKED_FOR_DELETE:
        throw new Exception(sprintf('service has been marked for deletion, look here https://stackoverflow.com/questions/20561990/how-to-solve-the-specified-service-has-been-marked-for-deletion-error'));
    case WIN32_SERVICE_START_PENDING:
        throw new Exception('service start is pending');
    case WIN32_ERROR_FAILED_SERVICE_CONTROLLER_CONNECT:
        echo 'can not connect to service controller'. PHP_EOL;
        break;
    default:
        # search for hex value of result win32service lib
        debug_zval_dump($result);
}

und ausführen (CMD als Administrator ausführen) mit:

run with: php win32_service.php install
//or
run with: php win32_service.php uninstall
//or
run with: php win32_service.php status
//or
run with: php win32_service.php stop

Dann sollte unter Systemsteuerung -> Dienste der Dienst FooCommand erstellt worden sein, der beim nächsten Windows Start automatisch gestartet wird.

Windows Dienst mit PHP Skript erstellen

Das langlaufende PHP Skript

Das Skript, dass über den Dienst ausgeführt werden soll befindet sich in der Datei: win32_run.php

<?php
set_time_limit(0);
ob_implicit_flush();
$serviceName = 'FooCommand';

$result = win32_start_service_ctrl_dispatcher($serviceName);
if (!$result) {
    die("I'm probably not running under the service control manager");
}
debug_zval_dump($result);
win32_set_service_status(WIN32_SERVICE_START_PENDING);

// Some lengthy process to get this service up and running.

win32_set_service_status(WIN32_SERVICE_RUNNING);

while (WIN32_SERVICE_CONTROL_STOP != win32_get_last_control_message()) {
    exec(sprintf('php %s/script.php', __DIR__));
}

In der script.php kann jetzt der auszuführende endlos Code eingefügt werden:

<?php
echo "hello service";

Konfiguration des Dienstes

Der Dienst kann im Fehlerfall genau konfiguriert werden:

Dienst Konfiguration im Fehlerfall

Debugging des Dienstes

Wenn der Dienst nicht auf Anhieb läuft, kann man eine Fehlermeldung in Systemsteuerung -> Ereignisanzeige mehr Aufschluss geben:

Windows Dienst Ereignisanzeige