¿Cómo se puede escribir una aplicación de servicio de Windows en Haskell?
He estado luchando para escribir una aplicación de servicio de Windows en Haskell.
Antecedentes
El Administrador de Control de Servicios de Windows ejecuta una aplicación de servicio. Al iniciar, realiza una llamada de bloqueo a StartServiceCtrlDispatcherque se suministra con una devolución de llamada para ser utilizada como la función principal del servicio .
Se supone que la función principal del servicio registra una segunda devolución de llamada para manejar comandos entrantes como start, stop, continuar etc. Lo hace llamando a RegisterServiceCtrlHandler.
Problema
Soy capaz de escribir un programa que registrará una función principal de servicio. Luego puedo instalar el programa como un servicio de Windows e iniciarlo desde la consola de administración de servicios. El servicio es capaz de iniciar, informar a sí mismo como en ejecución, y luego esperar a las solicitudes entrantes.
El problema es que no puedo hacer que se llame a mi función de controlador de servicio. Consultar el el estado de los servicios revela que se está ejecutando, pero tan pronto como le envío un comando 'stop', Windows aparece un mensaje que dice:
Windows could not stop the Test service on Local Computer.
Error 1061: The service cannot accept control messages at this time.
De acuerdo con MSDN documentation la función StartServiceCtrlDispatcher se bloquea hasta que todos los servicios informan que se han detenido. Después de que se llame a la función principal del servicio, se supone que un subproceso de despachador debe esperar hasta que el Administrador de Control de Servicio envíe un comando, momento en el que la función manejadora debe ser llamada por eso hilo.
Detalles
Lo que sigue es una versión muy simplificada de lo que estoy tratando de hacer, pero demuestra el problema de no llamar a mi función de controlador.
Primero, algunos nombres e importaciones:
module Main where
import Control.Applicative
import Foreign
import System.Win32
wIN32_OWN_PROCESS :: DWORD
wIN32_OWN_PROCESS = 0x00000010
sTART_PENDING, rUNNING :: DWORD
sTART_PENDING = 0x00000002
rUNNING = 0x00000004
aCCEPT_STOP, aCCEPT_NONE :: DWORD
aCCEPT_STOP = 0x00000001
aCCEPT_NONE = 0x00000000
nO_ERROR :: DWORD
nO_ERROR = 0x00000000
type HANDLER_FUNCTION = DWORD -> IO ()
type MAIN_FUNCTION = DWORD -> Ptr LPTSTR -> IO ()
Necesito definir algunos tipos de datos especiales con instancias almacenables para el marshalling de datos:
data TABLE_ENTRY = TABLE_ENTRY LPTSTR (FunPtr MAIN_FUNCTION)
instance Storable TABLE_ENTRY where
sizeOf _ = 8
alignment _ = 4
peek ptr = TABLE_ENTRY <$> peek (castPtr ptr) <*> peek (castPtr ptr `plusPtr` 4)
poke ptr (TABLE_ENTRY name proc) = do
poke (castPtr ptr) name
poke (castPtr ptr `plusPtr` 4) proc
data STATUS = STATUS DWORD DWORD DWORD DWORD DWORD DWORD DWORD
instance Storable STATUS where
sizeOf _ = 28
alignment _ = 4
peek ptr = STATUS
<$> peek (castPtr ptr)
<*> peek (castPtr ptr `plusPtr` 4)
<*> peek (castPtr ptr `plusPtr` 8)
<*> peek (castPtr ptr `plusPtr` 12)
<*> peek (castPtr ptr `plusPtr` 16)
<*> peek (castPtr ptr `plusPtr` 20)
<*> peek (castPtr ptr `plusPtr` 24)
poke ptr (STATUS a b c d e f g) = do
poke (castPtr ptr) a
poke (castPtr ptr `plusPtr` 4) b
poke (castPtr ptr `plusPtr` 8) c
poke (castPtr ptr `plusPtr` 12) d
poke (castPtr ptr `plusPtr` 16) e
poke (castPtr ptr `plusPtr` 20) f
poke (castPtr ptr `plusPtr` 24) g
Solo se necesitan tres importaciones extranjeras. Hay una importación de 'wrapper' para las dos devoluciones de llamada que suministraré a Win32:
foreign import stdcall "wrapper"
smfToFunPtr :: MAIN_FUNCTION -> IO (FunPtr MAIN_FUNCTION)
foreign import stdcall "wrapper"
handlerToFunPtr :: HANDLER_FUNCTION -> IO (FunPtr HANDLER_FUNCTION)
foreign import stdcall "windows.h RegisterServiceCtrlHandlerW"
c_RegisterServiceCtrlHandler
:: LPCTSTR -> FunPtr HANDLER_FUNCTION -> IO HANDLE
foreign import stdcall "windows.h SetServiceStatus"
c_SetServiceStatus :: HANDLE -> Ptr STATUS -> IO BOOL
foreign import stdcall "windows.h StartServiceCtrlDispatcherW"
c_StartServiceCtrlDispatcher :: Ptr TABLE_ENTRY -> IO BOOL
Programa principal
Finalmente, aquí está la aplicación de servicio principal:
main :: IO ()
main =
withTString "Test" $ \name ->
smfToFunPtr svcMain >>= \fpMain ->
withArray [TABLE_ENTRY name fpMain, TABLE_ENTRY nullPtr nullFunPtr] $ \ste ->
c_StartServiceCtrlDispatcher ste >> return ()
svcMain :: MAIN_FUNCTION
svcMain argc argv = do
appendFile "c:\\log.txt" "svcMain: svcMain here!\n"
args <- peekArray (fromIntegral argc) argv
fpHandler <- handlerToFunPtr svcHandler
h <- c_RegisterServiceCtrlHandler (head args) fpHandler
_ <- setServiceStatus h running
appendFile "c:\\log.txt" "svcMain: exiting\n"
svcHandler :: DWORD -> IO ()
svcHandler _ = appendFile "c:\\log.txt" "svcCtrlHandler: received.\n"
setServiceStatus :: HANDLE -> STATUS -> IO BOOL
setServiceStatus h status = with status $ c_SetServiceStatus h
running :: STATUS
running = STATUS wIN32_OWN_PROCESS rUNNING aCCEPT_STOP nO_ERROR 0 0 3000
Salida
He instalado previamente el servicio usando sc create Test binPath= c:\Main.exe
.
Aquí está la salida de la compilación del programa:
C:\path>ghc -threaded --make Main.hs
[1 of 1] Compiling Main ( Main.hs, Main.o )
Linking Main.exe ...
C:\path>
Luego inicio el servicio desde el Monitor de Control de Servicio. He aquí la prueba de que mi llamada a SetServiceStatus fue aceptada:
C:\Path>sc query Test
SERVICE_NAME: Test
TYPE : 10 WIN32_OWN_PROCESS
STATE : 4 RUNNING
(STOPPABLE, NOT_PAUSABLE, IGNORES_SHUTDOWN)
WIN32_EXIT_CODE : 0 (0x0)
SERVICE_EXIT_CODE : 0 (0x0)
CHECKPOINT : 0x0
WAIT_HINT : 0x0
C:\Path>
Aquí está el contenido de log.txt
, demostrando que mi primera devolución de llamada, svcMain
, fue llamado:
svcMain: svcMain here!
svcMain: exiting
Tan pronto como envío un comando stop usando el Administrador de Control de Servicio, recibo mi mensaje de error. Se suponía que mi función handler debía agregar una línea al archivo de registro, pero esto no sucede. Mi servicio aparece entonces en el estado detenido:
C:\Path>sc query Test
SERVICE_NAME: Test
TYPE : 10 WIN32_OWN_PROCESS
STATE : 1 STOPPED
WIN32_EXIT_CODE : 0 (0x0)
SERVICE_EXIT_CODE : 0 (0x0)
CHECKPOINT : 0x0
WAIT_HINT : 0x0
C:\Path>
Pregunta
¿Alguien tiene ideas sobre lo que puedo intentar para que se llame a mi función de controlador?
Actualización 20130306
Tengo este problema en Windows 7 de 64 bits, pero no en Windows XP. Otro las versiones de Windows no han sido probadas todavía. Cuando copio el ejecutable compilado en varias máquinas y realizo los mismos pasos, obtengo resultados diferentes.
3 answers
Admito que este problema me ha estado molestando desde hace algunos días. De recorrer los valores de retorno y el contenido de GetLastError
, He determinado que este código debería funcionar correctamente de acuerdo con el sistema.
Debido a que claramente no lo es (parece ingresar un estado indefinido que inhibe que el controlador de servicio se ejecute con éxito), he publicado mi diagnóstico completo y una solución alternativa. Este es el tipo exacto de escenario Microsoft debe ser consciente de, porque su las garantías de la interfaz no están siendo respetadas.
Inspección
Después de quedar muy insatisfecho con los mensajes de error reportados por Windows cuando intenté interrogar al servicio (a través de sc interrogate service
y sc control service
con una opción enlatada control
permitida), escribí mi propia llamada a GetLastError
para ver si algo interesante estaba pasando:
import Text.Printf
import System.Win32
foreign import stdcall "windows.h GetLastError"
c_GetLastError :: IO DWORD
...
d <- c_GetLastError
appendFile "c:\\log.txt" (Text.Printf.printf "%d\n" (fromEnum d))
Lo que descubrí, para mi disgusto, fue que ERROR_INVALID_HANDLE
y ERROR_ALREADY_EXISTS
estaban siendo arrojados... cuando ejecuta sus operaciones appendFile
secuencial. Phooey, y aquí yo había pensado que estaba en algo.
Lo que esto me dijo, sin embargo, es que StartServiceCtrlDispatcher
, RegisterServiceCtrlHandler
, y SetServiceStatus
no están estableciendo un código de error; de hecho, obtengo ERROR_SUCCESS
exactamente como esperaba.
Análisis
Es alentador que el Administrador de Tareas de Windows y los Registros del Sistema registren el servicio como RUNNING
. Por lo tanto, suponiendo que esa parte de la ecuación realmente está funcionando, debemos volver a por qué nuestro controlador de servicio no está siendo golpeado correctamente.
Inspeccionando estas líneas:
fpHandler <- handlerToFunPtr svcHandler
h <- c_RegisterServiceCtrlHandler (head args) fpHandler
_ <- setServiceStatus h running
Intenté inyectar nullFunPtr
en como mi fpHandler
. Alentadoramente, esto causó que el servicio se colgara en el estado START_PENDING
. Bueno: eso significa que los contenidos de fpHandler
están siendo realmente manejados cuando registramos el servicio.
Entonces, probé esto: {[30]]}
t <- newTString "Foo"
h <- c_RegisterServiceCtrlHandler t fpHandler
Y esto, desafortunadamente, tomó. Sin embargo, eso se espera :
Si el servicio está instalado con el servicio
SERVICE_WIN32_OWN_PROCESS
tipo, este miembro es ignorado, pero no puede ser NULO. Este miembro puede ser una cadena vacía ("").
De acuerdo con nuestro enganchado GetLastError
y los rendimientos de RegisterServiceCtrlHandler
y SetServiceStatus
(un SERVICE_STATUS_HANDLE
y true
válido, respectivamente), todo está bien según el sistema. Eso no puede ser correcto, y es completamente opaco en cuanto a por qué esto no simplemente funciona.
Solución actual
Debido a que no está claro si su declaración en RegisterServiceCtrlHandler
está funcionando de manera efectiva, recomiendo interrogar esta rama de su código en un depurador mientras su servicio se está ejecutando y, lo que es más importante, ponerse en contacto con Microsoft sobre este problema. Por todas las cuentas, parece que ha satisfecho todas las dependencias funcionales correctamente, el sistema devuelve todo lo que debería para una ejecución exitosa, y sin embargo, su programa todavía está entrando en un estado indefinido sin un remedio claro a la vista. Es un bicho.
Una solución útil mientras tanto es usar Haskell FFI para definir su arquitectura de servicio en otro lenguaje (por ejemplo, C++) y conectarse a su código mediante (a) exponer su código Haskell a su capa de servicio o (b) exponer su código de servicio a Haskell. En ambos casos, aquí hay una referencia inicial para usar para crear su servicio.
Desearía haber podido hacer más aquí (honestamente, lo intenté legítimamente), pero incluso esto debería ayudarte significativamente a hacer que esto funcione.
La mejor de las suertes por ti. Parece que tiene un número bastante grande de personas interesadas en sus resultados.
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2012-04-17 07:16:13
Pude resolver este problema y publiqué una biblioteca en hackage, Win32-services, para escribir aplicaciones de servicio de Windows en Haskell.
La solución era usar ciertas combinaciones de llamadas Win32 juntas, evitando otras combinaciones.
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2013-07-11 18:04:51
¿No sería más fácil escribir la parte que interactúa con el servicio en C, y hacer que llame a un DLL escrito en Haskell?
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2012-04-13 21:22:47