Copyright © 2020 Ashok P. Nadkarni. All rights reserved.
Windows services are applications that run in the background and are managed through the Windows service control manager. This chapter describes configuration, control and implementation of services using Tcl.
1. Introduction
The highest of distinctions is service to others.
Most applications that users are familiar with run in the context of the
user’s logged in desktop. However, some programs need to run
in the background, possibly starting when the system is booted and
independent of whether a user is logged in. Examples include
server applications like web and database servers as well as
critical Windows system components like Net Logon
and
Remote Procedure Call
. These applications run as Windows
services and conform to certain interfaces and restrictions.
Programming for Windows services has several aspects:
-
implementation of a program that can run as a service
-
installation and configuration of a service
-
executing control commands, such as stopping or starting services
-
monitoring service status
All these are more or less independent and can be implemented in isolation. We will describe them in the above order so that we can use the service we implement to illustrate the other operations.
All service related functionality requires the twapi_service
package to have been loaded.
% package require twapi_service
→ 4.4.1
% namespace path twapi
Most of the code in this chapter requires the Tcl interpreter running with administrative privileges enabled. |
2. Service concepts
Most of the details around implementation of a Windows service are hidden by the TWAPI package. We only describe what is necessary for Tcl programs to run as Windows services.
2.1. The Service Control Manager
On Windows, services run under the control of the Windows Service Control Manager (SCM) system component. They are managed through well-defined control interfaces that allow them to be configured, initiated and terminated by the SCM.
The SCM also maintains a service configuration database that includes definitions of each service, conditions under which it is to be run, security parameters and other service-specific information. It is also responsible for sending control commands and monitoring status information for all services.
2.2. Service states
The SCM defines certain values that represent the state of a service. In response to control signals from the SCM, a service may transition to a new state which it must report back to the SCM.
When retrieving service information, any of the states may be
returned. However, when implementing a service in Tcl, the
programmer only has to
deal with the states stopped
, running
, and paused
. The other
states are managed internally which
simplifies the state machine the application has to implement.
2.3. Control signals
The primary requirement when running as a service under the control of the SCM is that the program must receive and respond to control signals and update its status. Thanks to the Tcl event loop this is fairly straighforward to do.
Service control signals lists the signals that may be received by a service. A service can specify which signals it supports as part of its start-up configuration.
When the SCM sends a control signal, the service must respond by
reporting its state to the SCM
through update_service_status
. This new
state may or may not be the same as the current state.
2.4. Control notifications
In addition to control signals, the SCM may also send notifications to a service. These are listed in Service control notifications. Unlike for signals, a service does not need to explicitly respond or report its status to the SCM in response to a notification.
We will not be discussing notifications in detail as they are received in a similar manner to service control signals. Refer to TWAPI documentation or Windows Service Reference for details.
2.5. Service Control Programs
A service control program presents an interface that allows users
to start and send control signals to a service and retrieve status
information for display. Windows includes command line service
control programs like sc.exe
as well as GUI versions like the
control panel Services
applet.
We do not show examples of these here, preferring to demonstrate doing the same tasks directly from Tcl instead.
3. Writing a Windows service
In addition to the core functionality that it provides, a service must implement the following:
-
a dispatcher to receive the control signals and notifications from the SCM
-
a state machine, usually implemented as handlers for each of the possible control signals
-
registration of the implemented services with the SCM
-
running the Tcl event loop so that control signals are dispatched
We illustrate all these steps by implementing a sample service.
Our demo service is a simple time server that returns the current system time, similar to the sample program in Programming Server-Side Applications for Windows 2000.
The functionality of the time server is extremely simple. When a client connects, the server returns the current system time and closes the connection. The simplicity means the service does not have to deal with network protocols, security issues such as authentication, or safeguards against malicious input (it does not read any). That allows us to focus on the aspects related to Windows services without getting distracted by topics that are orthogonal to Windows services.
We start off loading the twapi_service
package
package require twapi_service
We will store the configuration and current state of the service
in the array state
. We practice good programming hygiene by
placing our code and definitions inside the timeserver
namespace.
namespace eval timeserver {
variable state
array set state {
name MyTimeServer
server_port 9999
run_state stopped
utc 0
}
}
3.1. Implementing the control dispatcher
The core of a Windows service is the dispatcher for the control signals so we start off writing that. The dispatcher is a callback that is invoked when a control event or notification is received from the SCM. It is called with at least the following three arguments:
-
the first parameter is the control signal or notification.
-
the second parameter is the name of the service for which the control is being sent. Processes that are running multiple services would make use of this to know which service is being targeted. In our case, we ignore the parameter since we have only one service implemented in our process. We do however need to pass it on to
update_service_status
. -
the third parameter is the sequence number of the notification. We do not do anything with it other than passing it on to
update_service_status
.
There may be additional arguments supplied in the case of notifications. These depend on the notification type.
proc timeserver::control_dispatcher {control name seq args} {
variable state
switch -exact -- $control {
start - stop - shutdown - pause - continue {
$control
twapi::update_service_status $name $seq $state(run_state)
}
userdefined {
switch [lindex $args 0] {
128 { set state(utc) 1}
129 { set state(utc) 0}
}
}
all_stopped {
set [namespace current]::done 1
}
}
}
The handler responds to control signals by calling a procedure with the same name as the signal. We will define these later.
Also notice that after calling the appropriate procedure for a signal,
we notify the SCM of the new status by calling
update_service_status
. This is mandatory for control signals.
We pass it the name of the service, the sequence number that was
passed in to us, and the new service state. The first two of these
allow the SCM to match up status updates with the control signals it
sends to services.
In addition to the control signals, control_dispatcher
deals with two
notifications:
-
The
user_defined
notification can be sent from a service control program such assc.exe
or from Tcl itself as we will see later. It can be passed additional numeric arguments which we use the switch the time responses from the server between local time and UTC. -
The
all_stopped
notification is internally generated by thetwapi_service
package when all services in the process have been stopped. It is treated as a signal that the process can terminate. We do this by setting thedone
variable which, as we see later, will cause the Tcl event loop to terminate.
Note that no service status update needs to be called for these notifications.
All other signals and notifications that are not of interest should be gracefully ignored as they are not blocked in the current TWAPI implementation. |
3.2. Implementing the control handlers
Implementation of the control handlers is straightforward. Note we have named the handlers based on the corresponding control signal names for convenience but there is no such requirement to always do so.
We begin with the handler for the start
signal. This opens a new
listening socket and sets the service state to running
.
The accept
callback bound to the server socket simply writes the
current time to the incoming connection and closes it.
proc timeserver::start {} {
variable state
if {[catch {
set state(server_socket) [socket -server [namespace current]::accept \
$state(server_port)]
} msg]} {
::twapi::eventlog_log "Error binding to port $state(server_port): $msg. \
$state(name) service exiting."
exit 1
} else {
::twapi::eventlog_log "$state(name) service started."
set state(run_state) "running"
}
}
proc timeserver::accept {sock addr port} {
variable state
if {$state(run_state) eq "running"} {
puts $sock [clock format [clock seconds] -gmt $state(utc)]
}
close $sock
}
Note the accept
checks if the service is in the running
state
before doing so. This check is required because the service might
have transitioned to a paused
or even stopped
state between the
time the accept
handler was queued and the time is was run. It is
important to keep the asynchronous nature of the event loop in mind
when implementing connection and control signal callbacks in services.
We move on to the handle for the stop
signal.
This simply shuts down the listening socket and
transition the service to a stopped
state. The handler does not
mark the process for exit. This is important in case we add other services
to the same process. In general, a service process should exit
only when it receives an all_stopped
notification.
proc timeserver::stop {} {
variable state
if {$state(run_state) ne "stopped"} {
close $state(server_socket)
set state(run_state) "stopped"
::twapi::eventlog_log "$state(name) service stopped."
}
}
We handle shutdown
in identical fashion to the
stop
control signal.
interp alias {} timeserver::shutdown {} timeserver::stop
The pause
and continue
control signals are complementary. Pausing
the service for our simple application just marks the state as
paused
so the accept
call will not respond to incoming connections.
The continue
handler simply reverses this effect.
proc timeserver::pause {} {
variable state
if {$state(run_state) eq "running"} {
set state(run_state) "paused"
::twapi::eventlog_log "$state(name) service paused."
}
}
proc timeserver::continue {} {
variable state
if {$state(run_state) eq "paused"} {
set state(run_state) "running"
::twapi::eventlog_log "$state(name) service continuing after pause."
}
}
3.3. Registering with the SCM
Once its internal initialization is completed, the process has to
register itself with the SCM in order to become a Windows service
through the run_as_service
command.
if {[catch {
twapi::run_as_service {
{mytimeserver timeserver::control_dispatcher}
} -controls {stop shutdown pause_continue}
} msg]} {
twapi::eventlog_log "Service error: $msg\n$::errorInfo"
exit 1
}
The first argument to the command is a list of service definitions for the services (there can be more than one) implemented by the process. Each service definition consists of the service’s name and the name of the callback procedure to be invoked when the service is sent a control signal or notification. Our example only has one service definition.
The service name is passed in to the callback procedure so, if desired, the same callback can be used for multiple services in a process. |
The -controls
option indicates which control signals are implemented
by the process. The SCM will not send signals that are not included
here, except for the start
signal which is always sent. If unspecified,
accepted signals default to stop
and shutdown
.
In case the registration is unsuccessful, we log an event and exit.
Because a service generally does not have a desktop attached and runs in the background, there is no way to display an error dialog to the user. By convention, errors are sent to the Windows error log, though some services may write to a private log file in addition to, or in lieu of, the event log. |
Once registered, the program must wait for control signals from the SCM.
A service should not actually begin its function, like serving
out time in our example, until it receives a start
signal from the
SCM.
3.4. Running the event loop
In order to receive control signals from the SCM, the program
must be running the Tcl event loop.
In our sample program, we use the vwait
command to do this. On
receiving an all_stopped
signal, our handler control_dispatcher
sets the done
variable which causes the vwait
command to return,
terminating the event loop. The script then ``falls off the end''
causing the process to exit in normal fashion.
vwait ::timeserver::done
3.5. Single Pager - Simple Time Server
Here is the complete program listing:
# timeserver.tcl
package require twapi_service
namespace eval timeserver {
variable state
array set state {
name MyTimeServer
server_port 9999
run_state stopped
utc 0
}
}
proc timeserver::control_dispatcher {control name seq args} {
variable state
switch -exact -- $control {
start - stop - shutdown - pause - continue {
$control
twapi::update_service_status $name $seq $state(run_state)
}
userdefined {
switch [lindex $args 0] {
128 { set state(utc) 1}
129 { set state(utc) 0}
}
}
all_stopped {
set [namespace current]::done 1
}
}
}
proc timeserver::start {} {
variable state
if {[catch {
set state(server_socket) [socket -server [namespace current]::accept \
$state(server_port)]
} msg]} {
::twapi::eventlog_log "Error binding to port $state(server_port): $msg. \
$state(name) service exiting."
exit 1
} else {
::twapi::eventlog_log "$state(name) service started."
set state(run_state) "running"
}
}
proc timeserver::accept {sock addr port} {
variable state
if {$state(run_state) eq "running"} {
puts $sock [clock format [clock seconds] -gmt $state(utc)]
}
close $sock
}
proc timeserver::stop {} {
variable state
if {$state(run_state) ne "stopped"} {
close $state(server_socket)
set state(run_state) "stopped"
::twapi::eventlog_log "$state(name) service stopped."
}
}
interp alias {} timeserver::shutdown {} timeserver::stop
proc timeserver::pause {} {
variable state
if {$state(run_state) eq "running"} {
set state(run_state) "paused"
::twapi::eventlog_log "$state(name) service paused."
}
}
proc timeserver::continue {} {
variable state
if {$state(run_state) eq "paused"} {
set state(run_state) "running"
::twapi::eventlog_log "$state(name) service continuing after pause."
}
}
if {[catch {
twapi::run_as_service {
{mytimeserver timeserver::control_dispatcher}
} -controls {stop shutdown pause_continue}
} msg]} {
twapi::eventlog_log "Service error: $msg\n$::errorInfo"
exit 1
}
vwait ::timeserver::done
We have not actually run the service though. We need to go through a couple of additional steps to run a service:
-
the service has to be installed and configured on the system
-
the SCM has to be told to start the installed service
We do these next.
4. Service installation and configuration
Installing a service is straightforward. Although the sc.exe
Windows
program can be used for the purpose, we will do it
programmatically with the create_service
call.
4.1. Configuration settings
Before installing our service, we discuss the configuration
settings that may be associated with a service and the
corresponding option to use with create_service
when installing
the service.
Note these can be
configured at the time a service is installed and also modified
later with the set_service_configuration
command.
4.1.1. Internal name
The service internal name is the unique name that identifies the service.
Most twapi_service
commands take this as the first argument to
identify which service is the target of the command.
4.1.2. Display name
The service display name is the user visible name for the
service. This is displayed to the user by the Windows
service-related programs like net start
or the services control
panel applet. The corresponding option to twapi_service
commands
is -displayname
.
4.1.3. Service type
The SCM deals with four types of Windows services indicated
through the -servicetype
option of create_service
. The
possible values are shown in Service types.
All these types of services may be installed and configured through Tcl. However, Tcl (or any language other than C/C++) can only implement the user mode services.
4.1.4. Start type
The conditions under which a service is started is configured with
the -starttype
option to create_service
. The possible values
are shown in Service start types.
4.1.5. Service accounts
A service executes under the security context of an account. This account may be a standard user account created for the service or one of the special built-in system accounts in System accounts.
Choosing an account under which to run a service is crucial for two opposing reasons:
-
The account must have sufficient privileges to let the service perform its function.
-
To limit the potential damage arising from a security vulnerability in the service, the account must have no more than the minimum required privileges.
Microsoft recommends following a least-privilege hierarchy, trying accounts in the following order, and selecting the first that allows the service to be fully functional.
-
LocalService
, always the first choice if possible -
NetworkService
, if anonymous network credentials are insufficient -
a local user account, possibly created specifically for the service with the exact privileges required
-
LocalSystem
, puts the entire system at risk but relatively limited network access using the computer credentials -
a domain user account, may be needed if the service needs to access network resources for which the local computer account does not have access
-
local adminstrator account, only if a local user account, even with specific privileges added, will not do
-
domain administrator, should never be needed for a properly written service.
As far as possible, services should be written to run under one of the first three account types.
When installing a service, the -account
option is used to
specify the account under which the service is to run. The
password for the account also needs to be specified with the
-password
option if the account is not one of the built-in
system accounts.
For historical reasons, the service is installed under the
LocalSystem account if the -account option is not specified
which, as stated earlier, is not generally desirable.
|
4.1.6. Dependencies
A service may make use of other services and require them to be running before it can start. These other services are then dependencies of the specified service while it is their dependent.
The SCM ensures that a service’s dependencies are running before it is started. Conversely, it will not stop a service if there are any dependent services running.
Service control programs such as sc.exe will explictly stop
dependent services before stopping the specified service if asked
to do so.
|
Service dependencies can be configured with the -dependencies
option to create_service
or set_service_configuration
.
4.1.7. Load order
Windows loads services in a defined sequence based on load order groups. All services in a group are loaded before those in a group that comes later in the sequence. Note however that any dependencies for a service will be loaded before that service even if they belong to a group that appears later in the order.
The list of load order groups can be obtained from the Windows registry.
% print_list [registry get \
HKEY_LOCAL_MACHINE\\System\\CurrentControlSet\\Control\\ServiceGroupOrder List]
→ System Reserved
EMS
WdfLoadGroup
Boot Bus Extender
System Bus Extender
...Additional lines omitted...
The -loadordergroup
option can be specified with
create_service
and set_service_configuration
to place the
service in a specific group. Not specifying this option will mean
the service is started after services in all the groups. For most
application services, this is the correct behaviour.
4.1.8. Error control
Services can be crucial to a system and failure to start a service
may make a system inoperational. The error control configuration
settings specifies the action the SCM should take if a service
fails to start. The possible actions and the corresponding values
to use with the -errorcontrol
option to create_service
are
shown in Service error controls.
4.1.9. Desktop interaction
In older versions of Windows, services that need to display a user interface could be configured to allow interaction with the desktop. However, this feature was deprecated and is not even available in newer versions of Windows. We therefore do not discuss it further.
The recommended method if user interaction is required is to implement a separate program that runs on the user’s desktop and communicates with the service through a secured mechanism like named pipes with appropriate security descriptors.
4.2. Installing a service
Having looked at the configuration options for a service,
installation of a service is a straightforward task. We simply
need to call create_service
passing the command line the SCM
needs to execute to run the service. In addition we can
specify any configuration options whose default values are not
suitable for the service.
Let us continue with our sample service demonstation by installing it with the following options:
-
We will run it under the
LocalService
account since we do not require any special privileges. -
We will set a display name for the service, just for the heck of it
-
We do not want it automatically started so we set it to demand start.
% set exe_path [file nativename [file attributes [info nameofexecutable] \
-shortname]]
→ D:\tcl\magic\bin\tclsh.exe
% set script_path [file nativename [file attributes [file join [pwd] scripts \
timeserver.tcl] -shortname]]
→ D:\src\tcl-on-windows\book\scripts\timeserver.tcl
% create_service MyTimeServer "$exe_path $script_path" -account "NT \
Authority\\LocalService" -displayname "Time Server" -starttype demand_start
Although the command itself is straightforward, a few points are worth noting in the above example.
-
Windows services are expected to be console applications and Tcl scripts that run as services must be run using the console version of Tcl,
tclsh.exe
, and not the GUI versionwish.exe
. The above code assume you are runningtclsh
and notwish
. In the latter case, you need to replace the executable path with the path totclsh
. -
The command line for the service invokes
tclsh
and passes it the script we wrote earlier. You may of course have to change the script path to match your system. -
To avoid quoting problems in case of paths with spaces in them, we construct a command line that uses the short names of the file paths.
-
We also pass the file names in native format using
\
rather than/
. Although many Windows programs will accept either, some do not and hence it is always recommended practice to usefile nativename
to convert them.
When constructing the command line, keep in mind that the account
under which the service will run is quite likely different than
the account used for installation. This means differences in drive
letter assignments, environment settings like PATH and TCLLIBPATH
etc. have to be taken into account. You have to also ensure that the
script is accessible to the service account which it may not
be if it is placed in your own secured Documents area.
|
4.3. Uninstalling a service
Like installation, uninstalling a service can be done either with
the sc.exe
Windows program or directly from Tcl with
the delete_service
command.
However, we leave illustrating uninstallation for the end as we still have to demonstrate our sample service.
4.4. Querying and modifying configuration
All configuration parameters of a service can be queried and
modified at any time. Yet again, we can use sc.exe
or do it from
Tcl with get_service_configuration
and set_service_configuration
.
We do not discuss this further except to mention that the
options used with
create_service
are applicable to get_service_configuration
and
set_service_configuration
as well. In addition, they also take a
-command
option which corresponds to the service invocation
command line that is passed as the second argument to create_service
.
% set_service_configuration MyTimeServer -displayname "Super Accurate Time \
Service"
% print_dict [get_service_configuration MyTimeServer -starttype -command \
-displayname]
→ -command = D:\tcl\magic\bin\tclsh.exe D:\src\tcl-on-windows\book\script...
-displayname = Super Accurate Time Service
-starttype = demand_start
5. Controlling services
We are now ready for the final steps needed to illustrate our sample service by running it and having it respond to control signals.
However, we will first define a small procedure that we can call to query it for the time.
proc query_time {} {
set so [socket 127.0.0.1 9999]
set response [read $so]
close $so
return $response
}
5.1. Starting a service
The start_service
command starts up a specified service.
% start_service MyTimeServer
→ 0
Let us verify that the service indeed started by querying its status and retrieving the time.
% query_time
→ Fri Oct 09 10:54:11 IST 2020
% get_service_state MyTimeServer
→ running
5.2. Pausing a service
We can ask the service to temporarily pause its function by
issuing the pause_service
command.
% pause_service MyTimeServer
→ 0
% query_time
% get_service_state MyTimeServer
→ paused
Note we get an empty response back from our time server since it is paused.
5.3. Resuming a service
The continue_service
command sends a continue
signal causing
the paused process to resume service.
% continue_service MyTimeServer
→ 0
% query_time
→ Fri Oct 09 10:54:11 IST 2020
% get_service_state MyTimeServer
→ running
5.4. Sending notifications
Let us send a user defined notification which will switch the service
to returning UTC time. The notify_service
command can send any
user defined signal in the range 128
-255
.
This command is new in TWAPI 4.1. If you are using version 4.0,
you can use control command of the Windows sc.exe program
instead.
|
% notify_service MyTimeServer 128
% query_time
→ Fri Oct 09 05:24:11 GMT 2020
Hey, seems to all work.
5.5. Stopping a service
The stop_service
command stops a running service.
% stop_service MyTimeServer
→ 0
% get_service_state MyTimeServer
→ stop_pending
Before moving on, let us uninstall the service as we have not more use for it in this chapter and there is no point having it clutter up the services database.
% delete_service MyTimeServer
6. Monitoring services
The final task we need to discuss is monitoring of service status.
6.1. Monitoring a service
The most basic command to retrieve the state of a service is
get_service_state
which we have already seen earlier. More
detailed information is available through the get_service_status
command. This command retrieves the last reported status of the
service. We can use interrogate_service
to force the service to
update its status.
% interrogate_service rpcss
% print_dict [get_service_status rpcss]
→ checkpoint = 0
controls_accepted = 192
exitcode = 0
interactive = 0
pid = 1168
service_code = 0
serviceflags = 0
servicetype = win32_share_process
state = running
wait_hint = 0
Refer to the TWAPI documentation for details about the
fields. The ones of particular interest are state
which holds
one of the values in Service states and pid
which
gives the process ID of the process hosting the service.
6.2. Monitoring all services
If the status of all services is desired, or for enumerating
services, use get_multiple_service_status
command. This command
returns a recordarray with the same fields as returned by
get_service_status
and accepts options to limit the services returned.
For example, names of all driver services can be enumerated as follows.
% print_list [recordarray column [get_multiple_service_status -kernel_driver \
-file_system_driver] displayname]
→ 1394 OHCI Compliant Host Controller
3ware
Microsoft ACPI Driver
ACPI Devices driver
Microsoft ACPIEx Driver
...Additional lines omitted...
Alternatively, more detailed information can be extracted, this time for user mode services that are active.
% recordarray iterate arec [get_multiple_service_status -win32_share_process \
-win32_own_process -active] {
puts "***Service $arec(displayname)***"
parray arec
}
→ ***Service Adobe Acrobat Update Service***
arec(checkpoint) = 0
arec(controls_accepted) = 1
arec(displayname) = Adobe Acrobat Update Service
arec(exitcode) = 0
arec(interactive) = 0
arec(name) = AdobeARMservice
arec(pid) = 4828
arec(service_code) = 0
arec(serviceflags) = 0
arec(servicetype) = win32_own_process
arec(state) = running
arec(wait_hint) = 0
***Service Application Information***
arec(checkpoint) = 0
arec(controls_accepted) = 129
arec(displayname) = Application Information
arec(exitcode) = 0
arec(interactive) = 0
arec(name) = Appinfo
...Additional lines omitted...
6.3. Monitoring dependent services
A related command is get_dependent_service_status
which is
similar to get_multiple_service_status
but takes an additional
argument - the name of a service - and returns the status of all
services that are dependent on it. This is useful when you want to
shut down a service so that you can stop its dependents first.
The following command prints the list of all dependents of rpcss
that are running.
% print_list [recordarray column [get_dependent_service_status rpcss -active] \
name]
→ ZeroConfigService
WSearch
wscsvc
WpnService
WlanSvc
...Additional lines omitted...
The dependent services are returned in reverse order of starting. Thus they should be stopped in the same order that they are returned. |
7. Managing Remote Services
So far all our discussion and examples have targeted services on
the local system. However, all commands we have described
take a -system
option which allows them to work with services on
a remote system as well.
For example, a small change to one of the earlier examples shows
the status of rpcss
on a remote system.
% print_dict [get_service_status rpcss -system IO]
→ checkpoint = 0
controls_accepted = 192
exitcode = 0
interactive = 0
pid = 1168
...Additional lines omitted...
This capability is not just for status and monitoring commands, but for commands related to control, installation and configuration as well.
For remote access to work, the process must have an authenticated connection to the remote system.
8. References
- RICH2000
-
Programming Server-Side Applications for Windows 2000, Richter, Clark, Microsoft Press, 2000. Chapters 3 and 4 detail writing Windows services and control programs.
- SDKSERV
-
Windows Service Reference, Windows SDK documentaion, http://msdn.microsoft.com/en-us/library/windows/desktop/ms685974(v=vs.85).aspx.