From patchwork Fri Jan 13 19:15:22 2012 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Luiz Capitulino X-Patchwork-Id: 135976 Return-Path: X-Original-To: incoming@patchwork.ozlabs.org Delivered-To: patchwork-incoming@bilbo.ozlabs.org Received: from lists.gnu.org (lists.gnu.org [140.186.70.17]) (using TLSv1 with cipher AES256-SHA (256/256 bits)) (Client did not present a certificate) by ozlabs.org (Postfix) with ESMTPS id 08794B6EF1 for ; Sat, 14 Jan 2012 06:16:10 +1100 (EST) Received: from localhost ([::1]:37879 helo=lists.gnu.org) by lists.gnu.org with esmtp (Exim 4.71) (envelope-from ) id 1RlmbV-00010i-AD for incoming@patchwork.ozlabs.org; Fri, 13 Jan 2012 14:15:57 -0500 Received: from eggs.gnu.org ([140.186.70.92]:50045) by lists.gnu.org with esmtp (Exim 4.71) (envelope-from ) id 1RlmbH-0000iY-QD for qemu-devel@nongnu.org; Fri, 13 Jan 2012 14:15:49 -0500 Received: from Debian-exim by eggs.gnu.org with spam-scanned (Exim 4.71) (envelope-from ) id 1RlmbC-0007m8-OC for qemu-devel@nongnu.org; Fri, 13 Jan 2012 14:15:43 -0500 Received: from mx1.redhat.com ([209.132.183.28]:59655) by eggs.gnu.org with esmtp (Exim 4.71) (envelope-from ) id 1RlmbC-0007m3-7q for qemu-devel@nongnu.org; Fri, 13 Jan 2012 14:15:38 -0500 Received: from int-mx12.intmail.prod.int.phx2.redhat.com (int-mx12.intmail.prod.int.phx2.redhat.com [10.5.11.25]) by mx1.redhat.com (8.14.4/8.14.4) with ESMTP id q0DJFaJS020354 (version=TLSv1/SSLv3 cipher=DHE-RSA-AES256-SHA bits=256 verify=OK); Fri, 13 Jan 2012 14:15:36 -0500 Received: from localhost (ovpn-116-63.ams2.redhat.com [10.36.116.63]) by int-mx12.intmail.prod.int.phx2.redhat.com (8.14.4/8.14.4) with ESMTP id q0DJFYPc010100; Fri, 13 Jan 2012 14:15:35 -0500 From: Luiz Capitulino To: qemu-devel@nongnu.org Date: Fri, 13 Jan 2012 17:15:22 -0200 Message-Id: <1326482122-12619-3-git-send-email-lcapitulino@redhat.com> In-Reply-To: <1326482122-12619-1-git-send-email-lcapitulino@redhat.com> References: <1326482122-12619-1-git-send-email-lcapitulino@redhat.com> X-Scanned-By: MIMEDefang 2.68 on 10.5.11.25 X-detected-operating-system: by eggs.gnu.org: Genre and OS details not recognized. X-Received-From: 209.132.183.28 Cc: eblake@redhat.com, jcody@redhat.com, mdroth@linux.vnet.ibm.com Subject: [Qemu-devel] [PATCH 2/2] qemu-ga: Add the guest-suspend command X-BeenThere: qemu-devel@nongnu.org X-Mailman-Version: 2.1.14 Precedence: list List-Id: List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: qemu-devel-bounces+incoming=patchwork.ozlabs.org@nongnu.org Sender: qemu-devel-bounces+incoming=patchwork.ozlabs.org@nongnu.org The guest-suspend command supports three modes: o hibernate (suspend to disk) o sleep (suspend to ram) o hybrid (save RAM contents to disk, but suspend instead of powering off) Before trying to suspend, the command queries the guest in order to know whether a given mode is supported. The sleep and hybrid modes are only supported in QEMU 1.1 and later though, because QEMU would adverstise broken S3 support in previous versions. The guest-suspend command will use the scripts provided by the pm-utils package if they are available. If they aren't, a manual process which directly writes to the "/sys/power/state" file will be tried. To reap terminated children, a new signal handler is installed to catch SIGCHLD signals and a non-blocking call to waitpid() is done to collect their exit statuses. The approach used to query the guest for suspend support deserves some explanation. It's implemented by bios_supports_mode() and shown below: qemu-ga | create pipe | fork() ----------------- | | | | | fork() | -------------------------- | | | | | | | | exec('pm-is-supported') | | | wait() | write exit status to pipe | exit | read pipe This might look complex, but the final code is quite simple. The purpose of that approach is to allow qemu-ga to reap its children (semi-)automatically from its SIGCHLD handler. Implementing this the obvious way, that's, doing the exec() call from the first child process, would force us to introduce a more complex way to reap qemu-ga's children. Like registering PIDs to be reaped and having a way to wait for them when returning their exit status to qemu-ga is necessary. Signed-off-by: Luiz Capitulino --- qapi-schema-guest.json | 29 ++++++ qemu-ga.c | 18 ++++- qga/guest-agent-commands.c | 212 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 258 insertions(+), 1 deletions(-) diff --git a/qapi-schema-guest.json b/qapi-schema-guest.json index 5f8a18d..6a0605b 100644 --- a/qapi-schema-guest.json +++ b/qapi-schema-guest.json @@ -219,3 +219,32 @@ ## { 'command': 'guest-fsfreeze-thaw', 'returns': 'int' } + +## +# @guest-suspend +# +# Suspend guest execution by entering ACPI power state S3 or S4. +# +# This command tries to execute the scripts provided by the pm-utils +# package. If they are not available, it will perform the suspend +# operation by manually writing to a sysfs file. +# +# For the best results it's strongly recommended to have the pm-utils +# package installed in the guest. +# +# @mode: 'hibernate' RAM content is saved to the disk and the guest is +# powered off (this corresponds to ACPI S4) +# 'sleep' execution is suspended but the RAM retains its contents +# (this corresponds to ACPI S3) +# 'hybrid' RAM content is saved to the disk but the guest is +# suspended instead of powering off +# +# Notes: o This is an asynchronous request. There's no guarantee a response +# will be sent. +# o Errors will be logged to guest's syslog. +# o It's strongly recommended to issue the guest-sync command before +# sending commands when the guest resumes. +# +# Since: 1.1 +## +{ 'command': 'guest-suspend', 'data': { 'mode': 'str' } } diff --git a/qemu-ga.c b/qemu-ga.c index 647df82..f531084 100644 --- a/qemu-ga.c +++ b/qemu-ga.c @@ -17,6 +17,7 @@ #include #include #include +#include #include "qemu_socket.h" #include "json-streamer.h" #include "json-parser.h" @@ -59,9 +60,16 @@ static void quit_handler(int sig) } } +/* reap _all_ terminated children */ +static void child_handler(int sig) +{ + int status; + while (waitpid(-1, &status, WNOHANG) > 0) /* NOTHING */; +} + static void register_signal_handlers(void) { - struct sigaction sigact; + struct sigaction sigact, sigact_chld; int ret; memset(&sigact, 0, sizeof(struct sigaction)); @@ -76,6 +84,14 @@ static void register_signal_handlers(void) if (ret == -1) { g_error("error configuring signal handler: %s", strerror(errno)); } + + memset(&sigact_chld, 0, sizeof(struct sigaction)); + sigact_chld.sa_handler = child_handler; + sigact_chld.sa_flags = SA_NOCLDSTOP; + ret = sigaction(SIGCHLD, &sigact_chld, NULL); + if (ret == -1) { + g_error("error configuring signal handler: %s", strerror(errno)); + } } static void usage(const char *cmd) diff --git a/qga/guest-agent-commands.c b/qga/guest-agent-commands.c index a09c8ca..963270c 100644 --- a/qga/guest-agent-commands.c +++ b/qga/guest-agent-commands.c @@ -23,6 +23,7 @@ #include #include +#include #include "qga/guest-agent-core.h" #include "qga-qmp-commands.h" #include "qerror.h" @@ -44,6 +45,21 @@ static void slog(const char *fmt, ...) va_end(ap); } +static int reopen_fd_to_null(int fd) +{ + int ret, nullfd; + + nullfd = open("/dev/null", O_RDWR); + if (nullfd < 0) { + return -1; + } + + ret = dup2(nullfd, fd); + close(nullfd); + + return ret; +} + int64_t qmp_guest_sync(int64_t id, Error **errp) { return id; @@ -574,6 +590,202 @@ int64_t qmp_guest_fsfreeze_thaw(Error **err) } #endif +#define LINUX_SYS_STATE_FILE "/sys/power/state" +#define SUS_MODE_SUPPORTED 0 +#define SUS_MODE_NOT_SUPPORTED 1 + +/** + * This function forks twice and the information about the mode support + * status is passed to the qemu-ga process via a pipe. + * + * This approach allows us to keep the way we reap terminated children + * in qemu-ga quite simple. + */ +static bool bios_supports_mode(const char *mode, Error **err) +{ + pid_t pid; + ssize_t ret; + int status, pipefds[2]; + + if (pipe(pipefds) < 0) { + error_set(err, QERR_UNDEFINED_ERROR); + return false; + } + + pid = fork(); + if (!pid) { + struct sigaction act; + + memset(&act, 0, sizeof(act)); + act.sa_handler = SIG_DFL; + sigaction(SIGCHLD, &act, NULL); + + setsid(); + close(pipefds[0]); + reopen_fd_to_null(0); + reopen_fd_to_null(1); + reopen_fd_to_null(2); + + pid = fork(); + if (!pid) { + char buf[32]; + FILE *sysfile; + const char *arg; + const char *pmutils_bin = "pm-is-supported"; + + if (strcmp(mode, "hibernate") == 0) { + arg = "--hibernate"; + } else if (strcmp(mode, "sleep") == 0) { + arg = "--suspend"; + } else if (strcmp(mode, "hybrid") == 0) { + arg = "--suspend-hybrid"; + } else { + _exit(SUS_MODE_NOT_SUPPORTED); + } + + execlp(pmutils_bin, pmutils_bin, arg, NULL); + + /* + * If we get here execlp() has failed. Let's try the manual + * approach if mode is not hybrid (as it's only suported by + * pm-utils) + */ + + if (strcmp(mode, "hybrid") == 0) { + _exit(SUS_MODE_NOT_SUPPORTED); + } + + sysfile = fopen(LINUX_SYS_STATE_FILE, "r"); + if (!sysfile) { + _exit(SUS_MODE_NOT_SUPPORTED); + } + + if (!fgets(buf, sizeof(buf), sysfile)) { + _exit(SUS_MODE_NOT_SUPPORTED); + } + + if (strcmp(mode, "hibernate") == 0 && strstr(buf, "disk")) { + _exit(SUS_MODE_SUPPORTED); + } else if (strcmp(mode, "sleep") == 0 && strstr(buf, "mem")) { + _exit(SUS_MODE_SUPPORTED); + } + + _exit(SUS_MODE_NOT_SUPPORTED); + } + + if (pid > 0) { + wait(&status); + } else { + status = SUS_MODE_NOT_SUPPORTED; + } + + ret = write(pipefds[1], &status, sizeof(status)); + if (ret != sizeof(status)) { + _exit(1); + } + + _exit(0); + } + + ret = read(pipefds[0], &status, sizeof(status)); + close(pipefds[0]); + close(pipefds[1]); + + if (ret == sizeof(status) && WIFEXITED(status) && + WEXITSTATUS(status) == SUS_MODE_SUPPORTED) { + return true; + } + + return false; +} + +static bool host_supports_mode(const char *mode) +{ + if (strcmp(mode, "hibernate")) { + /* sleep & hybrid are only supported in qemu 1.1.0 and above */ + return ga_has_support_level(1, 1, 0); + } + return true; +} + +void qmp_guest_suspend(const char *mode, Error **err) +{ + pid_t pid; + const char *pmutils_bin; + Error *local_err = NULL; + + if (strcmp(mode, "hibernate") == 0) { + pmutils_bin = "pm-hibernate"; + } else if (strcmp(mode, "sleep") == 0) { + pmutils_bin = "pm-suspend"; + } else if (strcmp(mode, "hybrid") == 0) { + pmutils_bin = "pm-suspend-hybrid"; + } else { + error_set(err, QERR_INVALID_PARAMETER, "mode"); + return; + } + + if (!host_supports_mode(mode)) { + error_set(err, QERR_UNSUPPORTED); + return; + } + + if (!bios_supports_mode(mode, &local_err)) { + if (error_is_set(&local_err)) { + error_propagate(err, local_err); + } else { + error_set(err, QERR_UNSUPPORTED); + } + return; + } + + pid = fork(); + if (pid == 0) { + /* child */ + int fd; + const char *cmd; + + setsid(); + reopen_fd_to_null(0); + reopen_fd_to_null(1); + reopen_fd_to_null(2); + + execlp(pmutils_bin, pmutils_bin, NULL); + + /* + * If we get here execlp() has failed. Let's try the manual + * approach if mode is not hybrid (as it's only suported by + * pm-utils) + */ + + slog("could not execute %s: %s\n", pmutils_bin, strerror(errno)); + if (strcmp(mode, "hybrid") == 0) { + _exit(1); + } + + slog("trying to suspend using the manual method...\n"); + + fd = open(LINUX_SYS_STATE_FILE, O_WRONLY); + if (fd < 0) { + slog("can't open file %s: %s\n", LINUX_SYS_STATE_FILE, + strerror(errno)); + _exit(1); + } + + cmd = strcmp(mode, "sleep") == 0 ? "mem" : "disk"; + if (write(fd, cmd, strlen(cmd)) < 0) { + slog("can't write to %s: %s\n", LINUX_SYS_STATE_FILE, + strerror(errno)); + _exit(1); + } + + _exit(0); + } else if (pid < 0) { + error_set(err, QERR_UNDEFINED_ERROR); + return; + } +} + /* register init/cleanup routines for stateful command groups */ void ga_command_state_init(GAState *s, GACommandState *cs) {