{-# LANGUAGE OverloadedStrings #-}

module System.Win32.Services
    ( HandlerFunction
    , ServiceMainFunction
    , ServiceAccept (..)
    , ServiceControl (..)
    , ServiceState (..)
    , ServiceStatus (..)
    , ServiceType (..)
    , queryServiceStatus
    , setServiceStatus
    , startServiceCtrlDispatcher
    ) where

import Control.Exception
import Control.Monad.Fix

import Import
import System.Win32.Services.Raw
import System.Win32.Services.Accept
import System.Win32.Services.Control
import qualified System.Win32.Services.Control as SC
import System.Win32.Services.State
import System.Win32.Services.Status
import System.Win32.Services.TableEntry
import System.Win32.Services.Type

-- | A handler function is registered with the service dispatcher thread
--   from a 'ServiceMainFunction'. The first argument is a 'HANDLE' returned
--   from calling 'registerServiceCtrlHandler'. The second argument represents
--   the command this service has been directed to perform.
type HandlerFunction = HANDLE -> ServiceControl -> IO Bool

-- | The service dispatcher thread will call each function of this type that
--   you provide. The first argument will be the name of the service. Any
--   additional command-line parameters will appear in the second argument.
--
--   Each of these functions should call 'registerServiceCtrlHandler' to
--   register a function to handle incoming commands. It should then set
--   the service's status to 'StartPending', and specify that no controls
--   will be accepted. At this point the function may perform any other
--   initialization steps before setting the service's status to
--   'Running'. All of this should take no more than 100ms.
type ServiceMainFunction = String -> [String] -> HANDLE -> IO ()

-- |Retrieves the current status of the specified service.
queryServiceStatus :: HANDLE
    -- ^ MSDN documentation: A handle to the service. This handle is returned
    -- by the OpenService or the CreateService function, and it must have the
    -- SERVICE_QUERY_STATUS access right. For more information, see Service
    -- Security and Access Rights.
    -> IO ServiceStatus
    -- ^ This function will throw an 'Win32Exception' when the internal
    -- Win32 call returnes an error condition. MSDN lists the following
    -- exceptions, but others might be thrown as well:
    --
    --   [@'AccessDenied'@] The handle does not have the
    --   SERVICE_QUERY_STATUS access right.
    --
    --   [@'InvalidHandle'@] The handle is invalid.
queryServiceStatus h = alloca $ \pStatus -> do
    failIfFalse_ "QueryServiceStatus" $ c_QueryServiceStatus h pStatus
    peek pStatus

-- | Register an handler function to be called whenever the operating system
-- receives service control messages.
registerServiceCtrlHandlerEx :: String
    -- ^ The name of the service. According to MSDN documentation this
    -- argument is unused in 'Win32OwnProcess' type services, which is the
    -- only type supported by this binding. Even so, it is recommended
    -- that the name of the service be used.
    --
    -- MSDN description: The name of the service run by the calling thread.
    -- This is the service name that the service control program specified in
    -- the CreateService function when creating the service.
    -> HandlerFunction
    -- ^ A Handler function to be called in response to service control
    -- messages. Behind the scenes this is translated into a "HandlerEx" type
    -- handler.
    -> IO (HANDLE, FunPtr HANDLER_FUNCTION_EX)
    -- ^ This function will throw an 'Win32Exception' when the internal
    -- Win32 call returnes an error condition. MSDN lists the following
    -- exceptions, but others might be thrown as well:
    --
    --   [@'ServiceNotInExe'@] The service entry was specified incorrectly
    --   when the process called 'startServiceCtrlDispatcher'.
registerServiceCtrlHandlerEx str handler =
    withTString str $ \lptstr ->
    -- use 'ret' instead of (h', _) to avoid divergence.
    mfix $ \ret -> do
    fpHandler <- handlerToFunPtr $ toHandlerEx (fst ret) handler
    h <- failIfNull "RegisterServiceCtrlHandlerEx"
        $ c_RegisterServiceCtrlHandlerEx lptstr fpHandler nullPtr
    return (h, fpHandler)

-- |Updates the service control manager's status information for the calling
-- service.
setServiceStatus :: HANDLE
    -- ^ MSDN documentation: A handle to the status information structure for
    -- the current service. This handle is returned by the
    -- RegisterServiceCtrlHandlerEx function.
    -> ServiceStatus
    -- ^ MSDN documentation: A pointer to the SERVICE_STATUS structure the
    -- contains the latest status information for the calling service.
    -> IO ()
    -- ^ This function will throw an 'Win32Exception' when the internal Win32
    -- call returnes an error condition. MSDN lists the following exceptions,
    -- but others might be thrown as well:
    --
    --   [@'InvalidData'@] The specified service status structure is invalid.
    --
    --   [@'InvalidHandle'@] The specified handle is invalid.
setServiceStatus h status =
    with status $ \pStatus ->
    failIfFalse_ "SetServiceStatus"
    $ c_SetServiceStatus h pStatus

-- |Register a callback function to initialize the service, which will be
-- called by the operating system immediately. startServiceCtrlDispatcher
-- will block until the provided callback function returns.
--
-- MSDN documentation: Connects the main thread of a service process to the
-- service control manager, which causes the thread to be the service control
-- dispatcher thread for the calling process.
startServiceCtrlDispatcher :: String
    -- ^ The name of the service. According to MSDN documentation this
    -- argument is unused in 'Win32OwnProcess' type services, which is the
    -- only type supported by this binding. Even so, it is recommended
    -- that the name of the service be used.
    --
    -- MSDN description: The name of the service run by the calling thread.
    -- This is the service name that the service control program specified in
    -- the CreateService function when creating the service.
    -> DWORD
    -- ^
    -- [@waitHint@] The estimated time required for a pending start, stop,
    -- pause, or continue operation, in milliseconds.
    -> HandlerFunction
    -> ServiceMainFunction
    -- ^ This is a callback function that will be called by the operating
    -- system whenever the service is started. It should perform service
    -- initialization including the registration of a handler function.
    -- MSDN documentation gives conflicting advice as to whether this function
    -- should return before the service has entered the stopped state.
    -- In the official example the service main function blocks until the
    -- service is ready to stop.
    -> IO ()
    -- ^ This function will throw an 'Win32Exception' when the internal Win32
    -- call returnes an error condition. MSDN lists the following exceptions,
    -- but others might be thrown as well:
    --
    --   ['FailedServiceControllerConnect']
    --   This error is returned if the program is being run as a console
    --   application rather than as a service. If the program will be run as
    --   a console application for debugging purposes, structure it such that
    --   service-specific code is not called when this error is returned.
    --
    --   ['InvalidData'] The specified dispatch table contains entries
    --   that are not in the proper format.
    --
    --   ['ServiceAlreadyRunning'] The process has already called
    --   @startServiceCtrlDispatcher@. Each process can call
    --   @startServiceCtrlDispatcher@ only one time.
startServiceCtrlDispatcher name wh handler main =
    withTString name $ \lptstr ->
    bracket (toSMF main handler wh >>= smfToFunPtr) freeHaskellFunPtr $ \fpMain ->
    withArray [ServiceTableEntry lptstr fpMain, nullSTE] $ \pSTE ->
    failIfFalse_ "StartServiceCtrlDispatcher"
    $ c_StartServiceCtrlDispatcher pSTE

toSMF :: ServiceMainFunction -> HandlerFunction -> DWORD -> IO SERVICE_MAIN_FUNCTION
toSMF f handler wh = return $ \argc argv -> do
    args <- convertToListOfStrings argc argv
    -- MSDN guarantees args will have at least 1 member.
    let name = head args
    (h, fpHandler) <- registerServiceCtrlHandlerEx name handler
    setServiceStatus h $ ServiceStatus Win32OwnProcess StartPending [] Success 0 0 wh
    f name (tail args) h
    freeHaskellFunPtr fpHandler
  where convertToListOfStrings length' pLPTSTR = do
          lptstrx <- peekArray (fromIntegral length') pLPTSTR
          mapM peekTString lptstrx


-- This was originally written with older style handle functions in mind.
-- I'm now using HandlerEx style functions, and need to add support for
-- the extra parameters here.
toHandlerEx :: HANDLE -> HandlerFunction -> HANDLER_FUNCTION_EX
toHandlerEx h f = \dwControl _ _ _ ->
    case SC.marshIn dwControl of
      Right control -> do
          handled <- f h control
          case control of
            Interrogate -> return $ toDWORD Success
            -- If we ever support extended control codes this will have to
            -- change. see "Dev Center - Desktop > Docs > Desktop app
            -- development documentation > System Services > Services >
            -- Service Reference > Service Functions > HandlerEx".
            _ -> return $ if handled then toDWORD Success
                                     else toDWORD CallNotImplemented
      Left _ -> return $ toDWORD CallNotImplemented