#
# Copyright (c) 2003-2007, Ashok P. Nadkarni
# All rights reserved.
#
# See the file LICENSE for license

namespace eval twapi {
    # When the process hosts Windows services, service_state
    # is used to keep state of each service. The variable
    # is indexed by NAME,FIELD where NAME is the name
    # of the service and FIELD is one of "state", "script",
    # "checkpoint", "waithint", "exitcode", "servicecode",
    # "seq", "seqack"
    variable service_state

    # Map service state names to integers
    variable service_state_values
    array set service_state_values {
        stopped       1
        start_pending 2
        stop_pending  3
        running       4
        continue_pending 5
        pause_pending 6
        paused        7
    }
}


# Return 1/0 depending on whether the given service exists
# $name may be either the internal or display name
proc twapi::service_exists {name args} {
    array set opts [parseargs args {system.arg database.arg} -nulldefault]
    # 0x00020000 -> STANDARD_RIGHTS_READ
    set scm [OpenSCManager $opts(system) $opts(database) 0x00020000]

    trap {
        GetServiceKeyName $scm $name
        set exists 1
    } onerror {TWAPI_WIN32 1060} {
        # "no such service" error for internal name.
        # Try display name
        trap {
            GetServiceDisplayName $scm $name
            set exists 1
        } onerror {TWAPI_WIN32 1060} {
            set exists 0
        }
    } finally {
        CloseServiceHandle $scm
    }

    return $exists
}


# Create a service of the specified name
proc twapi::create_service {name command args} {
    array set opts [parseargs args {
        displayname.arg
        {servicetype.arg     win32_own_process {win32_own_process win32_share_process file_system_driver kernel_driver}}
        {interactive.bool    0}
        {starttype.arg       auto_start {auto_start boot_start demand_start disabled system_start}}
        {errorcontrol.arg    normal {ignore normal severe critical}}
        loadordergroup.arg
        dependencies.arg
        account.arg
        password.arg
        system.arg
        database.arg
    } -nulldefault]


    if {[string length $opts(displayname)] == 0} {
        set opts(displayname) $name
    }

    if {[string length $command] == 0} {
        error "The executable path must not be null when creating a service"
    }
    set opts(command) $command

    switch -exact -- $opts(servicetype) {
        file_system_driver -
        kernel_driver {
            if {$opts(interactive)} {
                error "Option -interactive cannot be specified when -servicetype is $opts(servicetype)."
            }
        }
        default {
            if {$opts(interactive) && [string length $opts(account)]} {
                error "Option -interactive cannot be specified with the -account option as interactive services must run under the LocalSystem account."
            }
            if {[string equal $opts(starttype) "boot_start"]
                || [string equal $opts(starttype) "system_start"]} {
                error "Option -starttype value must be one of auto_start, demand_start or disabled when -servicetype is '$opts(servicetype)'."
            }
        }
    }

    # Map keywords to integer values
    set opts(servicetype)  [_map_servicetype_sym $opts(servicetype)]
    set opts(starttype)    [_map_starttype_sym $opts(starttype)]
    set opts(errorcontrol) [_map_errorcontrol_sym $opts(errorcontrol)]

    # If interactive, add the flag to the service type
    if {$opts(interactive)} {
        setbits opts(servicetype) 0x100; # SERVICE_INTERACTIVE_PROCESS
    }

    # Ignore password if username not specified
    if {[string length $opts(account)] == 0} {
        set opts(password) ""
    } else {
        # If domain/system not specified, tack on ".\" for local system
        if {[string first \\ $opts(account)] < 0} {
            set opts(account) ".\\$opts(account)"
        }
    }

    # 2 -> SC_MANAGER_CREATE_SERVICE
    set scm [OpenSCManager $opts(system) $opts(database) 2]
    trap {
        # 0x000F01FF -> SERVICE_ALL_ACCESS
        set svch [CreateService \
                      $scm \
                      $name \
                      $opts(displayname) \
                      0x000F01FF \
                      $opts(servicetype) \
                      $opts(starttype) \
                      $opts(errorcontrol) \
                      $opts(command) \
                      $opts(loadordergroup) \
                      "" \
                      $opts(dependencies) \
                      $opts(account) \
                      $opts(password)]

        CloseServiceHandle $svch

    } finally {
        CloseServiceHandle $scm
    }

    return
}


# Delete the given service
proc twapi::delete_service {name args} {

    array set opts [parseargs args {system.arg database.arg} -nulldefault]

    # 0x00010000 -> DELETE access
    set opts(scm_priv) 0x00010000 
    set opts(svc_priv) 0x00010000 
    set opts(proc)     twapi::DeleteService

    _service_fn_wrapper $name opts

    return
}


