Thursday, May 11, 2017

Daemons - 1st steps

In the very start of the endeavor to the daemons' underground world I thought it would be inevitable to be acquainted with a few, perhaps new, essential concepts about processes in terms of how they are collected within the system. So I set out to knowing more about sessions and process groups. It was very instructive as a ground to better undrestanding the necessary steps to turn an ordinary process into a daemon in the traditional way. So, without further words, in essence, the main() function of a Solaris C++ program (or application) that is (or becomes) a daemon in the traditional way is:

#include <sys/types.h> 
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

#include <cerrno> 
#include <cstring>
#include <csignal> 
#include <cstdlib>
#include <cstdio>  
#include <cstdarg>  

#include <string>
#include <iostream>

#include "toolbox.hxx"

int main()
{
  // Some other log file or even syslog could be better.
  // This is just for illustrational purposes.

  // The full implementation is elsewhere. 
  toolbox::log log( "trace.log" );

  if ( log.open() )
    std::cout
       << "Log file: "
       << log.filename
       << std::endl;
  else
  {
    std::cout
       << "FAILURE"
       << std::endl;

    ::exit( EXIT_FAILURE );
  }
  

  // This function does all the "magic"!
  daemon( log );

  log.write( "Finally, success! I'm a daemon!\n" );

  const int seconds = 30;
  log.write( "Resting for %d seconds...\n", seconds );
  ::sleep( seconds );

  log.write( "Bye cruel world!\n\n" );
  log.close();

  return EXIT_SUCCESS;
}


Now let me show the magical :-) daemon() function:

// Little helper...
void error( const toolbox::log & log, const char * msg, ... )
{

  int code = errno;

  va_list ap;
  va_start( ap, msg );

  // MT-safe if setlocale(3C) not called
  // Hence, possbily a serious limitation!
  // More advanced C++ to the rescue!

  char * buffer = 0;
  ::vasprintf( &buffer, msg, ap );
  log.write( "ERROR: %s (%s)\n", buffer, ::strerror(code) );
  ::free( buffer );

  va_end( ap );


  ::exit( EXIT_FAILURE );
}

void daemon( toolbox::log & log )
{
  if ( ! log.write( "\n\nTrying to \"become\" daemon ...\n\n" ) )
    ::exit( EXIT_FAILURE );

  pid_t pid;

  // Creates a new process in the same process group.
  // Sure is the new process is NOT a process group leader.
  // The new process will be entitled to create a new session.

  if ( ( pid = ::fork() ) < 0 )
    error( log, "1st call to fork()" );
  else if ( pid > 0 )
  {
    log.write( "Bye 1st generation descendant!\n" );
    log.write( "PID %d to become a daemon!\n\n", pid );
    ::exit( EXIT_SUCCESS );
  }

  // Creates a new session, effectively
  // disassociating from the controlling terminal.

  if ( ::setsid() < 0 )
    error( log, "setsid()" );

  // The SIGHUP does not apply to me anymmore.
  // There's no controlling terminal after all!
 
  if ( ::sigignore( SIGHUP ) )
    error( log, "sigignore()" ); 


  // Creates another new process in the new process group.
  // Sure is the new process is NOT a process group leader.
  // The new process won't EVER get a controlling terminal.

  if ( ( pid = ::fork() ) < 0 )
    error(  log, "2nd call to fork()" );
  else if ( pid > 0 )
  {
    log.write( "Bye 2nd generation descendant!\n" );
    log.write( "PID %d to become a daemon!\n\n", pid );
    ::exit( EXIT_SUCCESS );
  }

  //
  // Finally! Almost there...
  //


  // Initially assures full access to daemon's own files.
  // Later, should be diligently and temporarily set to 007,
  // specially before library calls that involve files.

  ::umask( 0 );

  // Changes to the selected working directory
  // or at least to / so any unmounts should succeed.

  // After this call don't forget to specify full paths!
  if ( ::chdir( "/" ) < 0 )
    error( log, "chdir()" );


  {
    // Makes sure there's no left over open files.
    log.close();
    ::fcloseall();

    // Mute all the standard files.
    const int std_stream[] =
    {
      ::open( "/dev/null", O_RDWR ), // stdin
      ::dup2( 0, 1 ),                // stdout
      ::dup2( 0, 2 )                 // stderr
    };

    if ( ! log.open() )
      exit( EXIT_FAILURE );

    // Double-check muted descriptors are 0, 1 and 2.
    for ( auto i=0; i<3; i++ )
      if ( std_stream[ i ] != i )
        error( log, "Muting standard files." );
  }
}


A log file produced by running this daemon could be:
 
$ cat trace.log
[2017-05-11 16:29:29]
 

Trying to "become" daemon ...

[2017-05-11 16:29:29] Bye 1st generation descendant!
[2017-05-11 16:29:29] PID 2508 to become a daemon!

[2017-05-11 16:29:29] Bye 2nd generation descendant!
[2017-05-11 16:29:29] PID 2509 to become a daemon!

[2017-05-11 16:29:29] Finally, success! I'm a daemon!
[2017-05-11 16:29:29] Resting for 30 seconds...
[2017-05-11 16:29:59] Bye cruel world!

   
The above approach, although traditional is not the recommended one. Nevertheless it's still possible to catch once in a while some daemons logs in SMF logs which reveal they could only have been programmed the old way...