/*
 * utils.cc: Various utility functions for cygrunsrv.
 *
 * Copyright 2001, 2002, 2003, 2004, 2005, 2012, 2013, 2014 Corinna Vinschen,
 * <corinna@vinschen.de>
 *
 * This program is free software; 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 3 of the License, or
 * (at your option) any later version.
 * 
 * This program 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.
 * 
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
*/

#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <syslog.h>
#include <unistd.h>
#include <sys/stat.h>

#include <windows.h>

#include "cygrunsrv.h"
#include "utils.h"

const char *reason_list[] = {
  "",
  "Exactly one of --install, --remove, --start, --stop, --query, or --list is required",
  "--path is required with --install",
  "Given path doesn't point to a valid executable",
  "--path is only allowed with --install",
  "Only one --path is allowed",
  "--crs-path is only allowed with --install",
  "Only one --crs-path is allowed",
  "--args is only allowed with --install",
  "Only one --args is allowed",
  "--chdir is only allowed with --install",
  "Only one --chdir is allowed",
  "--env is only allowed with --install",
  "--disp is only allowed with --install",
  "Only one --disp is allowed",
  "--desc is only allowed with --install",
  "Only one --desc is allowed",
  "--user is only allowed with --install",
  "Only one --user is allowed",
  "--pass is only allowed with --install",
  "Only one --pass is allowed",
  "--type is only allowed with --install",
  "Only one --type is allowed",
  "Invalid type, only `a[uto]' or `m[anual]' are vaild.",
  "--termsig is only allowed with --install",
  "Only one --termsig is allowed",
  "--shutsig is only allowed with --install",
  "Only one --shutsig is allowed",
  "Invalid signal; must be number or name like INT, QUIT, TERM, etc.",
  "--dep is only allowed with --install",
  "--std{in,out,err},--pidfile are only allowed with --install",
  "Each of --std{in,out,err},--pidfile is allowed only once",
  "--neverexits is only allowed with --install",
  "Only one --neverexits is allowed",
  "--preshutdown is only allowed with --install",
  "--shutdown is only allowed with --install",
  "Only one --preshutdown/--shutdown is allowed",
  "--shutsig option requires one of --preshutdown/--shutdown",
  "--interactive is only allowed with --install",
  "Only one --interactive is allowed",
  "--interactive not allowed with --user",
  "--nohide is only allowed with --install",
  "Only one --nohide is allowed",
  "--timeout is only allowed with --install",
  "Only one --timeout allowed",
  "Invalid timeout value",
  "--stop-timeout is only allowed with --install",
  "Only one --stop-timeout allowed",
  "Trailing commandline arguments not allowed",
  "You must specify one of the `-IRSE' options",
  "Error installing a service",
  "Error removing a service",
  "Error starting a service",
  "Error stopping a service",
  "Error querying a service",
  "Error enumerating services",
  "Error trying to connect to remote registry",
  NULL
};

int
error (reason_t reason, const char *func, DWORD win_err)
{
  if (reason > UnrecognizedOption && reason < MaxReason)
    {
      fprintf (stderr, "%s: %s", appname, reason_list[reason]);
      if (reason > StartAsSvcErr)
        {
	  if (func)
	    fprintf (stderr, ": %s", func);
	  if (win_err)
	    fprintf (stderr, ":  Win32 error %u:\n%s",
	    	     (unsigned) win_err, winerror (win_err));
	}
      if (reason <= StartAsSvcErr)
        fprintf (stderr, "\nTry `%s --help' for more information.", appname);
      fputc ('\n', stderr);
    }
  return 1;
}

char *
winerror (DWORD win_err)
{
  static char errbuf[1024]; /* That's really sufficient. */

  errbuf[0] = '\0';
  FormatMessage(FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS,
  		NULL, win_err, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
		errbuf, 1024, NULL);
  return errbuf;
}

void
syslog_starterr (const char *func, DWORD win_err, int posix_err)
{
  syslog (LOG_ERR,
  	  "starting service `%s' failed: %s: %d, %s",
  	  svcname,
	  func,
	  win_err ?: posix_err,
	  win_err ? winerror (win_err) : strerror (posix_err));
}