# Get the internal name of a service
proc twapi::get_service_internal_name {name args} {
    array set opts [parseargs args {system.arg database.arg} -nulldefault]
    # 0x00020000 -> STANDARD_RIGHTS_READ
    set scm [OpenSCManager $opts(system) $opts(database) 0x00020000]

    trap {
        if {[catch {GetServiceKeyName $scm $name} internal_name]} {
            # Maybe this is an internal name itself
            GetServiceDisplayName $scm $name; # Will throw an error if not internal name
            set internal_name $name
        }
    } finally {
        CloseServiceHandle $scm
    }

    return $internal_name
}

proc twapi::get_service_display_name {name args} {
    array set opts [parseargs args {system.arg database.arg} -nulldefault]
    # 0x00020000 -> STANDARD_RIGHTS_READ
    set scm [OpenSCManager $opts(system) $opts(database) 0x00020000]

    trap {
        if {[catch {GetServiceDisplayName $scm $name} display_name]} {
            # Maybe this is an display name itself
            GetServiceKeyName $scm $name; # Will throw an error if not display name
            set display_name $name
        }
    } finally {
        CloseServiceHandle $scm
    }

    return $display_name
}

proc twapi::start_service {name args} {
    array set opts [parseargs args {
        system.arg
        database.arg
        params.arg
        wait.int
    } -nulldefault]
    set opts(svc_priv) 0x10;    # SERVICE_START
    set opts(proc)     twapi::StartService
    set opts(args)     [list $opts(params)]
    unset opts(params)

    trap {
        _service_fn_wrapper $name opts
    } onerror {TWAPI_WIN32 1056} {
        # Error 1056 means service already running
    }

    return [wait {twapi::get_service_state $name -system $opts(system) -database $opts(database)} running $opts(wait)]
}

# TBD - document and test
proc twapi::notify_service {name code args} {
    array set opts [parseargs args {
        system.arg
        database.arg
        ignorecodes.arg
    } -nulldefault]

    if {[string is integer -strict $code] && $code >= 128 && $code <= 255} {
        # 0x100 -> SERVICE_USER_DEFINED_CONTROL 
        set access 0x100
    } elseif {$code eq "paramchange"} {
        # 0x40 -> SERVICE_PAUSE_CONTINUE
        set access 0x40
        set code 6;             # PARAMCHANGE
    } else {
        badargs! "Invalid service notification code \"$code\"."
    }

    set scm [OpenSCManager $opts(system) $opts(database) 0x00020000]
    trap {
        set svch [OpenService $scm $name $access]
    } finally {
        CloseServiceHandle $scm
    }
    
    trap {
        ControlService $svch $code
    } onerror {TWAPI_WIN32} {
        if {[lsearch -exact -integer $opts(ignorecodes) [lindex $::errorCode 1]] < 0} {
            # Not one of the error codes we can ignore. 
            rethrow
        }
    } finally {
        CloseServiceHandle $svch
    }
    return
}

proc twapi::control_service {name code access finalstate args} {
    array set opts [parseargs args {
        system.arg
        database.arg
        ignorecodes.arg
        wait.int
    } -nulldefault]
    # 0x00020000 -> STANDARD_RIGHTS_READ
    set scm [OpenSCManager $opts(system) $opts(database) 0x00020000]
    trap {
        set svch [OpenService $scm $name $access]
    } finally {
        CloseServiceHandle $scm
    }

    trap {
        ControlService $svch $code
    } onerror {TWAPI_WIN32} {
        if {[lsearch -exact -integer $opts(ignorecodes) [lindex $::errorCode 1]] < 0} {
            # Not one of the error codes we can ignore. 
            rethrow
        }
    } finally {
        CloseServiceHandle $svch
    }

    if {[string length $finalstate]} {
        # Wait until service is in specified state
        return [wait {twapi::get_service_state $name -system $opts(system) -database $opts(database)} $finalstate $opts(wait)]
    } else {
        return 0
    }
}

proc twapi::stop_service {name args} {
    # 1 -> SERVICE_CONTROL_STOP
    # 0x20 -> SERVICE_STOP
    control_service $name 1 0x20 stopped -ignorecodes 1062 {*}$args
}

proc twapi::pause_service {name args} {
    # 2 -> SERVICE_CONTROL_PAUSE
    # 0x40 -> SERVICE_PAUSE_CONTINUE
    control_service $name 2 0x40 paused {*}$args
}

