A Place Where No Dreams Come True...

Current Topic: A simple Linux WebSocket echo server...

Creating A Proper Persistant Linux Daemon or Not...

The accepted methods for developing a Linux service, or 'daemon', are a little to general for my needs. Most are taylored toward mearly running programs, or even worse scripts, as background processes. For me... A daemon is an extension of the O.S I can use to shape the role of the host computer on my network. So... My first point is to distinguish between a background process and an actual Service.

In the Linux world there are only daemons. However... There is quite a difference in requirement from a simple user background process daemon and a root level server daemon directly accessable via a public network. If for nothing else the inhearent security risk. There is a fine point here which is how the daemon is started. If the daemon is mearly launched from, say, a command line or spawned from a user level application it really doesn't matter. However, in my case (always), it is enabled and started through the 'init' system manager at boot time. For me using 'Arch Linux' it is 'systemd'. As a result of this distinction I will point out the suttle differences I found that need consideration when desiging a proper server daemon.

Procedure For Creating A Background Process On Linux...

There are several steps neccessary for detaching from the user world. In order to operate in the shadows a program must...

Here's How It Works...

fork() : Creates a copy of the calling process (parent) for which there are three possible return values. Of these possibilities the parent (calling) process receives two... -1 An error occured or a positive integer representing the id of the new 'child' process. If a child process was created (no error) the child will receive 0 (zero) as the return from fork. It is the difference in return value that provides the context neccessary for a monolithic section of code to differentiate between running as the parent, or, running as the child. So... If... The fork succeded the parent has a choice to remain resident or exit. For a daemon to run in the background it must detach from the parent. But instead the parent process just exits and orphans the child.

setsid() : Creates a new 'Session' and attaches the child to it. The child is now effectively a parent process (session leader)... But who is its parent. This is another fine point determining the daemons role. If the daemon was started by a user level shell or script then it is necessary to fork an additional time in order to detach from the session/group and be orphaned again hopefully to be adopted by the init process. Quite simply... The whole effort here is to become a 'background' process owned by the init process. In my example, since the daemon has been started by the system manager (systemd), it is already running in the system session/group and is a child of 'init' (the system).

chdir() : Change the working directory making sure that it not some random directory that might be transient (umount).

umask() : Change the file mode access flags. Without this step the new process will have no access privileges to file I/O.

close() : Close any file descriptors inhereted from the parent (fork) particularly stdin, stdout, stderr. Necessary because, if all went well, the new process is NOT attached to any tty so stdio is now invalid. Another fine point to make here. If the daemon uses stdio then it is additionally necessary to restablish those file handles. One way to accomplish this is to simply create a null set; ie... open("/dev/null"). Another is to use stdio to redirect them to maybe a daemon private log file.

Daemons Exposed To A Public Network Require Special Consideration...

Mostly... When I write a daemon its role is usually as a server on either a private network or occasionally a direct route from the Internet (public server). This is my final point... The first true daemon I wrote and started with the system manager was running as a 'root' process. With a direct public network interface (Internet). This was an instant panic moment! In this paricular case I would strongly recommend that the daemon 'user' and 'group' be changed to a benign set that has only the privileges neccessary to satisfy the service purpose. Additionally it is also necessary to make sure that any log file privileges have been modified to include access by the daemon user/group.

The following code is a typical example of the startup procedure I use to launch a 'system' daemon. The daemon referenced is a Whiteboard Chat server that is, as many of my daemons are, directly exposed to the public Internet.

//-----------------------------------------------------------------------------
// main.c
//
//  WebSocket Whiteboard/Chat Echo Server Example.
//
// Copyright (C) 2017 - Dysfunctional Farms A.K.A. www.smegware.com
//
//  All Smegware software is free; you can redistribute it and/or modify
//  it under the terms of the GNU General Public License as published by
//  the Free Software Foundation; either version 2 of the License, or
//  (at your option) any later version.
//
//  This software is distributed in the hope that it will be useful,
//  but WITHOUT ANY WARRANTY; without even the implied warranty of
//  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
//  GNU General Public License for more details.
//
//-----------------------------------------------------------------------------
//
// History...
//
//   $Source$
//   $Author$
// $Revision$
//
// $Log$
//
//-----------------------------------------------------------------------------