int
usage ()
{
  fprintf (stderr, 
"Usage: %s [OPTION]...\n"
"\n"
"Main options: Exactly one is required.\n"
"\n"
"  -I, --install <svc_name>  Installes a new service named <svc_name>.\n"
"  -R, --remove <svc_name>   Removes a service named <svc_name>.\n"
"  -S, --start <svc_name>    Starts a service named <svc_name>.\n"
"  -E, --stop <svc_name>     Stops a service named <svc_name>.\n"
"  -Q, --query <svc_name>    Queries a service named <svc_name>.\n"
"  -L, --list [server]       Lists services that have been installed\n"
"                            with cygrunsrv.\n"
"  <svc_name> can be a local service or \"server/svc_name\" or \"server\\svc_name\".\n"
"\n"
"Required install options:\n"
"  -p, --path <app_path>     Application path which is run as a service.\n"
"\n"
"Miscellaneous install options:\n"
"  -P, --crs-path <path>     Path to cygrunsrv. This is useful for testing or\n"
"                            when installing a service on a remote machine.\n"
"  -a, --args <args>         Optional string with command line options which\n"
"                            is given to the service application on startup.\n"
"  -c, --chdir <directory>   Optional directory which will be used as working\n"
"                            directory for the application.\n"
"  -e, --env <VAR=VALUE>     Optional environment strings which are added\n"
"                            to the environment when service is started.\n"
"                            You can add up to " MAX_ENV_STR " environment strings using\n"
"                            the `--env' option.\n"
"                            Note: /bin is always added to $PATH to allow all\n"
"                            started applications to find cygwin DLLs.\n"
"  -d, --disp <display name> Optional string which contains the display name\n"
"                            of the service. Defaults to service name.\n"
"  -f, --desc <description>  Optional string which contains the service\n"
"                            description.\n"
"  -t, --type [auto|manual]  Optional start type of service. Defaults to `auto'.\n"
"  -u, --user <user name>    Optional user name to start service as.\n"
"                            Defaults to SYSTEM account.\n"
"  -w, --passwd <password>   Optional password for user. Only needed\n"
"                            if a user is given. If a user has an empty\n"
"                            password, enter `-w ""'. If a user is given but\n"
"                            no password, cygrunsrv will ask for a password\n"
"                            interactively.\n"
"  -s, --termsig <signal>    Optional signal to send to service application\n"
"                            when service is stopped.  <signal> can be a number\n"
"                            or a signal name such as HUP, INT, QUIT, etc.\n"
"                            Default is TERM.\n"
"  -z, --shutsig <signal>    Optional signal to send to service application\n"
"                            when shutdown has been initiated.  Default is the\n"
"                            same signal as defined as termination signal.\n"
"  -y, --dep <svc_name2>     Optional name of service that must be started\n"
"                            before this new service.  The --dep option may\n"
"                            be given up to " MAX_DEPS_STR " times, listing another dependent\n"
"                            service each time.\n"
"  -0, --stdin <file>        Optional input file used for stdin redirection.\n"
"                            Default is /dev/null.\n"
"  -1, --stdout <file>       Optional output file used for stdout redirection.\n"
"                            Default is /var/log/<svc_name>.log.\n"
"  -2, --stderr <file>       Optional output file used for stderr redirection.\n"
"                            Default is /var/log/<svc_name>.log.\n"
"  -x, --pidfile <file>      Optional path for .pid file written by application\n"
"                            after fork().\n"
"                            Default is that application must not fork().\n"
"  -n, --neverexits          Service should never exit by itself.\n"
"  -O, --preshutdown         Stop service application during system preshutdown.\n"
"  -o, --shutdown            Stop service application during system shutdown.\n"
"                            (Only one of --preshutdown and --shutdown is\n"
"                             accepted.  Preshutdown is preferred, but only\n"
"                             available since Windows Vista/Longhorn.  On earlier\n"
"                             OS versions it's silently converted to --shutdown).\n"
"  -i, --interactive         Allow service to interact with the desktop\n"
"                            (No effect since Windows Vista/Longhorn).\n"
"  -j, --nohide              Don't hide console window when service interacts\n"
"                            with desktop.\n"
"  -T, --timeout <secs>      Timeout for lengthy service operations (start/stop),\n"
"                            default is " SERVICE_TMO_STR " seconds\n"
"  -X, --stop-timeout <secs> Timeout to stop service (defaults to the -T value).\n"
"\n"
"Informative output:\n"
"  -V, --verbose             When used with --query or --list, causes extra\n"
"  -h, --help                print this help, then exit.\n"
"  -v, --version             print cygrunsrv program version number, then exit.\n"
"\n"
"Report bugs to <cygwin@cygwin.com>.\n", appname);

  return 1;
}

BOOL
is_executable (const char *path)
{
  struct stat st;

  if (stat (path, &st))
    return 0;
  if (!S_ISREG (st.st_mode))
    return 0;
  return !access (path, X_OK);
}

int
create_parent_directory (const char *path)
{
  char buf[MAX_PATH + 1], *c;
  struct stat st;

  strcpy (buf, path);
  /* If not in the root dir. */
  if ((c = strrchr (buf, '/')) && c > buf + 1 && c[-1] != ':')
    {
      *c = '\0';
      /* Parent path already exists ... */
      if (!stat (buf, &st))
        {
	  /* ...but is not a directory. */
	  if (!S_ISDIR (st.st_mode))
	    return -1;
	  /* ...but isn't accessible for me. */
	  if (access (buf, W_OK) &&
	      chmod (buf, S_ISVTX | S_IRWXU | S_IRWXG | S_IRWXO))
	    return -1;
	  return 0;
	}
      /* If we can't create the dir, try to create the parent dir (recursive).
         If that fails or another try to create the dir, give up. */
      if (mkdir (buf, S_ISVTX | S_IRWXU | S_IRWXG | S_IRWXO) &&
          (create_parent_directory (buf) ||
	   mkdir (buf, S_ISVTX | S_IRWXU | S_IRWXG | S_IRWXO)))
        return -1;
    }
  return 0;
}

int
redirect_fd (int fd, const char *path, BOOL output)
{
  char funcbuf[MAX_PATH + 64];
  int nfd = -1;

  close (fd);
  if (!output)
    nfd = open (path, O_RDWR);
  else if (!create_parent_directory (path))
    nfd = open (path, O_WRONLY | O_CREAT | O_APPEND, 0644);
  if (nfd < 0)
    {
      sprintf (funcbuf, "redirect_fd: open (%d, %s)", fd, path);
      syslog_starterr (funcbuf, 0, errno);
      return -1;
    }
  if (nfd != fd)
    {
      if (dup2 (nfd, fd))
        {
	  sprintf (funcbuf, "redirect_fd: dup2 (%d, %s)", fd, path);
	  syslog_starterr (funcbuf, 0, errno);
	  return -1;
	}
      close (nfd);
    }
  return 0;
}

int
redirect_io (const char *stdin_path, const char *stdout_path,
	     const char *stderr_path)
{
  if (redirect_fd (STDIN_FILENO, stdin_path, FALSE) ||
      redirect_fd (STDOUT_FILENO, stdout_path, TRUE) ||
      redirect_fd (STDERR_FILENO, stderr_path, TRUE))
    return -1;
  return 0;
}
