Controlling the Freelancer server with an NT service

Creating an NT service

This article describes making an NT service to control the server. As usual, documentation from Microsoft is fairly thin on the ground. And as usual, meaningful documentation is thinner than a supermodel squashed under a cartoon anvil.

Here's what I managed to figure out...

Services overview

A program which wishes to run one or more services must call StartServiceCtrlDispatcher() with a list of those services, and provide a pointer to a function (the documentation uses ServiceMain() as an example but you can call it whatever you like) that handles the service startup.

ServiceMain() is responsible for registering a callback function which handles requests from the service manager (START, STOP, PAUSE etc) and signalling to the service manager that the service was started successfully. This is done via RegisterServiceCtrlHandlerEx().

ServiceMain() receives the familiar argc and argv arguments, with argv[0] being the name of the service and any subsequent arguments coming from the start parameters box in the services manager.

The control handler responds to control requests by returning NO_ERROR on successful completion of the requested action, or ERROR_CALL_NOT_IMPLEMENTED if it cannot (or will not) process the request. Thus your service may ignore the instruction to stop, for example.

Each time it is asked to do something, the service should call SetServiceStatus() to inform the service manager what state it is in (running, shutting down, stopped etc) and any error status it might have to report.

Writing the service

Without further ado, let's look at the code to implement the service API.

/* Initialise service status */
SERVICE_STATUS status;
ZeroMemory(&status, sizeof(status));
status.dwServiceType = SERVICE_WIN32_OWN_PROCESS | SERVICE_INTERACTIVE_PROCESS;
status.dwCurrentState = SERVICE_RUNNING;
status.dwControlsAccepted = SERVICE_ACCEPT_SHUTDOWN | SERVICE_ACCEPT_STOP;
status.dwWin32ExitCode = NO_ERROR;
status.dwServiceSpecificExitCode = 0;
status.dwCheckPoint = 0;
status.dwWaitHint = 1000;

The above code is run in main() before doing anything else. It describes the type of service and what control requests can be processed. The important parts of the SERVICE_STATUS data structure are dwCurrentState, dwWin32ExitCode and dwServiceSpecificExitCode. These values will be changed during the lifetime of the service and must be signalled to the service manager.

main()'s final act is to start the service control dispatcher, passing the name of the service and the startup function in a SERVICE_TABLE_ENTRY structure:

SERVICE_TABLE_ENTRY table[] = { { ME, service_main }, { 0, 0 } };
if (! StartServiceCtrlDispatcher(table)) /* Error */

Here is service_main():

/* Service initialisation */
SERVICE_STATUS_HANDLE handle;

void WINAPI service_main(unsigned long argc, char **argv) {
  char exe[MAX_PATH];
  char flags[MAX_PATH];
  char dir[MAX_PATH];

  /* Get startup parameters */
  if (get_parameters(argv[0], exe, sizeof(exe), flags, sizeof(flags), dir, sizeof(dir))) /* Error */

  /* Register control handler */
  handle = RegisterServiceCtrlHandlerEx(ME, service_control_handler, 0);
  if (! handle) /* Error */

  /* Start the service */
  if (start_service(exe, flags, dir)) /* Error */;

  /* Monitor service */
  HANDLE handle;
  RegisterWaitForSingleObject(&handle, pid, monitor_service, 0, INFINITE, WT_EXECUTEDEFAULT);
}

The exe, flags and dir variables define the server executable, command line arguments and working directory respectively. They are grabbed from the registry by the get_parameters() function, of which I will make no further mention, except to say that it makes use of the fact that argv[0] is the service name. This article is about services, not the registry.

After successfully obtaining the startup parameters, we call start_service() to start the Freelancer server and then register a callback which will be called when the server exits, and whose sole task is to stop the service. This allows the service to stop when the server dies and avoids the common problem of a service claiming to be running when in fact it isn't.

Here is start_service():

/* Start the service */
int start_service(char *exe, char *flags, char *dir) {
  if (pid) return 0;

  /* Allocate a STARTUPINFO structure for a new process */
  STARTUPINFO si;
  ZeroMemory(&si, sizeof(si));
  si.cb = sizeof(si);

  /* Allocate a PROCESSINFO structure for the process */
  PROCESS_INFORMATION pi;
  ZeroMemory(&pi, sizeof(pi));

  /* Launch flserver.exe */
  char cmd[MAX_PATH];
  if (_snprintf(cmd, sizeof(cmd), "%s %s %s", exe, FL_FLAGS, flags) < 0) {
    return stop_service(2);
  }
  if (! CreateProcess(0, cmd, 0, 0, 0, 0, 0, dir, &si, &pi)) {
    return stop_service(3);
  }
  pid = pi.hProcess;

  /* Signal successful start */
  status.dwCurrentState = SERVICE_RUNNING;
  SetServiceStatus(handle, &status);

  return 0;
}

After allocating some data structures which the operating system wants, start_service() constructs a command line to run the server. This is formed by taking the full path to the executable, any user-supplied flags and the /c flag which tells the server to start directly rather than wait for any configuration changes to be made.

start_service() then calls CreateProcess() to actually run the server, and stores a load of information in pi. In particular, we will be interested in a handle to the process later on. This is stored in the global variable pid.

Here is service_control_handler():

/* Service control handler */
unsigned long WINAPI service_control_handler(unsigned long control, unsigned long event, void *data, void *context) {
  switch (control) {
    case SERVICE_CONTROL_SHUTDOWN:
    case SERVICE_CONTROL_STOP:
      stop_service(0);
      return NO_ERROR;
  }
  
  /* Unknown control */
  return ERROR_CALL_NOT_IMPLEMENTED;
}

Requests to stop the service provoke a call to stop_service() while other requests are ignored. There is no request to start the service; you start your service when you run and exit when it stops.

Finally, here is stop_service():

/* Stop the service */
int stop_service(unsigned long exitcode) {
  /* Signal we are stopping */
  status.dwCurrentState = SERVICE_STOP_PENDING;
  SetServiceStatus(handle, &status);

  /* Nothing to do if server isn't running */
  if (pid) {
    /* Shut down server */
    TerminateProcess(pid, 0);
    pid = 0;
  }

  /* Signal we stopped */
  status.dwCurrentState = SERVICE_STOPPED;
  if (exitcode) {
    status.dwWin32ExitCode = ERROR_SERVICE_SPECIFIC_ERROR;
    status.dwServiceSpecificExitCode = exitcode;
  }
  else {
    status.dwWin32ExitCode = NO_ERROR;
    status.dwServiceSpecificExitCode = 0;
  }
  SetServiceStatus(handle, &status);

  return exitcode;
}

The first thing we do is tell the service manager that the service has changed its state to STOP_PENDING. In other words, that we are shutting down. Next it calls TerminateProcess() to kill the server, TerminateThread() to kill the monitoring thread and signals its exit status to the service manager using SetServiceStatus().

Conclusion

We now have a service application which can start up the Freelancer server, monitor its activity and shut it down when requested. Currently, however, we don't have a way to add the service to the system database. Read on.


Jump to a section

intro | part 1: Creating an NT service | part 2: Installing the service