#include <sys/stat.h>
#include <stdlib.h>
#include <memory.h>
#include <fcntl.h>
#include <unistd.h>
#include <signal.h>
#include <pwd.h>
#include <grp.h>

#include "util.h"
#include "smegchat.h"

//-----------------------------------------------------------------------------

static int wsport  = DEFAULTWSPORT;
static int service = 0;

//-----------------------------------------------------------------------------

static void show_usage(void)
{
  printf("Usage: -[dmp[n]vw?] -[S|Q|I].\n");
  printf(" -d  -D   : Report Debug messages.\n");
  printf(" -I       : Set Interactive command interpreter interface.\n");
  printf(" -m  -M   : Report general progress messages.\n");
  printf(" -pn -Pn  : WebSocket server port (default=%i).\n", DEFAULTWSPORT);
  printf(" -Q       : Alert daemon to quit and exit.\n");
  printf(" -S       : Run as systemd service.\n");
  printf(" -v  -V   : Report Verbose debug messages.\n");
  printf(" -w  -W   : Report Warning messages.\n");
  printf(" -?       : Display this message.\n");
}

//-----------------------------------------------------------------------------

static void show_config(char *prog, pid_t sid, uid_t uid, gid_t gid)
{
  server_log(dbglevl_msg, "%s SID=%i UID=%i GID=%i Smegchat daemon startup using parameters...\n",
             prog, sid, uid, gid);
  enum report_level hmm = report_get_level();
  switch(hmm)
  {
    case dbglevl_msg:
      server_log(dbglevl_msg, " Log level  : 'Message'.\n");
      break;

    case dbglevl_warn:
      server_log(dbglevl_msg, " Log level  : 'Warn'.\n");
      break;

    case dbglevl_debug:
      server_log(dbglevl_msg, " Log level  : 'Debug'.\n");
      break;

    case dbglevl_verbose:
      server_log(dbglevl_msg, " Log level  : 'Verbose'.\n");
      break;

    case dbglevl_VERBOSE:
      server_log(dbglevl_msg, " Log level  : 'Extra Verbose'.\n");
      break;

    case dbglevl_ERROR:
      server_log(dbglevl_msg, " Log level  : 'Error'.\n");
      break;

    default:
      break;
  }
  server_log(dbglevl_msg, " port       : %i.\n", wsport);
  if(service)
  {
    server_log(dbglevl_msg, " Run As     : Service.\n");
  }
  else
  {
    server_log(dbglevl_msg, " Run As     : Console.\n");
  }
}

//-----------------------------------------------------------------------------