proc twapi::continue_service {name args} {
    # 3 -> SERVICE_CONTROL_CONTINUE
    # 0x40 -> SERVICE_PAUSE_CONTINUE
    control_service $name 3 0x40 running {*}$args
}

proc twapi::interrogate_service {name args} {
    # 4 -> SERVICE_CONTROL_INTERROGATE
    # 0x80 -> SERVICE_INTERROGATE
    control_service $name 4 0x80 "" {*}$args
    return
}


# Retrieve status information for a service
proc twapi::get_service_status {name args} {
    array set opts [parseargs args {system.arg database.arg} -nulldefault]
    # 0x00020000 -> STANDARD_RIGHTS_READ
    set scm [OpenSCManager $opts(system) $opts(database) 0x00020000]
    trap {
        # 4 -> SERVICE_QUERY_STATUS
        set svch [OpenService $scm $name 4]
    } finally {
        # Do not need SCM anymore
        CloseServiceHandle $scm
    }

    trap {
        return [QueryServiceStatusEx $svch 0]
    } finally {
        CloseServiceHandle $svch
    }
}


# Get the state of the service
proc twapi::get_service_state {name args} {
    return [kl_get [get_service_status $name {*}$args] state]
}


# Get the current configuration for a service
proc twapi::get_service_configuration {name args} {
    array set opts [parseargs args {
        system.arg
        database.arg
        all
        servicetype
        interactive
        errorcontrol
        starttype
        command
        loadordergroup
        account
        displayname
        dependencies
        description
        scm_handle.arg
        tagid
        failureactions
        delayedstart
    } -nulldefault -hyphenated]

    if {$opts(-scm_handle) eq ""} {
        # Use 0x00020000 -> STANDARD_RIGHTS_READ for SCM 
        set scmh [OpenSCManager $opts(-system) $opts(-database) 0x00020000]
        trap {
            set svch [OpenService $scmh $name 1]; # 1 -> SERVICE_QUERY_CONFIG
        } finally {
            CloseServiceHandle $scmh
        }
    } else {
        set svch [OpenService $opts(-scm_handle) $name 1]; # 1 -> SERVICE_QUERY_CONFIG
    }

    trap {
        set result [QueryServiceConfig $svch]
        if {$opts(-all) || $opts(-description)} {
            dict set result -description {}
            # For backwards compatibility, ignore errors if description
            # cannot be obtained
            catch {
                dict set result -description [QueryServiceConfig2 $svch 1]; # 1 -> SERVICE_CONFIG_DESCRIPTION
            }
        }

        if {$opts(-all) || $opts(-failureactions)} {
            # 2 -> SERVICE_CONFIG_FAILURE_ACTIONS
            lassign  [QueryServiceConfig2 $svch 2] resetperiod rebootmsg command failure_actions
            set actions {}
            foreach action $failure_actions {
                lappend actions [list [dict* {0 none 1 restart 2 reboot 3 run} [lindex $action 0]] [lindex $action 1]]
            }
            dict set result -failureactions [list -resetperiod $resetperiod -rebootmsg $rebootmsg -command $command -actions $actions]
        }
        if {$opts(-all) || $opts(-delayedstart)} {
            if {[min_os_version 6]} {
                # 3 -> SERVICE_CONFIG_DELAYED_AUTO_START_INFO
                dict set result -delayedstart [QueryServiceConfig2 $svch 3]
            } else {
                dict set result -delayedstart 0
            }
        }
    } finally {
        CloseServiceHandle $svch
    }

    if {! $opts(-all)} {
        set result [dict filter $result script {k val} {set opts($k)}]
    }

    if {[dict exists $result -errorcontrol]} {
        dict set result -errorcontrol [_map_errorcontrol_code [dict get $result -errorcontrol]]
    }

    if {[dict exists $result -starttype]} {
        dict set result -starttype [_map_starttype_code [dict get $result -starttype]]
    }

    return $result
}

