From 3f918161aafa61c1c2005709fda0b9bec4c412d8 Mon Sep 17 00:00:00 2001 From: William Hubbs Date: Fri, 5 Oct 2018 14:10:59 -0500 Subject: [PATCH] openrc-shutdown: Add scheduled shutdown and the ability to cancel a shutdown You can now schedule a shutdown for a certain time or a cpecific number of minutes into the future. When a shutdown is running, you can now cancel it with ^c from the keyboard or by running "openrc-shutdown -c" from another shell. --- man/openrc-shutdown.8 | 3 + src/rc/Makefile | 4 +- src/rc/broadcast.c | 209 +++++++++++++++++++++++++++++++++++++++ src/rc/broadcast.h | 16 +++ src/rc/openrc-shutdown.c | 179 +++++++++++++++++++++++++++++++-- 5 files changed, 402 insertions(+), 9 deletions(-) create mode 100644 src/rc/broadcast.c create mode 100644 src/rc/broadcast.h diff --git a/man/openrc-shutdown.8 b/man/openrc-shutdown.8 index 5db21334..b09e8c20 100644 --- a/man/openrc-shutdown.8 +++ b/man/openrc-shutdown.8 @@ -16,6 +16,7 @@ .Nd bring the system down .Sh SYNOPSIS .Nm +.Op Fl c , -cancel .Op Fl d , -no-write .Op Fl D , -dry-run .Op Fl H , -halt @@ -32,6 +33,8 @@ is the utility that communicates with to bring down the system or instruct openrc-init to re-execute itself. It supports the following options: .Bl -tag -width "poweroff" +.It Fl c , -cancel +Cancel a pending shutdown. .It Fl d , -no-write Do not write the wtmp boot record. .It Fl D , -dry-run diff --git a/src/rc/Makefile b/src/rc/Makefile index b09c5058..9ba240fa 100644 --- a/src/rc/Makefile +++ b/src/rc/Makefile @@ -14,7 +14,7 @@ SRCS+= rc-selinux.c endif ifeq (${OS},Linux) -SRCS+= kill_all.c openrc-init.c openrc-shutdown.c rc-wtmp.c +SRCS+= kill_all.c openrc-init.c openrc-shutdown.c broadcast.c rc-wtmp.c endif CLEANFILES= version.h rc-selinux.o @@ -134,7 +134,7 @@ mountinfo: mountinfo.o _usage.o rc-misc.o openrc rc: rc.o rc-logger.o rc-misc.o rc-plugin.o _usage.o ${CC} ${LOCAL_CFLAGS} ${LOCAL_LDFLAGS} ${CFLAGS} ${LDFLAGS} -o $@ $^ ${LDADD} -openrc-shutdown: openrc-shutdown.o _usage.o rc-wtmp.o +openrc-shutdown: openrc-shutdown.o rc-misc.o _usage.o broadcast.o rc-wtmp.o ${CC} ${LOCAL_CFLAGS} ${LOCAL_LDFLAGS} ${CFLAGS} ${LDFLAGS} -o $@ $^ ${LDADD} openrc-run runscript: openrc-run.o _usage.o rc-misc.o rc-plugin.o diff --git a/src/rc/broadcast.c b/src/rc/broadcast.c new file mode 100644 index 00000000..dbc861d1 --- /dev/null +++ b/src/rc/broadcast.c @@ -0,0 +1,209 @@ +/* + * broadcast.c + * broadcast a message to every logged in user + */ + +/* + * Copyright 2018 Sony Interactive Entertainment Inc. + * + * 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/master/LICENSE + * This file may not be copied, modified, propagated, or distributed + * except according to the terms contained in the LICENSE file. + */ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "broadcast.h" +#include "helpers.h" + +#ifndef _PATH_DEV +# define _PATH_DEV "/dev/" +#endif + +#ifndef UT_LINESIZE +#define UT_LINESIZE __UT_LINESIZE +#endif + +static sigjmp_buf jbuf; + +/* + * Alarm handler + */ +/*ARGSUSED*/ +# ifdef __GNUC__ +static void handler(int arg __attribute__((unused))) +# else +static void handler(int arg) +# endif +{ + siglongjmp(jbuf, 1); +} + +static void getuidtty(char **userp, char **ttyp) +{ + struct passwd *pwd; + uid_t uid; + char *tty; + static char uidbuf[32]; + static char ttynm[UT_LINESIZE + 4]; + + uid = getuid(); + if ((pwd = getpwuid(uid)) != NULL) { + uidbuf[0] = 0; + strncat(uidbuf, pwd->pw_name, sizeof(uidbuf) - 1); + } else { + if (uid) + sprintf(uidbuf, "uid %d", (int) uid); + else + sprintf(uidbuf, "root"); + } + + if ((tty = ttyname(0)) != NULL) { + const size_t plen = strlen(_PATH_DEV); + if (strncmp(tty, _PATH_DEV, plen) == 0) { + tty += plen; + if (tty[0] == '/') + tty++; + } + snprintf(ttynm, sizeof(ttynm), "(%.*s) ", + UT_LINESIZE, tty); + } else + ttynm[0] = 0; + + *userp = uidbuf; + *ttyp = ttynm; +} + +/* + * Check whether the given filename looks like a tty device. + */ +static int file_isatty(const char *fname) +{ + struct stat st; + int major; + + if (stat(fname, &st) < 0) + return 0; + + if (st.st_nlink != 1 || !S_ISCHR(st.st_mode)) + return 0; + + /* + * It would be an impossible task to list all major/minors + * of tty devices here, so we just exclude the obvious + * majors of which just opening has side-effects: + * printers and tapes. + */ + major = major(st.st_dev); + if (major == 1 || major == 2 || major == 6 || major == 9 || + major == 12 || major == 16 || major == 21 || major == 27 || + major == 37 || major == 96 || major == 97 || major == 206 || + major == 230) + return 0; + return 1; +} + +/* + * broadcast function. + */ +void broadcast(char *text) +{ + char *tty; + char *user; + struct utsname name; + time_t t; + char *date; + char *p; + char *line = NULL; + struct sigaction sa; + int fd; + FILE *tp; + int flags; + char *term = NULL; + struct utmpx *utmp; + + getuidtty(&user, &tty); + + /* + * Get and report current hostname, to make it easier to find out + * which machine is being shut down. + */ + uname(&name); + + /* Get the time */ + time(&t); + date = ctime(&t); + p = strchr(date, '\n'); + if (p) + *p = 0; + + xasprintf(&line, "\007\r\nBroadcast message from %s@%s %s(%s):\r\n\r\n", + user, name.nodename, tty, date); + + /* + * Fork to avoid hanging in a write() + */ + if (fork() != 0) + return; + + memset(&sa, 0, sizeof(sa)); + sa.sa_handler = handler; + sigemptyset(&sa.sa_mask); + sigaction(SIGALRM, &sa, NULL); + + setutxent(); + + while ((utmp = getutxent()) != NULL) { + if (utmp->ut_type != USER_PROCESS || utmp->ut_user[0] == 0) + continue; + if (strncmp(utmp->ut_line, _PATH_DEV, strlen(_PATH_DEV)) == 0) + xasprintf(&term, "%s", utmp->ut_line); + else + xasprintf(&term, "%s%s", _PATH_DEV, utmp->ut_line); + if (strstr(term, "/../")) { + free(term); + continue; + } + + /* + * Open it non-delay + */ + if (sigsetjmp(jbuf, 1) == 0) { + alarm(2); + flags = O_WRONLY|O_NDELAY|O_NOCTTY; + if (file_isatty(term) && (fd = open(term, flags)) >= 0) { + if (isatty(fd) && (tp = fdopen(fd, "w")) != NULL) { + fputs(line, tp); + fputs(text, tp); + fflush(tp); + } + } + } + alarm(0); + if (fd >= 0) + close(fd); + if (tp != NULL) + fclose(tp); + free(term); + } + endutxent(); + free(line); + exit(0); +} diff --git a/src/rc/broadcast.h b/src/rc/broadcast.h new file mode 100644 index 00000000..5f948272 --- /dev/null +++ b/src/rc/broadcast.h @@ -0,0 +1,16 @@ +/* + * Copyright 2018 Sony Interactive Entertainment Inc. + * + * 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/master/LICENSE + * This file may not be copied, modified, propagated, or distributed + * except according to the terms contained in the LICENSE file. + */ + +#ifndef BROADCAST_H +#define BROADCAST_H + +void broadcast(char *text); + +#endif diff --git a/src/rc/openrc-shutdown.c b/src/rc/openrc-shutdown.c index b17a63d8..ab2e7469 100644 --- a/src/rc/openrc-shutdown.c +++ b/src/rc/openrc-shutdown.c @@ -25,20 +25,24 @@ #include #include #include +#include #include #include #include +#include "broadcast.h" #include "einfo.h" #include "rc.h" #include "helpers.h" +#include "rc-misc.h" #include "_usage.h" #include "rc-wtmp.h" const char *applet = NULL; const char *extraopts = NULL; -const char *getoptstring = "dDHKpRrsw" getoptstring_COMMON; +const char *getoptstring = "cdDfFHKpRrsw" getoptstring_COMMON; const struct option longopts[] = { + { "cancel", no_argument, NULL, 'c'}, { "no-write", no_argument, NULL, 'd'}, { "dry-run", no_argument, NULL, 'D'}, { "halt", no_argument, NULL, 'H'}, @@ -51,6 +55,7 @@ const struct option longopts[] = { longopts_COMMON }; const char * const longopts_help[] = { + "cancel a pending shutdown", "do not write wtmp record", "print actions instead of executing them", "halt the system", @@ -64,8 +69,12 @@ const char * const longopts_help[] = { }; const char *usagestring = NULL; const char *exclusive = "Select one of " -"--halt, --kexec, --poweroff, --reexec, --reboot, --single or --write-only"; + "--cancel, --halt, --kexec, --poweroff, --reexec, --reboot, --single or \n" + "--write-only"; +const char *nologin_file = RC_SYSCONFDIR"/nologin"; +const char *shutdown_pid = "/run/openrc-shutdown.pid"; +static bool do_cancel = false; static bool do_dryrun = false; static bool do_halt = false; static bool do_kexec = false; @@ -76,6 +85,40 @@ static bool do_single = false; static bool do_wtmp = true; static bool do_wtmp_only = false; +static void cancel_shutdown(void) +{ + pid_t pid; + + pid = get_pid(applet, shutdown_pid); + if (pid <= 0) + eerrorx("%s: Unable to cancel shutdown", applet); + + if (kill(pid, SIGTERM) != -1) + einfo("%s: shutdown canceled", applet); + else + eerrorx("%s: Unable to cancel shutdown", applet); +} + +/* + * Create the nologin file. + */ +static void create_nologin(int mins) +{ + FILE *fp; + time_t t; + + time(&t); + t += 60 * mins; + + if ((fp = fopen(nologin_file, "w")) != NULL) { + fprintf(fp, "\rThe system is going down on %s\r\n", ctime(&t)); + fclose(fp); + } +} + +/* + * Send a command to our init + */ static void send_cmd(const char *cmd) { FILE *fifo; @@ -99,16 +142,59 @@ static void send_cmd(const char *cmd) fclose(fifo); } +/* + * sleep without being interrupted. + * The idea for this code came from sysvinit. + */ +static void sleep_no_interrupt(int seconds) +{ + struct timespec duration; + struct timespec remaining; + + duration.tv_sec = seconds; + duration.tv_nsec = 0; + + while(nanosleep(&duration, &remaining) < 0 && errno == EINTR) + duration = remaining; +} + +static void stop_shutdown(int sig) +{ + /* use the sig parameter so the compiler will not complain */ + if (sig == SIGINT) + ; + unlink(nologin_file); + unlink(shutdown_pid); +einfo("Shutdown canceled"); +exit(0); +} + int main(int argc, char **argv) { + char *ch = NULL; int opt; int cmd_count = 0; + int hour = 0; + int min = 0; + int shutdown_delay = 0; + struct sigaction sa; + struct tm *lt; + time_t tv; + bool need_warning = false; + char *msg = NULL; + char *state = NULL; + char *time_arg = NULL; + FILE *fp; applet = basename_c(argv[0]); while ((opt = getopt_long(argc, argv, getoptstring, longopts, (int *) 0)) != -1) { switch (opt) { + case 'c': + do_cancel = true; + cmd_count++; + break; case 'd': do_wtmp = false; break; @@ -117,14 +203,17 @@ int main(int argc, char **argv) break; case 'H': do_halt = true; + xasprintf(&state, "%s", "halt"); cmd_count++; break; case 'K': do_kexec = true; + xasprintf(&state, "%s", "reboot"); cmd_count++; break; case 'p': do_poweroff = true; + xasprintf(&state, "%s", "power off"); cmd_count++; break; case 'R': @@ -133,10 +222,12 @@ int main(int argc, char **argv) break; case 'r': do_reboot = true; + xasprintf(&state, "%s", "reboot"); cmd_count++; break; case 's': do_single = true; + xasprintf(&state, "%s", "go down for maintenance"); cmd_count++; break; case 'w': @@ -146,12 +237,88 @@ int main(int argc, char **argv) case_RC_COMMON_GETOPT } } - if (geteuid() != 0 && ! do_dryrun) + if (geteuid() != 0) eerrorx("%s: you must be root\n", applet); if (cmd_count != 1) { eerror("%s: %s\n", applet, exclusive); usage(EXIT_FAILURE); } + + if (do_cancel) { + cancel_shutdown(); + exit(EXIT_SUCCESS); + } else if (do_reexec) { + send_cmd("reexec"); + exit(EXIT_SUCCESS); + } + + if (optind >= argc) { + eerror("%s: No shutdown time specified", applet); + usage(EXIT_FAILURE); + } + time_arg = argv[optind]; + if (*time_arg == '+') + time_arg++; + if (strcasecmp(time_arg, "now") == 0) + strcpy(time_arg, "0"); + for (ch=time_arg; *ch; ch++) + if ((*ch < '0' || *ch > '9') && *ch != ':') { + eerror("%s: invalid time %s", applet, time_arg); + usage(EXIT_FAILURE); + } + if (strchr(time_arg, ':')) { + if ((sscanf(time_arg, "%2d:%2d", &hour, &min) != 2) || + (hour > 23) || (min > 59)) { + eerror("%s: invalid time %s", applet, time_arg); + usage(EXIT_FAILURE); + } + time(&tv); + lt = localtime(&tv); + shutdown_delay = (hour * 60 + min) - (lt->tm_hour * 60 + lt->tm_min); + if (shutdown_delay < 0) + shutdown_delay += 1440; + } else { + shutdown_delay = atoi(time_arg); + } + + fp = fopen(shutdown_pid, "w"); + if (!fp) + eerrorx("%s: fopen `%s': %s", applet, shutdown_pid, strerror(errno)); + fprintf(fp, "%d\n", getpid()); + fclose(fp); + + openlog(applet, LOG_PID, LOG_DAEMON); + memset(&sa, 0, sizeof(sa)); + sa.sa_handler = stop_shutdown; + sigemptyset(&sa.sa_mask); + sigaction(SIGINT, &sa, NULL); + sigaction(SIGTERM, &sa, NULL); + while (shutdown_delay > 0) { + need_warning = false; + if (shutdown_delay > 180) + need_warning = (shutdown_delay % 60 == 0); + else if (shutdown_delay > 60) + need_warning = (shutdown_delay % 30 == 0); + else if (shutdown_delay > 10) + need_warning = (shutdown_delay % 15 == 0); + else + need_warning = true; + if (shutdown_delay <= 5) + create_nologin(shutdown_delay); + if (need_warning) { + xasprintf(&msg, "\rThe system will %s in %d minutes\r\n", + state, shutdown_delay); + broadcast(msg); + free(msg); + } + sleep_no_interrupt(60); + shutdown_delay--; + } + xasprintf(&msg, "\rThe system will %s now\r\n", state); + broadcast(msg); + syslog(LOG_NOTICE, "The system will %s now", state); + unlink(nologin_file); + unlink(shutdown_pid); if (do_halt) send_cmd("halt"); else if (do_kexec) @@ -160,11 +327,9 @@ int main(int argc, char **argv) send_cmd("poweroff"); else if (do_reboot) send_cmd("reboot"); - else if (do_reexec) - send_cmd("reexec"); - else if (do_wtmp_only) - log_wtmp("shutdown", "~~", 0, RUN_LVL, "~~"); else if (do_single) send_cmd("single"); + else if (do_wtmp_only) + log_wtmp("shutdown", "~~", 0, RUN_LVL, "~~"); return 0; }