/*
* anopa - Copyright (C) 2015-2017 Olivier Brunel
*
* enable_service.c
* Copyright (C) 2015-2017 Olivier Brunel <jjk@jjacky.com>
*
* This file is part of anopa.
*
* anopa 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.
*
* anopa 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
* anopa. If not, see http://www.gnu.org/licenses/
*/
#define _BSD_SOURCE
#include <errno.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <dirent.h>
#include <fcntl.h>
#include <stdio.h> /* rename() */
#include <skalibs/bytestr.h>
#include <skalibs/djbunix.h>
#include <skalibs/direntry.h>
#include <skalibs/skamisc.h>
#include <skalibs/stralloc.h>
#include <anopa/enable_service.h>
#include <anopa/copy_file.h>
#include <anopa/scan_dir.h>
#include <anopa/err.h>
#include "service_internal.h"
static int
copy_from_source (const char *name,
size_t len,
aa_warn_fn warn_fn,
aa_enable_flags flags,
aa_auto_enable_cb ae_cb);
static int
copy_dir (const char *src,
const char *dst,
mode_t mode,
int depth,
aa_warn_fn warn_fn,
aa_enable_flags flags,
aa_auto_enable_cb ae_cb,
const char *instance);
static int
do_auto_needs_wants (const char *name, aa_enable_flags flags, aa_auto_enable_cb ae_cb);
static int
copy_log (const char *name,
const char *cfg,
mode_t mode,
aa_warn_fn warn_fn,
aa_enable_flags flags,
aa_auto_enable_cb ae_cb)
{
int fd;
int r;
int e;
/* get current dir (repo) so we can come back */
fd = open_read (".");
if (fd < 0)
return fd;
/* and temporarily go into the servicedir */
r = chdir (name);
if (r < 0)
{
e = errno;
fd_close (fd);
errno = e;
return r;
}
flags |= _AA_FLAG_IS_LOGGER;
/* this is a logger, so there's no autoenable of any kind; hence we can use
* 0 for flags (don't process it as a servicedir either, since it doesn't
* apply) and not bother with a callback */
r = copy_from_source ("log", 3, warn_fn, flags | _AA_FLAG_IS_SERVICEDIR, NULL);
if (r >= 0 && cfg)
r = copy_dir (cfg, "log", mode, 0, warn_fn, flags | _AA_FLAG_IS_CONFIGDIR, NULL, NULL);
if (r >= 0 && ae_cb && flags & (AA_FLAG_AUTO_ENABLE_NEEDS | AA_FLAG_AUTO_ENABLE_WANTS))
r = do_auto_needs_wants ("log", flags, ae_cb);
e = errno;
fd_chdir (fd);
fd_close (fd);
errno = e;
return r;
}
static int
clear_dir (const char *path, int excludes, aa_warn_fn warn_fn)
{
DIR *dir;
size_t salen = satmp.len;
dir = opendir (path);
if (!dir)
return -ERR_IO;
errno = 0;
for (;;)
{
direntry *d;
int r = 0;
d = readdir (dir);
if (!d)
break;
if (d->d_name[0] == '.'
&& (d->d_name[1] == '\0' || (d->d_name[1] == '.' && d->d_name[2] == '\0')))
continue;
if (!stralloc_cats (&satmp, path)
|| !stralloc_cats (&satmp, "/")
|| !stralloc_cats (&satmp, d->d_name)
|| !stralloc_0 (&satmp))
goto err;
if (d->d_type == DT_UNKNOWN)
{
struct stat st;
r = stat (satmp.s + salen, &st);
if (r < 0)
goto err;
if (S_ISREG (st.st_mode))
d->d_type = DT_REG;
else if (S_ISDIR (st.st_mode))
d->d_type = DT_DIR;
}
if (excludes)
{
if (d->d_type == DT_REG
&& (str_equal (d->d_name, "status.anopa")
|| str_equal (d->d_name, "down")))
goto skip;
else if (d->d_type == DT_DIR
&& (str_equal (d->d_name, "supervise")
|| str_equal (d->d_name, "event")))
goto skip;
}
if (d->d_type == DT_DIR)
{
int is_log = (str_equal (d->d_name, "log")) ? 1 : 0;
r = clear_dir (satmp.s + salen, is_log, warn_fn);
if (r == 0 && !is_log)
r = rmdir (satmp.s + salen);
}
else
r = unlink (satmp.s + salen);
err:
if (r < 0)
warn_fn (satmp.s + salen, errno);
skip:
satmp.len = salen;
if (r < 0)
break;
}
if (errno)
{
int e = errno;
dir_close (dir);
errno = e;
return -ERR_IO;
}
dir_close (dir);
return 0;
}
static int
copy_dir (const char *src,
const char *dst,
mode_t mode,
int depth,
aa_warn_fn warn_fn,
aa_enable_flags flags,
aa_auto_enable_cb ae_cb,
const char *instance)
{
unsigned int l_satmp = satmp.len;
unsigned int l_max = strlen (AA_SCANDIR_DIRNAME);
DIR *dir;
struct stat st;
struct {
unsigned int run : 1;
unsigned int down : 1;
unsigned int began : 1;
unsigned int log : 1;
} has = { .run = 0, .down = 0, .began = 0, .log = 0 };
dir = opendir (src);
if (!dir)
return -ERR_IO;
if (depth == 0 && (flags & (_AA_FLAG_IS_SERVICEDIR | AA_FLAG_SKIP_DOWN))
== (_AA_FLAG_IS_SERVICEDIR | AA_FLAG_SKIP_DOWN))
/* treat as if there'e one, so don't create it */
has.down = 1;
errno = 0;
for (;;)
{
direntry *d;
size_t len;
d = readdir (dir);
if (!d)
break;
if (d->d_name[0] == '.'
&& (d->d_name[1] == '\0' || (d->d_name[1] == '.' && d->d_name[2] == '\0')))
continue;
len = strlen (d->d_name);
if (len > l_max)
l_max = len;
if (!stralloc_catb (&satmp, d->d_name, len + 1))
break;
if (depth == 0 && (flags & _AA_FLAG_IS_SERVICEDIR)
/* if UPGRADE we don't need this, so skip those tests */
&& !(flags & AA_FLAG_UPGRADE_SERVICEDIR))
{
if (!has.run && str_equal (d->d_name, "run"))
has.run = 1;
if (!has.down && str_equal (d->d_name, "down"))
has.down = 1;
}
}
if (errno)
{
int e = errno;
dir_close (dir);
errno = e;
goto err;
}
dir_close (dir);
if ((flags & (_AA_FLAG_IS_SERVICEDIR | AA_FLAG_UPGRADE_SERVICEDIR))
== (_AA_FLAG_IS_SERVICEDIR | AA_FLAG_UPGRADE_SERVICEDIR))
{
if (stat (dst, &st) < 0)
{
/* logger might be new */
if ((flags & _AA_FLAG_IS_LOGGER) && errno == ENOENT)
{
if (mkdir (dst, S_IRWXU) < 0)
goto err;
}
else
goto err;
}
else if (!S_ISDIR (st.st_mode))
{
errno = ENOTDIR;
goto err;
}
/* logger: was already cleared when processing main servicedir */
else if (!(flags & _AA_FLAG_IS_LOGGER) && clear_dir (dst, 1, warn_fn) < 0)
goto err;
}
else
{
if (mkdir (dst, S_IRWXU) < 0)
{
if (errno != EEXIST || stat (dst, &st) < 0)
goto err;
else if (!S_ISDIR (st.st_mode))
{
errno = ENOTDIR;
goto err;
}
else if (flags & _AA_FLAG_IS_SERVICEDIR)
{
errno = EEXIST;
goto err;
}
}
}
if (flags & _AA_FLAG_IS_SERVICEDIR)
{
has.began = 1;
flags &= ~_AA_FLAG_IS_SERVICEDIR;
}
{
size_t l_inst = (instance) ? strlen (instance) : 0;
size_t l_src = strlen (src);
size_t l_dst = strlen (dst);
size_t i = l_satmp;
char buf_src[l_src + 1 + l_max + 1];
char buf_dst[l_dst + 1 + l_max + l_inst + 1];
byte_copy (buf_src, l_src, src);
buf_src[l_src] = '/';
byte_copy (buf_dst, l_dst, dst);
buf_dst[l_dst] = '/';
while (i < satmp.len)
{
size_t len;
int r;
len = strlen (satmp.s + i);
byte_copy (buf_src + l_src + 1, len + 1, satmp.s + i);
byte_copy (buf_dst + l_dst + 1, len + 1, satmp.s + i);
if (stat (buf_src, &st) < 0)
{
warn_fn (buf_src, errno);
goto err;
}
if (S_ISREG (st.st_mode))
{
if (has.began && depth == 0 && !(flags & _AA_FLAG_IS_LOGGER)
&& str_equal (satmp.s + i, "log"))
{
r = copy_log (dst, NULL, 0, warn_fn, flags, ae_cb);
if (r == 0)
has.log = 1;
st.st_mode = 0755;
}
else if ((flags & _AA_FLAG_IS_CONFIGDIR) && len > 1
&& (satmp.s[i] == '-' || satmp.s[i] == '+'))
{
byte_copy (buf_dst + l_dst + 1, len, satmp.s + i + 1);
/* for any file in one of the 4 special places that ends
* with a '@' we append our instance name
* (don't make much sense for '+' but useful for '-' to
* remove "generic" ordering/dependencies) */
if (depth == 1 && instance && (flags & _AA_FLAG_IS_1OF4)
&& satmp.s[i + len - 1] == '@')
byte_copy (buf_dst + l_dst + len, l_inst + 1, instance);
if (satmp.s[i] == '-')
{
r = unlink (buf_dst);
if (r < 0 && errno == ENOENT)
/* not an error */
r = 0;
/* skip lchown/chmod calls */
goto next;
}
else /* '+' */
r = aa_copy_file (buf_src, buf_dst, st.st_mode, AA_CP_APPEND);
}
else
{
/* for any file in one of the 4 special places that ends
* with a '@' we append our instance name */
if (depth == 1 && instance && (flags & _AA_FLAG_IS_1OF4)
&& satmp.s[i + len - 1] == '@')
byte_copy (buf_dst + l_dst + 1 + len, l_inst + 1, instance);
r = aa_copy_file (buf_src, buf_dst, st.st_mode, AA_CP_OVERWRITE);
}
}
else if (S_ISDIR (st.st_mode))
{
if (has.began && depth == 0 && !(flags & _AA_FLAG_IS_LOGGER)
&& str_equal (satmp.s + i, "log"))
{
r = copy_log (dst, buf_src, st.st_mode, warn_fn, flags, ae_cb);
if (r == 0)
has.log = 1;
}
else
{
/* use depth because this is also enabled for the config part */
if (depth == 0)
{
/* flag to enable auto-rename of files above */
if (str_equal (satmp.s + i, "needs")
|| str_equal (satmp.s + i, "wants")
|| str_equal (satmp.s + i, "before")
|| str_equal (satmp.s + i, "after"))
flags |= _AA_FLAG_IS_1OF4;
}
r = copy_dir (buf_src, buf_dst, st.st_mode, depth + 1,
warn_fn, flags, ae_cb, instance);
if (depth == 0)
flags &= ~_AA_FLAG_IS_1OF4;
}
}
else if (S_ISFIFO (st.st_mode))
r = mkfifo (buf_dst, st.st_mode);
else if (S_ISLNK (st.st_mode))
{
size_t l_tmp = satmp.len;
if ((sareadlink (&satmp, buf_src) < 0) || !stralloc_0 (&satmp))
r = -1;
else
r = symlink (satmp.s + l_tmp, buf_dst);
satmp.len = l_tmp;
}
else if (S_ISCHR (st.st_mode) || S_ISBLK (st.st_mode) || S_ISSOCK (st.st_mode))
r = mknod (buf_dst, st.st_mode, st.st_rdev);
else
{
errno = EOPNOTSUPP;
r = -1;
}
if (r >= 0)
r = lchown (buf_dst, st.st_uid, st.st_gid);
if (r >= 0 && !S_ISLNK (st.st_mode) && !S_ISDIR (st.st_mode))
r = chmod (buf_dst, st.st_mode);
next:
if (r < 0)
{
warn_fn (buf_dst, errno);
goto err;
}
i += len + 1;
}
if (has.run && !(flags & AA_FLAG_UPGRADE_SERVICEDIR))
{
if (!has.down)
{
char buf[l_dst + 1 + strlen ("down") + 1];
int fd;
byte_copy (buf, l_dst, dst);
buf[l_dst] = '/';
byte_copy (buf + l_dst + 1, 5, "down");
fd = open_create (buf);
if (fd < 0)
{
warn_fn (buf, errno);
goto err;
}
else
fd_close (fd);
}
if (!(flags & _AA_FLAG_IS_LOGGER))
{
char buf_lnk[3 + l_dst + 1];
char buf_dst[sizeof (AA_SCANDIR_DIRNAME) + l_dst + 1];
byte_copy (buf_lnk, 3, "../");
byte_copy (buf_lnk + 3, l_dst + 1, dst);
byte_copy (buf_dst, sizeof (AA_SCANDIR_DIRNAME), AA_SCANDIR_DIRNAME "/");
byte_copy (buf_dst + sizeof (AA_SCANDIR_DIRNAME), l_dst + 1, dst);
if (symlink (buf_lnk, buf_dst) < 0)
{
warn_fn (buf_dst, errno);
goto err;
}
}
if (!(flags & AA_FLAG_NO_SUPERVISE))
{
char buf[l_dst + 1 + strlen ("supervise") + 1];
byte_copy (buf, l_dst, dst);
buf[l_dst] = '/';
byte_copy (buf + l_dst + 1, strlen ("supervise") + 1, "supervise");
if (mkdir (buf, 0711) < 0)
warn_fn (buf, errno);
}
}
}
if (chmod (dst, mode) < 0)
{
if (has.began)
warn_fn (dst, errno);
goto err;
}
satmp.len = l_satmp;
return (int) has.log;
err:
satmp.len = l_satmp;
if (!has.began)
return -ERR_IO;
else
{
size_t l_dst = strlen (dst);
char buf[1 + l_dst + 1];
*buf = '@';
byte_copy (buf + 1, l_dst + 1, dst);
/* rename dst servicedir by prefixing with a '@' so that aa-start would
* fail to find/start the service, and make it easilly noticable on the
* file system, since it's in an undetermined/invalid state */
if (rename (dst, buf) < 0)
warn_fn (dst, errno);
return -ERR_FAILED_ENABLE;
}
}
static int
copy_from_source (const char *name,
size_t len,
aa_warn_fn warn_fn,
aa_enable_flags flags,
aa_auto_enable_cb ae_cb)
{
size_t i;
if (aa_sa_sources.len == 0)
return -ERR_UNKNOWN;
i = 0;
for (;;)
{
size_t l_sce = strlen (aa_sa_sources.s + i);
char buf[l_sce + 1 + len + 1];
struct stat st;
byte_copy (buf, l_sce, aa_sa_sources.s + i);
buf[l_sce] = '/';
byte_copy (buf + l_sce + 1, len, name);
buf[l_sce + 1 + len] = '\0';
if (stat (buf, &st) < 0)
{
if (errno != ENOENT)
warn_fn (buf, errno);
}
else if (!S_ISDIR (st.st_mode))
warn_fn (buf, ENOTDIR);
else
return copy_dir (buf, name, st.st_mode, 0, warn_fn, flags, ae_cb,
(name[len - 1] == '@') ? name + len : NULL);
i += l_sce + 1;
if (i >= aa_sa_sources.len)
return -ERR_UNKNOWN;
}
}
static int
it_cb (direntry *d, void *_data)
{
struct {
aa_auto_enable_cb cb;
unsigned int flag;
} *data = _data;
data->cb (d->d_name, data->flag);
return 0;
}
static int
do_auto_needs_wants (const char *name, aa_enable_flags flags, aa_auto_enable_cb ae_cb)
{
stralloc sa = STRALLOC_ZERO;
struct {
aa_auto_enable_cb cb;
unsigned int flag;
} data = { .cb = ae_cb };
size_t l_name = strlen (name);
int r = 0;
if (!stralloc_catb (&sa, name, l_name))
{
errno = ENOMEM;
return -1;
}
if (flags & AA_FLAG_AUTO_ENABLE_NEEDS)
{
if (!stralloc_cats (&sa, "/needs") || !stralloc_0 (&sa))
{
stralloc_free (&sa);
errno = ENOMEM;
return -1;
}
data.flag = AA_FLAG_AUTO_ENABLE_NEEDS;
r = aa_scan_dir (&sa, 1, it_cb, &data);
if (r == -ERR_IO && errno == ENOENT)
r = 0;
sa.len = l_name;
}
if (r == 0 && flags & AA_FLAG_AUTO_ENABLE_WANTS)
{
if (!stralloc_cats (&sa, "/wants") || !stralloc_0 (&sa))
{
stralloc_free (&sa);
errno = ENOMEM;
return -1;
}
data.flag = AA_FLAG_AUTO_ENABLE_WANTS;
r = aa_scan_dir (&sa, 1, it_cb, &data);
if (r == -ERR_IO && errno == ENOENT)
r = 0;
}
{
int e = errno;
stralloc_free (&sa);
errno = e;
}
return r;
}
int
aa_enable_service (const char *_name,
aa_warn_fn warn_fn,
aa_enable_flags flags,
aa_auto_enable_cb ae_cb)
{
const char *name = _name;
const char *instance = NULL;
mode_t _mode = 0; /* silence warning */
size_t l_name = strlen (name);
size_t len;
int has_log = 0;
int r;
/* if name is a /path/to/file we get the actual/service name */
if (*name == '/')
{
if (l_name == 1)
return -ERR_INVALID_NAME;
r = byte_rchr (name, l_name, '/') + 1;
name += r;
l_name -= r;
}
if (!_is_valid_service_name (name, l_name))
return -ERR_INVALID_NAME;
if (*_name == '/')
{
struct stat st;
if (stat (_name, &st) < 0)
return -ERR_IO;
else if (S_ISREG (st.st_mode))
/* file; so nothing special to do, we can "drop" the path */
_name = name;
else if (!S_ISDIR (st.st_mode))
return (errno = EINVAL, -ERR_IO);
else
_mode = st.st_mode;
}
/* len is l_name unless there's a '@', then we want up to (inc.) the '@' */
len = byte_chr (name, l_name, '@');
if (len < l_name)
{
++len;
instance = name + len;
}
r = copy_from_source (name, len, warn_fn, flags | _AA_FLAG_IS_SERVICEDIR, ae_cb);
if (r < 0)
return r;
else if (r > 0)
has_log = 1;
if (name != _name)
{
r = copy_dir (_name, name, _mode, 0, warn_fn, flags | _AA_FLAG_IS_CONFIGDIR, ae_cb, instance);
if (r < 0)
return r;
else if (r > 0)
has_log = 1;
}
if (has_log)
{
size_t l = sizeof ("/log/run-args");
char buf[l_name + l];
struct stat st;
byte_copy (buf, l_name, name);
byte_copy (buf + l_name, l, "/log/run-args");
r = stat (buf, &st);
if (r == 0 && S_ISREG (st.st_mode))
{
char dst[l_name + l - 5];
byte_copy (dst, l_name, name);
byte_copy (dst + l_name, l - 5, "/log/run");
r = aa_copy_file (buf, dst, st.st_mode, AA_CP_APPEND);
if (r == 0)
unlink (buf);
}
else if (r < 0 && errno == ENOENT)
r = 0;
}
if (ae_cb && flags & (AA_FLAG_AUTO_ENABLE_NEEDS | AA_FLAG_AUTO_ENABLE_WANTS))
r = do_auto_needs_wants (name, flags, ae_cb);
return (r >= 0) ? has_log : r;
}