# Sets a service configuration
proc twapi::set_service_configuration {name args} {
    # Get the current values - we will need these for validation
    # with the new values
    array set current [get_service_configuration $name -all]
    set current(-password) ""; # This is not returned by get_service_configuration

    # Now parse arguments, filling in defaults
    array set opts [parseargs args {
        displayname.arg
        servicetype.arg
        interactive.bool
        starttype.arg
        errorcontrol.arg
        command.arg
        loadordergroup.arg
        dependencies.arg
        account.arg
        password.arg
        {system.arg ""}
        {database.arg ""}
    }]

    if {[info exists opts(account)] && ! [info exists opts(password)]} {
        error "Option -password must also be specified when -account is specified."
    }

    # Merge current configuration with specified options
    foreach opt {
        displayname
        servicetype
        interactive
        starttype
        errorcontrol
        command
        loadordergroup
        dependencies
        account
        password
    } {
        if {[info exists opts($opt)]} {
            set winparams($opt) $opts($opt)
        } else {
            set winparams($opt) $current(-$opt)
        }
    }

    # Validate the new configuration
    switch -exact -- $winparams(servicetype) {
        file_system_driver -
        kernel_driver {
            if {$winparams(interactive)} {
                error "Option -interactive cannot be specified when -servicetype is $winparams(servicetype)."
            }
        }
        default {
            if {$winparams(interactive) &&
                [string length $winparams(account)] &&
                [string compare -nocase $winparams(account) "LocalSystem"]
            } {
                error "Option -interactive cannot be specified with the -account option as interactive services must run under the LocalSystem account."
            }
            if {[string equal $winparams(starttype) "boot_start"]
                || [string equal $winparams(starttype) "system_start"]} {
                error "Option -starttype value must be one of auto_start, demand_start or disabled when -servicetype is '$winparams(servicetype)'."
            }
        }
    }

    # Map keywords to integer values
    set winparams(servicetype)  [_map_servicetype_sym $winparams(servicetype)]
    set winparams(starttype)    [_map_starttype_sym $winparams(starttype)]
    set winparams(errorcontrol) [_map_errorcontrol_sym $winparams(errorcontrol)]

    # Merge the interactive setting
    # 0x100 -> SERVICE_INTERACTIVE_PROCESS
    if {$winparams(interactive)} {
        setbits winparams(servicetype) 0x100
    } else {
        resetbits winparams(servicetype) 0x100 
    }

    # If domain/system not specified, tack on ".\" for local system
    if {[string length $winparams(account)]} {
        if {[string first \\ $winparams(account)] < 0} {
            set winparams(account) ".\\$winparams(account)"
        }
    }

    # Now replace any options that were not specified with "no change"
    # tokens.
    foreach opt {servicetype starttype errorcontrol} {
        if {![info exists opts($opt)]} {
            set winparams($opt) 0xffffffff;  # SERVICE_NO_CHANGE
        }
    }
    # -servicetype and -interactive go in same field
    if {![info exists opts(servicetype)] && ![info exists opts(interactive)]} {
        set winparams(servicetype) 0xffffffff; # SERVICE_NO_CHANGE
    }

    foreach opt {command loadordergroup dependencies account password displayname} {
        if {![info exists opts($opt)]} {
            set winparams($opt) $twapi::nullptr
        }
    }

    set opts(scm_priv) 0x00020000; # 0x00020000 -> STANDARD_RIGHTS_READ
    set opts(svc_priv) 2;    # 2 -> SERVICE_CHANGE_CONFIG

    set opts(proc)     twapi::ChangeServiceConfig
    set opts(args) \
        [list \
             $winparams(servicetype) \
             $winparams(starttype) \
             $winparams(errorcontrol) \
             $winparams(command) \
             $winparams(loadordergroup) \
             "" \
             $winparams(dependencies) \
             $winparams(account) \
             $winparams(password) \
             $winparams(displayname)]

    _service_fn_wrapper $name opts

    return
}

proc twapi::set_service_delayed_start {name delay args} {
    array set opts [parseargs args {
        {system.arg ""}
        {database.arg ""}
    } -maxleftover 0]

    set opts(scm_priv) 0x00020000; # 0x00020000 -> STANDARD_RIGHTS_READ
    set opts(svc_priv) 2;    # 2 -> SERVICE_CHANGE_CONFIG

    set opts(proc) twapi::ChangeServiceConfig2
    set opts(args) [list 3 $delay]

    _service_fn_wrapper $name opts
    return
}

proc twapi::set_service_description {name description args} {
    array set opts [parseargs args {
        {system.arg ""}
        {database.arg ""}
    } -maxleftover 0]

    set opts(scm_priv) 0x00020000; # 0x00020000 -> STANDARD_RIGHTS_READ
    set opts(svc_priv) 2;    # 2 -> SERVICE_CHANGE_CONFIG

    set opts(proc) twapi::ChangeServiceConfig2
    set opts(args) [list 1 $description]

    _service_fn_wrapper $name opts
    return
}