static int smegchat_daemon(char *prog)
{
  int rtn = EXIT_FAILURE;
  int tmp;
  pid_t pid;
  pid_t sid;
  uid_t uid;
  gid_t gid;
  struct passwd *pw;
  struct group *grp;
  FILE *log = NULL;
  int fd1;
  int fd2;
  char s[32];

  // Fork off the parent process.
  pid = fork();
  if(pid == 0)
  {
    // Child process.
    // Catch/Ignore signals.
    signal(SIGCHLD, SIG_IGN);
    signal(SIGHUP, SIG_IGN);
    // Create a new SID for the child process.
    sid = setsid();
    if(sid > 0)
    {
      // Change the current working directory.
      if((chdir("/")) < 0)
      {
        // CHANGEME: Fatal error maybe?
        report_error("error : smegchat_daemon():chdir(\"/\") failed - ");
      }
      // Change the file mode mask.
      umask(0);
      report(dbglevl_debug, "smegchat_daemon():umask(0) child continuing...\n");
      report(dbglevl_debug, "smegchat_daemon():setsid() returned %i creating file %s.\n",
             sid, SMEGCHATRUNPID);
      if((tmp = creat(SMEGCHATRUNPID, 0644)) != -1)
      {
        sprintf(s, "%i\n", sid);
        write(tmp, s, strlen(s));
        close(tmp);
        // Run daemon with no specific privileges.
        pw = getpwnam(SMEGCHATUSER);
        if(pw != NULL)
        {
          uid = pw->pw_uid;
          grp = getgrnam(SMEGCHATGROUP);
          if(grp != NULL)
          {
            gid = grp->gr_gid;
            if(setgid(gid) == 0)
            {
              if(setuid(uid) == 0)
              {
                // Close out the standard file descriptors.
                server_log(dbglevl_debug, "smegchat_daemon() - Closing std file descriptors.\n");
                close(STDIN_FILENO);
                close(STDOUT_FILENO);
                close(STDERR_FILENO);
                // Create files 0,1,2 to catch any tty I/O.
                // NOTE: log file needs wr access for daemon user.
                log = fopen(DEFAULTLOGNAM, "a+");
                if(log)
                {
                  tmp = report_get_level();
                  report_init(log);
                  report_set_level(tmp);
                  server_log(dbglevl_debug, "smegchat_daemon() - Opened log file %s.\n",
                             DEFAULTLOGNAM);
                }
                else
                {
                  // Not a fatal error.
                  report_error("error : smegchat_daemon():fopen(%s) failed - ",
                               DEFAULTLOGNAM);
                  // Redirect to null device.
                  open("/dev/null", O_RDWR);
                }
                // Duplicate first descriptor to complete set.
                fd1 = dup(0);
                fd2 = dup(0);
                // The main event.
                // Update log.
                show_config(prog, sid, uid, gid);
                report_flush();
                // Start the network servers.
                rtn = smegchat_server(wsport);
                // Update log.
                report_flush();
                if(log)
                {
                  // Close log file.
                  fclose(log);
                }
                close(fd1);
                close(fd2);
              }
              else
              {
                report_error("error : smegchat_daemon():setuid(%i) failed - ",uid);
              }
            }
            else
            {
              report_error("error : smegchat_daemon():setgid(%i) failed - ", gid);
            }
          }
          else
          {
            report_error("error : smegchat_daemon():getgrnam(%s) failed - ", SMEGCHATGROUP);
          }
        }
        else
        {
          report_error("error : smegchat_daemon():getpwnam(%s) failed - ", SMEGCHATUSER);
        }
      }
      else
      {
        report_error("error : smegchat_daemon():creat(%s) failed - ", SMEGCHATRUNPID);
      }
    }
    else
    {
      report_error("error : smegchat_daemon():setsid() failed - ");
    }
  }
  else
  {
    // Parent process.
    if(pid > 0)
    {
      // A valid PID means the parent process can exit gracefully.
      report(dbglevl_debug, "smegchat_daemon():fork() success - parent exiting...\n");
      rtn = EXIT_SUCCESS;
    }
    else
    {
      // Exit in shame...
      report_error("error : smegchat_daemon():fork() failed - ");
    }
  }
  return rtn;
}

//-----------------------------------------------------------------------------

int main(int argc, char **argv)
{
  int  rtn = -1;
  int  arg;

  // Can be changed later.
  report_init(stdout);
  for(arg = 1; arg < argc; arg++)
  {
    if(argv[arg][0] == '-')
    {
      switch(argv[arg][1])
      {
        case 'd':
        case 'D':
          // Debug level logging.
          report_set_level(dbglevl_debug);
          break;

        case 'i':
        case 'I':
          // Command line interpreter interface.
          interactive_smegchat();
          return 0;
          break;

        case 'm':
        case 'M':
          // Message level logging.
          report_set_level(dbglevl_msg);
          break;

        case 'p':
        case 'P':
          // WebSocket listen port.
          wsport = 0;
          sscanf(&argv[arg][2], "%i", &wsport);
          break;

        case 'q':
        case 'Q':
          // Request daemon graceful exit.
          stop_smegchat();
          return 0;
          break;

        case 's':
        case 'S':
          // Run as systemd service (daemon).
          service = 1;
          break;

        case 'v':
        case 'V':
          // Verbose level logging.
          report_set_level(dbglevl_verbose);
          break;

        case 'w':
        case 'W':
          // Warning level logging.
          report_set_level(dbglevl_warn);
          break;

        case '?':
          // Display usage/help list.
          show_usage();
          rtn = 0;
          break;

        default:
          break;
      }
    }
  }
  if(rtn)
  {
    if(service)
    {
      rtn = smegchat_daemon(argv[0]);
    }
    else
    {
      rtn = smegchat_server(wsport);
    }
  }
  return rtn;
}

//-----------------------------------------------------------------------------
// end: main.c

Conclusions...

A Live Demonstration of the Whiteboard Chat is usually available here...

10199