https://github.com/OpenRC/openrc/pull/811/commits diff --git a/etc/rc.conf b/etc/rc.conf index ca0f9207..09467b51 100644 --- a/etc/rc.conf +++ b/etc/rc.conf @@ -92,8 +92,8 @@ #unicode="YES" # This is how long fuser should wait for a remote server to respond. The -# default is 60 seconds, but it can be adjusted here. -#rc_fuser_timeout=60 +# default is 20 seconds, but it can be adjusted here. +#rc_fuser_timeout=20 # Below is the default list of network fstypes. # diff --git a/init.d/localmount.in b/init.d/localmount.in index e336164d..7c71171d 100644 --- a/init.d/localmount.in +++ b/init.d/localmount.in @@ -112,7 +112,7 @@ stop() # Umount loop devices einfo "Unmounting loop devices" eindent - do_unmount "umount -d" --skip-point-regex "$no_umounts_r" \ + rc_unmount -d -- --skip-point-regex "$no_umounts_r" \ --node-regex "^/dev/loop" eoutdent @@ -125,7 +125,7 @@ stop() fs="$fs${fs:+|}$x" done [ -n "$fs" ] && fs="^($fs)$" - do_unmount umount --skip-point-regex "$no_umounts_r" \ + rc_unmount -- --skip-point-regex "$no_umounts_r" \ "${fs:+--skip-fstype-regex}" $fs --nonetdev eoutdent diff --git a/init.d/mount-ro.in b/init.d/mount-ro.in index fba65c81..8f489252 100644 --- a/init.d/mount-ro.in +++ b/init.d/mount-ro.in @@ -25,8 +25,6 @@ start() sync ebegin "Remounting remaining filesystems read-only" - # We need the do_unmount function - . "$RC_LIBEXECDIR"/sh/rc-mount.sh eindent # Bug 381783 @@ -48,7 +46,7 @@ start() fs="$fs${fs:+|}$x" done [ -n "$fs" ] && fs="^($fs)$" - do_unmount "umount -r" \ + rc_unmount -r -- \ --skip-point-regex "$m" \ "${fs:+--skip-fstype-regex}" $fs --nonetdev ret=$? diff --git a/init.d/netmount.in b/init.d/netmount.in index d89fa639..756253eb 100644 --- a/init.d/netmount.in +++ b/init.d/netmount.in @@ -64,7 +64,6 @@ stop() local x= fs= ebegin "Unmounting network filesystems" - . "$RC_LIBEXECDIR"/sh/rc-mount.sh for x in $net_fs_list $extra_net_fs_list; do fs="$fs${fs:+,}$x" @@ -79,7 +78,7 @@ stop() fs="$fs${fs:+|}$x" done [ -n "$fs" ] && fs="^($fs)$" - do_unmount umount ${fs:+--fstype-regex} $fs --netdev + rc_unmount -- ${fs:+--fstype-regex} $fs --netdev retval=$? eoutdent diff --git a/man/openrc-run.8 b/man/openrc-run.8 index a090c30f..aeca09de 100644 --- a/man/openrc-run.8 +++ b/man/openrc-run.8 @@ -550,6 +550,17 @@ The f, F, n, N, o, O, p, P, e and E options specify what you want to search for or skip in the mounted file systems. The i, s and t options specify what you want to display. If no mount points are given, all mount points will be considered. +.It Xo +.Ic rc_unmount +.Op umount args... +.Op -- mountinfo args... +.Xc +Parallel unmounting utility. All arguments before +.Ic -- +are passed to +.Xr umount 1 +and arguments after it are processed the same as +.Xr mountinfo 1 . .It Ic yesno Ar value If .Ar value diff --git a/sh/meson.build b/sh/meson.build index 300a8a00..550bc22f 100644 --- a/sh/meson.build +++ b/sh/meson.build @@ -9,7 +9,6 @@ sh_conf_data.set('SYSCONFDIR', get_option('sysconfdir')) sh = [ 'rc-functions.sh', - 'rc-mount.sh', 'runit.sh', 's6.sh', 'start-stop-daemon.sh', diff --git a/sh/rc-mount.sh b/sh/rc-mount.sh deleted file mode 100644 index 6cd6a85b..00000000 --- a/sh/rc-mount.sh +++ /dev/null @@ -1,87 +0,0 @@ -# Copyright (c) 2007-2015 The OpenRC Authors. -# See the Authors file at the top-level directory of this distribution and -# https://github.com/OpenRC/openrc/blob/HEAD/AUTHORS -# -# This file is part of OpenRC. It is subject to the license terms in -# the LICENSE file found in the top-level directory of this -# distribution and at https://github.com/OpenRC/openrc/blob/HEAD/LICENSE -# This file may not be copied, modified, propagated, or distributed -# except according to the terms contained in the LICENSE file. - -# Declare this here so that no formatting doesn't affect the embedded newline -__IFS=" -" - -# Handy function to handle all our unmounting needs -# mountinfo is a C program to actually find our mounts on our supported OS's -# We rely on fuser being present, so if it's not then don't unmount anything. -# This isn't a real issue for the BSD's, but it is for Linux. -do_unmount() -{ - local cmd="$1" retval=0 retry= pids=- - local f_opts="-m -c" f_kill="-s " mnt= - if [ "$RC_UNAME" = "Linux" ]; then - f_opts="-m" - f_kill="-" - fi - - shift - local IFS="$__IFS" - set -- $(mountinfo "$@") - unset IFS - for mnt; do - # Unmounting a shared mount can unmount other mounts, so - # we need to check the mount is still valid - mountinfo --quiet "$mnt" || continue - # Ensure we interpret all characters properly. - mnt=$(printf "$mnt") - - case "$cmd" in - umount) - ebegin "Unmounting $mnt" - ;; - *) - ebegin "Remounting $mnt read only" - ;; - esac - - retry=4 # Effectively TERM, sleep 1, TERM, sleep 1, KILL, sleep 1 - while ! LC_ALL=C $cmd "$mnt" 2>/dev/null; do - if command -v fuser >/dev/null 2>&1; then - pids="$(timeout -s KILL "${rc_fuser_timeout:-60}" \ - fuser $f_opts "$mnt" 2>/dev/null)" - fi - case " $pids " in - *" $$ "*) - eend 1 "failed because we are using" \ - "$mnt" - retry=0;; - " - ") - eend 1 - retry=0;; - " ") - eend 1 "in use but fuser finds nothing" - retry=0;; - *) - if [ $retry -le 0 ]; then - eend 1 - else - local sig="TERM" - : $(( retry -= 1 )) - [ $retry = 1 ] && sig="KILL" - fuser $f_kill$sig -k $f_opts \ - "$mnt" >/dev/null 2>&1 - sleep 1 - fi - ;; - esac - [ $retry -le 0 ] && break - done - if [ $retry -le 0 ]; then - retval=1 - else - eend 0 - fi - done - return $retval -} diff --git a/src/mountinfo/meson.build b/src/mountinfo/meson.build index 71b8a652..67a4f9c8 100644 --- a/src/mountinfo/meson.build +++ b/src/mountinfo/meson.build @@ -1,7 +1,9 @@ -executable('mountinfo', - ['mountinfo.c', misc_c, usage_c, version_h], - c_args : cc_branding_flags, - include_directories: [incdir, einfo_incdir, rc_incdir], - link_with: [libeinfo, librc], - install: true, - install_dir: rc_bindir) +foreach exec : ['mountinfo', 'rc_unmount'] + executable(exec, + ['mountinfo.c', rc_exec_c, timeutils_c, misc_c, usage_c, version_h], + c_args : cc_branding_flags, + include_directories: [incdir, einfo_incdir, rc_incdir], + link_with: [libeinfo, librc], + install: true, + install_dir: rc_bindir) +endforeach diff --git a/src/mountinfo/mountinfo.c b/src/mountinfo/mountinfo.c index 001fe08a..e07dad38 100644 --- a/src/mountinfo/mountinfo.c +++ b/src/mountinfo/mountinfo.c @@ -43,6 +43,8 @@ #include "einfo.h" #include "queue.h" #include "rc.h" +#include "rc_exec.h" +#include "timeutils.h" #include "_usage.h" #include "helpers.h" @@ -84,6 +86,21 @@ const char * const longopts_help[] = { }; const char *usagestring = NULL; +#define UMOUNT_ARGS_MAX 16 +#define RUN_MAX 32 +#define TRY_MAX 3 +#define TRY_DELAY_MS 1000 + +struct run_queue { + const char *mntpath; + int64_t last_exec_time; + int64_t fuser_exec_time; + pid_t pid; + pid_t fuser_pid; + int try_count; + int fuser_stdoutfd; +}; + typedef enum { mount_from, mount_to, @@ -97,7 +114,12 @@ typedef enum { net_no } net_opts; +struct args; +typedef int process_func_t(RC_STRINGLIST *, struct args *, + char *, char *, char *, char *, int); + struct args { + process_func_t *process; regex_t *node_regex; regex_t *skip_node_regex; regex_t *fstype_regex; @@ -107,6 +129,7 @@ struct args { RC_STRINGLIST *mounts; mount_type mount_type; net_opts netdev; + const char *check_mntpath; }; static int @@ -238,7 +261,7 @@ static struct opt { }; static RC_STRINGLIST * -find_mounts(struct args *args) +find_mounts(struct args *args, size_t *num_mounts) { struct statfs *mnts; int nmnts; @@ -272,7 +295,7 @@ find_mounts(struct args *args) flags &= ~o->o_opt; } - process_mount(list, args, + *num_mounts += 0 == args->process(list, args, mnts[i].f_mntfromname, mnts[i].f_mntonname, mnts[i].f_fstypename, @@ -307,7 +330,7 @@ getmntfile(const char *file) } static RC_STRINGLIST * -find_mounts(struct args *args) +find_mounts(struct args *args, size_t *num_mounts) { FILE *fp; char *buffer; @@ -342,7 +365,9 @@ find_mounts(struct args *args) netdev = 1; } - process_mount(list, args, from, to, fst, opts, netdev); + *num_mounts += 0 == args->process(list, args, + from, to, fst, opts, netdev); + free(buffer); buffer = NULL; } @@ -356,6 +381,157 @@ find_mounts(struct args *args) # error "Operating system not supported!" #endif +static int is_prefix(const char *needle, const char *hay) +{ + size_t nlen = strlen(needle); + if (strncmp(needle, hay, nlen) == 0 && hay[nlen] == '/') + return true; + return false; +} + +static char *unescape_octal(char *beg) +{ + int n, i; + char *w = beg, *r = beg; + while (*r) { + if (*r != '\\' || *++r == '\\') { + *w++ = *r++; + } else { + /* octal. should have at least 3 bytes, + * but don't choke on malformed input + */ + for (i = n = 0; i < 3; ++i) { + if (*r >= '0' && *r <= '7') { + n <<= 3; + n |= *r++ - '0'; + } else { + break; + } + } + if (n) + *w++ = n; + } + } + *w = '\0'; + return beg; +} + +static int check_is_mounted(RC_STRINGLIST *list RC_UNUSED, struct args *args, + char *from RC_UNUSED, char *to, char *fstype RC_UNUSED, + char *options RC_UNUSED, int netdev RC_UNUSED) +{ + return strcmp(args->check_mntpath, unescape_octal(to)) == 0 ? 0 : -1; +} + +static int is_mounted(const char *mntpath) +{ + size_t num_mounts = 0; + struct args args = { .process = check_is_mounted, .check_mntpath = mntpath }; + RC_STRINGLIST *l = find_mounts(&args, &num_mounts); + rc_stringlist_free(l); + return num_mounts > 0; +} + +static pid_t run_umount(const char *mntpath, + const char **umount_args, int umount_args_num) +{ + struct exec_args args; + struct exec_result res; + const char *argv[UMOUNT_ARGS_MAX + 3]; + int k, i = 0; + + argv[i++] = "umount"; + for (k = 0; k < umount_args_num; ++k) + argv[i++] = umount_args[k]; + argv[i++] = mntpath; + argv[i++] = NULL; + + args = exec_init(argv); + args.redirect_stdout = args.redirect_stderr = EXEC_DEVNULL; + res = do_exec(&args); + if (res.pid < 0) + eerrorx("%s: failed to run umount: %s", applet, strerror(errno)); + return res.pid; +} + +static void fuser_run(struct run_queue *rp, const char *fuser_opt) +{ + static int fuser_exec_failed = 0; + const char *argv[] = { "fuser", fuser_opt, rp->mntpath, NULL }; + struct exec_result res; + struct exec_args args; + + /* if exec failed, fuser likely doesn't exist. so don't retry */ + if (fuser_exec_failed) + return; + + args = exec_init(argv); + args.redirect_stdout = EXEC_MKPIPE; + args.redirect_stderr = EXEC_DEVNULL; + res = do_exec(&args); + if (res.pid < 0) { + fuser_exec_failed = 1; + } else { + rp->fuser_pid = res.pid; + rp->fuser_stdoutfd = res.proc_stdout; + rp->fuser_exec_time = tm_now(); + } +} + +static int fuser_decide(struct run_queue *rp, + const char *fuser_opt, const char *fuser_kill_prefix) +{ + char buf[1<<12]; + char selfpid[64]; + int read_maybe_truncated; + ssize_t n; + + if (rp->fuser_stdoutfd < 0) + return 0; + + buf[0] = ' '; + n = read(rp->fuser_stdoutfd, buf + 1, sizeof buf - 3); + close(rp->fuser_stdoutfd); + rp->fuser_stdoutfd = -1; + read_maybe_truncated = (n == sizeof buf - 3); + if (n < 0 || read_maybe_truncated) + return 0; + while (n > 0 && buf[n] == '\n') + --n; + buf[n+1] = ' '; + buf[n+2] = '\0'; + snprintf(selfpid, sizeof selfpid, " %lld ", (long long)getpid()); + + if (strstr(buf, selfpid)) { + /* lets not kill ourselves */ + eerror("Unmounting %s failed because we are using it", rp->mntpath); + return -1; + } else if (strcmp(buf, " ") == 0) { + if (rp->try_count >= TRY_MAX) { + eerror("Unmounting %s failed but fuser finds no one using it", rp->mntpath); + return -1; + } + /* it's possible that whatever was using the mount stopped + * using it now, so allow 1 more retry */ + rp->try_count = TRY_MAX; + return 0; + } else { + char sig[32]; + const char *argv[] = { + "fuser", sig, "-k", fuser_opt, rp->mntpath, NULL + }; + struct exec_result res; + struct exec_args args = exec_init(argv); + args.redirect_stdout = args.redirect_stderr = EXEC_DEVNULL; + snprintf(sig, sizeof sig, "%s%s", fuser_kill_prefix, + rp->try_count == TRY_MAX ? "KILL" : "TERM"); + res = do_exec(&args); + if (res.pid > 0) + rc_waitpid(res.pid); + return 0; + } +} + static regex_t * get_regex(const char *string) { @@ -382,7 +558,23 @@ int main(int argc, char **argv) char *real_path = NULL; int opt; int result; - char *this_path; + char *this_path, *argv0; + const char *tmps; + const char *fuser_opt, *fuser_kill_prefix; + size_t num_mounts = 0; + size_t unmount_index; + int doing_unmount = 0; + pid_t pid; + int status, flags; + int64_t tmp, next_retry, now; + int64_t rc_fuser_timeout = -1; + const char *umount_args[UMOUNT_ARGS_MAX]; + int umount_args_num = 0; + const char **mounts = NULL; + struct run_queue running[RUN_MAX] = {0}; + struct run_queue *rp; + size_t num_running = 0, num_waiting = 0; + enum { STATE_RUN, STATE_REAP, STATE_RETRY, STATE_END } state; #define DO_REG(_var) \ if (_var) free(_var); \ @@ -390,11 +582,46 @@ int main(int argc, char **argv) #define REG_FREE(_var) \ if (_var) { regfree(_var); free(_var); } + argv0 = argv[0]; applet = basename_c(argv[0]); memset (&args, 0, sizeof(args)); args.mount_type = mount_to; args.netdev = net_ignore; args.mounts = rc_stringlist_new(); + args.process = process_mount; + + if (strcmp(applet, "rc_unmount") == 0) { + doing_unmount = 1; + while (argv[1]) { + /* shift over */ + tmps = argv[1]; + argv[1] = argv0; + ++argv; + --argc; + + if (strcmp(tmps, "--") == 0) + break; + if (umount_args_num >= (int)ARRAY_SIZE(umount_args)) + eerrorx("%s: Too many umount arguments", applet); + umount_args[umount_args_num++] = tmps; + } + + tmps = rc_conf_value("rc_fuser_timeout"); + if (tmps && (rc_fuser_timeout = parse_duration(tmps)) < 0) + ewarn("%s: Invalid rc_fuser_timeout value: `%s`. " + "Defaulting to 20", applet, tmps); + if (rc_fuser_timeout < 0) + rc_fuser_timeout = 20 * 1000; + + tmps = getenv("RC_UNAME"); + if (!tmps || strcmp(tmps, "Linux") == 0) { + fuser_opt = "-m"; + fuser_kill_prefix = "-"; + } else { + fuser_opt = "-cm"; + fuser_kill_prefix = "-s"; + } + } while ((opt = getopt_long(argc, argv, getoptstring, longopts, (int *) 0)) != -1) @@ -456,18 +683,13 @@ int main(int argc, char **argv) free(real_path); real_path = NULL; } - nodes = find_mounts(&args); + nodes = find_mounts(&args, &num_mounts); rc_stringlist_free(args.mounts); - REG_FREE(args.fstype_regex); - REG_FREE(args.skip_fstype_regex); - REG_FREE(args.node_regex); - REG_FREE(args.skip_node_regex); - REG_FREE(args.options_regex); - REG_FREE(args.skip_options_regex); - + if (doing_unmount) + mounts = xmalloc(num_mounts * sizeof(*mounts)); + num_mounts = 0; result = EXIT_FAILURE; - /* We should report the mounts in reverse order to ease unmounting */ TAILQ_FOREACH_REVERSE(s, nodes, rc_stringlist, entries) { if (point_regex && @@ -476,12 +698,162 @@ int main(int argc, char **argv) if (skip_point_regex && regexec(skip_point_regex, s->value, 0, NULL, 0) == 0) continue; - if (!rc_yesno(getenv("EINFO_QUIET"))) + if (doing_unmount) + mounts[num_mounts++] = unescape_octal(s->value); + else if (!rc_yesno(getenv("EINFO_QUIET"))) printf("%s\n", s->value); result = EXIT_SUCCESS; } - rc_stringlist_free(nodes); + if (!doing_unmount) + goto exit; + + /* STATE_RUN: + * can unmount => stays in STATE_RUN + * cannot unmount (for any of the reasons below) => STATE_REAP + * (a) nothing left to unmount + * (b) running queue is full + * (c) conflicts with running queue + * + * STATE_REAP: + * successful reap => STATE_RUN + * couldn't reap with WNOHANG and there are retries pending => STATE_RETRY + * nothing left to reap => STATE_RETRY + * + * STATE_RETRY: + * successfully launched a retry => STATE_REAP + * need to wait before retring -> sleep + * sleep successful => STATE_RETRY + * sleep interrupted via SIGCHILD (EINTR) => STATE_REAP + * nothing left to retry, reap + * and nothing to run either => STATE_END + * otherwise => STATE_RUN + */ + result = EXIT_SUCCESS; + state = STATE_RUN; + while (state != STATE_END) switch (state) { + case STATE_RUN: + for (unmount_index = 0; unmount_index < num_mounts; ++unmount_index) { + const char *candidate = mounts[unmount_index]; + int safe_to_unmount = 1; + for (size_t k = 0; safe_to_unmount && k < num_running; ++k) + safe_to_unmount = !is_prefix(candidate, running[k].mntpath); + for (size_t k = 0; safe_to_unmount && k < num_mounts; ++k) + safe_to_unmount = !is_prefix(candidate, mounts[k]); + if (!safe_to_unmount) + continue; + if (!is_mounted(candidate)) { + /* probably a shared mount and got unmounted, remove */ + mounts[unmount_index--] = mounts[--num_mounts]; + } else { + break; + } + } + if (num_running == RUN_MAX || unmount_index >= num_mounts) { + state = STATE_REAP; + break; + } + rp = running + num_running++; + rp->mntpath = mounts[unmount_index]; + rp->last_exec_time = tm_now(); + rp->try_count = 0; + rp->pid = run_umount(rp->mntpath, umount_args, umount_args_num); + rp->fuser_pid = -1; + rp->fuser_stdoutfd = -1; + rp->fuser_exec_time = -1; + mounts[unmount_index] = mounts[--num_mounts]; + break; + case STATE_REAP: + flags = (num_waiting > 0) ? WNOHANG : 0; + pid = waitpid(-1, &status, flags); + rp = NULL; + for (size_t i = 0; i < num_running; ++i) { + rp = running + i; + if (rp->fuser_pid == pid) + rp->fuser_pid = -1; + if (rp->pid == pid && pid > 0) + break; + rp = NULL; + } + if (rp) { + if ((WIFEXITED(status) && WEXITSTATUS(status) == 0) || + !is_mounted(rp->mntpath)) { + einfo("Unmounted %s", rp->mntpath); + *rp = running[--num_running]; + state = STATE_RUN; + } else if (rp->try_count >= TRY_MAX) { + eerror("Failed to unmount %s", rp->mntpath); + *rp = running[--num_running]; + result = EXIT_FAILURE; + } else { /* put into waiting queue */ + rp->pid = -1; + rp->try_count += 1; + num_waiting += 1; + fuser_run(rp, fuser_opt); + } + } else { + state = STATE_RETRY; + } + break; + case STATE_RETRY: + rp = NULL; + next_retry = INT64_MAX; + for (size_t i = 0; i < num_running; ++i) { + if (running[i].pid > 0) + continue; + if (running[i].fuser_pid > 0) + tmp = running[i].fuser_exec_time + rc_fuser_timeout; + else + tmp = running[i].last_exec_time + TRY_DELAY_MS; + if (tmp < next_retry) { + rp = running + i; + next_retry = tmp; + } + } + if (!rp) { + state = (num_mounts > 0) ? STATE_RUN : STATE_END; + break; + } + now = tm_now(); + if (next_retry > now) { + int64_t sleep_for = next_retry - now; + /* a child may become available for reaping *before* we + * enter sleep. cap the timeout to stay responsive. */ + if (sleep_for > 500) + sleep_for = 500; + if (tm_sleep(sleep_for, 0) != 0 && errno == EINTR) + state = STATE_REAP; + now = tm_now(); + } + if (next_retry <= now) { + if (rp->fuser_pid > 0) { + kill(rp->fuser_pid, SIGKILL); + waitpid(rp->fuser_pid, NULL, 0); + rp->fuser_pid = -1; + } + if (fuser_decide(rp, fuser_opt, fuser_kill_prefix) < 0) { /* abort */ + *rp = running[--num_running]; + result = EXIT_FAILURE; + } else { /* retry */ + rp->last_exec_time = tm_now(); + rp->pid = run_umount(rp->mntpath, + umount_args, umount_args_num); + } + num_waiting -= 1; + state = STATE_REAP; + } + break; + default: break; + } +exit: + free(mounts); + rc_stringlist_free(nodes); + REG_FREE(args.fstype_regex); + REG_FREE(args.skip_fstype_regex); + REG_FREE(args.node_regex); + REG_FREE(args.skip_node_regex); + REG_FREE(args.options_regex); + REG_FREE(args.skip_options_regex); REG_FREE(point_regex); REG_FREE(skip_point_regex); diff --git a/tools/manymounts.sh b/tools/manymounts.sh new file mode 100755 index 00000000..21c9327d --- /dev/null +++ b/tools/manymounts.sh @@ -0,0 +1,20 @@ +#!/bin/sh +# can be used for testing rc_unmount: +# # ./tools/manymounts.sh +# # rc_unmount -- -p '^/tmp/manymounts.*' + +set -- "0" "1" "2" "3" "4" "5" "6" "7" "8" "9" "A" "B" "C" "D" "E" "F" + +mntdir="/tmp/manymounts" +for a in "$@"; do + mkdir -p "${mntdir}/${a}" + mount -t tmpfs -o size=256K tmpfs "${mntdir}/${a}" + for b in "$@"; do + mkdir -p "${mntdir}/${a}/${b}" + mount -t tmpfs -o size=256K tmpfs "${mntdir}/${a}/${b}" + for c in "$@"; do + mkdir -p "${mntdir}/${a}/${b}/${c}" + mount -t tmpfs -o size=256K tmpfs "${mntdir}/${a}/${b}/${c}" + done + done +done