proc twapi::set_service_failure_actions {name args} {
    array set opts [parseargs args {
        {system.arg ""}
        {database.arg ""}
        resetperiod.arg
        {rebootmsg.arg __null__}
        {command.arg __null__}
        actions.arg
    } -maxleftover 0]

    set opts(scm_priv) 0x00020000; # 0x00020000 -> STANDARD_RIGHTS_READ
    set opts(svc_priv) 2;    # 2 -> SERVICE_CHANGE_CONFIG

    # If option actions is not specified, actions for the service
    # are left unchanged.
    if {[info exists opts(actions)]} {
        set actions {}
        foreach action $opts(actions) {
            if {[llength $action] != 2} {
                error "Invalid format for failure action"
            }
            set action_code [dict* {none 0 restart 1 reboot 2 run 3} [lindex $action 0]]
            if {$action_code == 1} {
                # Also need SERVICE_START access right for restart action
                set opts(svc_priv) [expr {$opts(svc_priv) | 0x10}]
            }
            lappend actions [list $action_code [lindex $action 1]]
        }
        if {![info exists opts(resetperiod)] || $opts(resetperiod) eq "infinite"} {
            set opts(resetperiod) 0xffffffff
        }
        set fail_params [list $opts(resetperiod) $opts(rebootmsg) $opts(command) $actions]
    } else {
        if {[info exists opts(resetperiod)]} {
            badargs! "Option -resetperiod can only be used if the -actions option is also specified."
        }
        set fail_params [list 0 $opts(rebootmsg) $opts(command)]
    }

    set opts(proc) twapi::ChangeServiceConfig2
    set opts(args) [list 2 $fail_params]; # 2 -> SERVICE_CONFIG_FAILURE_ACTIONS
    _service_fn_wrapper $name opts
    return
}

# Get status for the specified service types
proc twapi::get_multiple_service_status {args} {
    set service_types [list \
                           kernel_driver \
                           file_system_driver \
                           adapter \
                           recognizer_driver \
                           user_own_process \
                           user_share_process \
                           win32_own_process \
                           win32_share_process]
    set switches [concat $service_types \
                      [list active inactive] \
                      [list system.arg database.arg]]
    array set opts [parseargs args $switches -nulldefault]

    set servicetype 0
    foreach type $service_types {
        if {$opts($type)} {
            set servicetype [expr { $servicetype | [_map_servicetype_sym $type]}]
        }
    }
    if {$servicetype == 0} {
        # No type specified, return all
        set servicetype 0x3f
    }

    set servicestate 0
    if {$opts(active)} {
        set servicestate 1;     # 1 -> SERVICE_ACTIVE
    }
    if {$opts(inactive)} {
        set servicestate [expr {$servicestate | 2}]; # 2 -> SERVICE_INACTIVE
    }
    if {$servicestate == 0} {
        # No state specified, include all
        set servicestate 3
    }

    # 4 -> SC_MANAGER_ENUMERATE_SERVICE
    set scm [OpenSCManager $opts(system) $opts(database) 4]
    trap {
        set fields {
            servicetype state controls_accepted  exitcode service_code
            checkpoint wait_hint pid serviceflags name displayname interactive 
        }
        return [list $fields [EnumServicesStatusEx $scm 0 $servicetype $servicestate __null__]]
    } finally {
        CloseServiceHandle $scm
    }
}


# Get status for the dependents of the specified service
proc twapi::get_dependent_service_status {name args} {
    array set opts [parseargs args \
                        [list active inactive system.arg database.arg] \
                        -nulldefault]

    set servicestate 0
    if {$opts(active)} {
        set servicestate 1;     # 1 -> SERVICE_ACTIVE
    }
    if {$opts(inactive)} {
        set servicestate [expr {$servicestate | 2}]; # SERVICE_INACTIVE
    }
    if {$servicestate == 0} {
        # No state specified, include all
        set servicestate 3
    }

    set opts(svc_priv) 8; # SERVICE_ENUMERATE_DEPENDENTS
    set opts(proc)     twapi::EnumDependentServices
    set opts(args)     [list $servicestate]

    set fields {
        servicetype state controls_accepted  exitcode service_code
        checkpoint wait_hint name displayname interactive 
    }

    return [list $fields [_service_fn_wrapper $name opts]]


}


################################################################
# Commands for running as a service

proc twapi::run_as_service {services args} {
    variable service_state

    if {[llength $services] == 0} {
        win32_error 87 "No services specified"
    }

    array set opts [parseargs args {
        interactive.bool
        {controls.arg {stop shutdown}}
    } -nulldefault -maxleftover 0]

    # Currently service controls are per process, not per service and
    # are fixed for the duration of the process.
    # TBD - C code actually allows for per service controls. Expose?
    set service_state(controls) [_parse_service_accept_controls $opts(controls)]
    if {![min_os_version 5 1]} {
        # Not accepted on Win2k
        if {$service_state(controls) & 0x80} {
            error "Service control type 'sessionchange' is not valid on this platform"
        }
    }

    if {[llength $services] == 1} {
        set type 0x10;          # WIN32_OWN_PROCESS
    } else {
        set type 0x20;          # WIN32_SHARE_PROCESS
    }
    if {$opts(interactive)} {
        setbits type 0x100;     # INTERACTIVE_PROCESS
    }

    set service_defs [list ]
    foreach service $services {
        lassign $service name script
        set name [string tolower $name]
        lappend service_defs [list $name $service_state(controls)]
        set service_state($name,state)       stopped
        set service_state($name,script)      $script
        set service_state($name,checkpoint)  0
        set service_state($name,waithint)    2000; # 2 seconds
        set service_state($name,exitcode)    0
        set service_state($name,servicecode) 0
        set service_state($name,seq)         0
        set service_state($name,seqack)      0
    }

    twapi::Twapi_BecomeAService $type {*}$service_defs

    # Turn off console events by installing our own handler,
    # else tclsh will exit when a user logs off even if it is running
    # as a service
    # COMMENTED OUT because now done in C code itself
    # proc ::twapi::_service_console_handler args { return 1 }
    # set_console_control_handler ::twapi::_service_console_handler

    # Redefine ourselves as we should not be called again
    proc ::twapi::run_as_service args {
        error "Already running as a service"
    }
}


# Callback that handles requests from the service control manager
proc twapi::_service_handler {name service_status_handle control args} {
    # TBD - should we catch the error or let the C code see it ?
    if {[catch {
        _service_handler_unsafe $name $service_status_handle $control $args
    } msg]} {
        # TBD - log error message
        catch {eventlog_log "Error in service handler for service $name. $msg Stack: $::errorInfo" -type error}
    }
}

# Can raise an error
proc twapi::_service_handler_unsafe {name service_status_handle control extra_args} {
    variable service_state

    set name [string tolower $name]

    # The service handler will receive control codes from the service
    # control manager and modify the state of a service accordingly.
    # It also calls the script registered by the application for
    # the service. The caller is expected to complete the state change
    # by calling service_change_state_complete either inside the
    # callback or at some later point.

    set tell_app true;          # Does app need to be notified ?
    set report_status true;     # Whether we should update status
    set need_response true;     # App should report status back

    switch -glob -- "$service_state($name,state),$control" {
        stopped,start {
            set service_state($name,state) start_pending
            set service_state($name,checkpoint) 1
        }
        start_pending,shutdown -
        paused,shutdown        -
        pause_pending,shutdown -
        continue_pending,shutdown -
        running,shutdown -
        start_pending,stop -
        paused,stop        -
        pause_pending,stop -
        continue_pending,stop -
        running,stop {
            set service_state($name,state) stop_pending
            set service_state($name,checkpoint) 1
        }
        running,pause {
            set service_state($name,state) pause_pending
            set service_state($name,checkpoint) 1
        }
        pause_pending,continue -
        paused,continue {
            set service_state($name,state) continue_pending
            set service_state($name,checkpoint) 1
        }
        *,interrogate {
            # No state change, we will simply report status below
            set tell_app false; # No need to bother the application
        }
        *,userdefined -
        *,paramchange -
        *,netbindadd -
        *,netbindremove -
        *,netbindenable -
        *,netbinddisable -
        *,deviceevent -
        *,hardwareprofilechange -
        *,powerevent -
        *,sessionchange {
            # Notifications, should not report status.
            set report_status false
            set need_response false
        }
        default {
            # All other cases are no-ops (e.g. paused,pause) or
            # don't make logical sense (e.g. stop_pending,continue)
            # For now, we simply ignore them but not sure
            # if we should just update service status anyways
            return
        }
    }

    if {$report_status} {
        _report_service_status $name
    }

    set result 0
    if {$tell_app} {
        if {[catch {
            if {$need_response} {
                set seq [incr service_state($name,seq)]
            } else {
                set seq -1
            }
            set result [uplevel #0 [linsert $service_state($name,script) end $control $name $seq {*}$extra_args]]
            # Note that if the above script may call back into us,
            # via update_service_status for example, the service
            # state may be updated at this point
        } msg]} {
            # TBD - report if the script throws errors
        }
    }

    if {$result eq "allow"} {
        set result 0
    } elseif {$result eq "deny"} {
        set result  0x424D5144; # BROADCAST_QUERY_DENY
    }

    return $result
}

# Called by the application to update it's status
# status should be one of "running", "paused" or "stopped"
# seq is 0 or the sequence number of a previous callback to
# the application to which this is the response.
proc twapi::update_service_status {name seq state args} {
    variable service_state

    if {$state ni {running paused stopped}} {
        error "Invalid state token $state"
    }

    if {$seq == -1} {
        # This was a notification. App should not have responded.
        # Just ignore it
        return ignored
    }

    array set opts [parseargs args {
        exitcode.int
        servicecode.int
        waithint.int
    } -maxleftover 0]

    set name [string tolower $name]

    # Depending on the current state of the application,
    # we may or may not be able to change state. For
    # example, if the current state is "running" and
    # the new state is "stopped", that is ok. But the
    # converse is not allowed since we cannot
    # transition from stopped to running unless
    # the SCM has sent us a start signal.

    # If the seq is greater than the last one we sent, bug somewhere
    if {$service_state($name,seq) < $seq} {
        error "Invalid sequence number $seq (too large) for service status update."
    }

    # If we have a request outstanding (to the app) that the app
    # has not yet responded to, then all calls from the app with
    # no seq number (i.e. 0) or calls with an older sequence number
    # are ignored.
    if {($service_state($name,seq) > $service_state($name,seqack)) &&
        ($seq == 0 || $seq < $service_state($name,seq))} {
        # Ignore this request
        return ignored
    }

    set service_state($name,seqack) $seq; # last responded sequence number

    # If state specified as stopped, store the exit codes
    if {$state eq "stopped"} {
        if {[info exists opts(exitcode)]} {
            set service_state($name,exitcode) $opts(exitcode)
        }
        if {[info exists opts(servicecode)]} {
            set service_state($name,servicecode) $opts(servicecode)
        }
    }

    upvar 0 service_state($name,state) current_state

    # If there is no state change, nothing to do
    if {$state eq $current_state} {
        return nochange
    }

    switch -exact -- $state {
        stopped {
            # Application can stop at any time from any other state.
            # No questions asked.
        }
        running {
            if {$current_state eq "stopped" || $current_state eq "paused"} {
                # This should not happen if all the rules are followed by the
                # application code.
                #error "Service $name attempted to transition directly from stopped or paused state to running state without an intermediate pending state"
                return invalidchange
            }
        }
        paused {
            if {$current_state ne "pause_pending" &&
                $current_state ne "continue_pending"} {
                # This should not happen if all the rules are followed by the
                # application code.
                #error "Service $name attempted to transition from $current_state state to paused state"
                return invalidchange
            }
        }
    }

    set current_state $state
    _report_service_status $name

    if {$state eq "stopped"} {
        # If all services have stopped, tell the app
        set all_stopped true
        foreach {entry val} [array get service_state *,state] {
            if {$val ne "stopped"} {
                set all_stopped false
                break
            }
        }
        if {$all_stopped} {
            uplevel #0 [linsert $service_state($name,script) end all_stopped $name 0]
        }
    }

    return changed;             # State changed
}


# Report the status of a service back to the SCM
proc twapi::_report_service_status {name} {
    variable service_state
    upvar 0 service_state($name,state) current_state

    # If the state is a pending state, then make sure we
    # increment the checkpoint value
    if {[string match *pending $current_state]} {
        incr service_state($name,checkpoint)
        set waithint $service_state($name,waithint)
    } else {
        set service_state($name,checkpoint) 0
        set waithint 0
    }

    # Currently service controls are per process, not per service and
    # are fixed for the duration of the process. So we always pass
    # service_state(controls). Applications has to ensure it can handle
    # all control signals in all states (ignoring them as desired)
    if {[catch {
        Twapi_SetServiceStatus $name $::twapi::service_state_values($current_state) $service_state($name,exitcode) $service_state($name,servicecode) $service_state($name,checkpoint) $waithint $service_state(controls)
    } msg]} {
        # TBD - report error - but how ? bgerror?
        catch {twapi::eventlog_log "Error setting service status: $msg"}
    }

    # If we had supplied a wait hint, we are telling the SCM, we will call
    # it back within that period of time, so schedule ourselves.
    if {$waithint} {
        set delay [expr {($waithint*3)/4}]
        after $delay ::twapi::_call_scm_within_waithint $name $current_state $service_state($name,checkpoint)
    }

    return
}


# Queued to regularly update the SCM when we are in any of the pending states
proc ::twapi::_call_scm_within_waithint {name orig_state orig_checkpoint} {
    variable service_state

    # We only call to update staus if the state and checkpoint have
    # not changed since the routine was queued
    if {($service_state($name,state) eq $orig_state) &&
        ($service_state($name,checkpoint) == $orig_checkpoint)} {
        _report_service_status $name
    }
}


################################################################
# Utility procedures

# Map an integer service type code into a list consisting of
# {SERVICETYPESYMBOL BOOLEAN}. If there is not symbolic service type
# for the service, just the integer code is returned. The BOOLEAN
# is 1/0 depending on whether the service type code is interactive
proc twapi::_map_servicetype_code {servicetype} {
    # 0x100 -> SERVICE_INTERACTIVE_PROCESS
    set interactive [expr {($servicetype & 0x100) != 0}]
    set servicetype [expr {$servicetype & (~ 0x100)}]
    set servicetype [kl_get [list \
                                 16 win32_own_process \
                                 32 win32_share_process \
                                 80 user_own_process \
                                 96 user_share_process \
                                 1 kernel_driver \
                                 2 file_system_driver \
                                 4 adapter \
                                 8 recognizer_driver \
                                 ] $servicetype $servicetype]
    return [list $servicetype $interactive]
}

# Map service type sym to int code
proc twapi::_map_servicetype_sym {sym} {
    return [dict get {kernel_driver 1 file_system_driver 2 adapter 4 recognizer_driver 8 win32_own_process 16 win32_share_process 32 user_own_process 80 user_share_process 96} $sym]
}

# Map a start type code into a symbol. Returns the integer code if
# no mapping possible
proc twapi::_map_starttype_code {code} {
    incr code 0;                # Make canonical int
    set type [lindex {boot_start system_start auto_start demand_start disabled} $code]
    if {$type eq ""} {
        return $code
    } else {
        return $type
    }
}

# Map starttype sym to int code
proc twapi::_map_starttype_sym {sym} {
    return [dict get {boot_start 0 system_start 1 auto_start 2 demand_start 3 disabled 4} $sym]
}

# Map a error control code into a symbol. Returns the integer code if
# no mapping possible
proc twapi::_map_errorcontrol_code {code} {
    incr code 0;                # Make canonical int
    set error [lindex {ignore normal severe critical} $code]
    if {$error eq ""} {
        return $code
    } else {
        return $error
    }
}

# Map error control sym to int code
proc twapi::_map_errorcontrol_sym {sym} {
    return [dict get {ignore 0 normal 1 severe 2 critical 3} $sym]
}

# Standard template for calling a service function. v_opts should refer
# to an array with the following elements:
# opts(system) - target system. Must be specified
# opts(database) - target database. Must be specified
# opts(scm_priv) - requested privilege when opening SCM. STANDARD_RIGHTS_READ
#   is used if unspecified. Not used if scm_handle is specified
# opts(scm_handle) - handle to service control manager. Optional
# opts(svc_priv) - requested privilege when opening service. Must be present
# opts(proc) - proc/function to call. The first arg is the service handle
# opts(args) - additional arguments to pass to the function.
#   Empty if unspecified
proc twapi::_service_fn_wrapper {name v_opts} {
    upvar $v_opts opts

    # Use 0x00020000 -> STANDARD_RIGHTS_READ for SCM if not specified
    set scm_priv [expr {[info exists opts(scm_priv)] ? $opts(scm_priv) : 0x00020000}]

    if {[info exists opts(scm_handle)] &&
        $opts(scm_handle) ne ""} {
        set scm $opts(scm_handle)
    } else {
        set scm [OpenSCManager $opts(system) $opts(database) $scm_priv]             }
    trap {
        set svch [OpenService $scm $name $opts(svc_priv)]
    } finally {
        # No need for scm handle anymore. Close it unless it was
        # passed to us
        if {(![info exists opts(scm_handle)]) ||
            ($opts(scm_handle) eq "")} {
            CloseServiceHandle $scm
        }
    }

    set proc_args [expr {[info exists opts(args)] ? $opts(args) : ""}]
    trap {
        set results [eval [list $opts(proc) $svch] $proc_args]
    } finally {
        CloseServiceHandle $svch
    }

    return $results
}

# Called back for reporting background errors. Note this is called
# from the C++ services code, not from scripts.
proc twapi::_service_background_error {winerror msg} {
    twapi::win32_error $winerror $msg
}

# Parse symbols for controls accepted by a service
proc twapi::_parse_service_accept_controls {controls} {
    return [_parse_symbolic_bitmask $controls {
        stop                    0x00000001
        pause_continue          0x00000002
        shutdown                0x00000004
        paramchange             0x00000008
        netbindchange           0x00000010
        hardwareprofilechange   0x00000020
        powerevent              0x00000040
        sessionchange           0x00000080
    }]
}
