[Top][All Lists]
[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]
Re: [Qemu-devel] [PATCH 2/2] qemu-ga: Add the guest-suspend command
From: |
Luiz Capitulino |
Subject: |
Re: [Qemu-devel] [PATCH 2/2] qemu-ga: Add the guest-suspend command |
Date: |
Tue, 17 Jan 2012 10:22:59 -0200 |
On Mon, 16 Jan 2012 16:17:34 -0600
Michael Roth <address@hidden> wrote:
> On 01/16/2012 02:09 PM, Luiz Capitulino wrote:
> > 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 the given mode is supported. The sleep and hybrid
> > modes are only supported in QEMU 1.1 and later though, because
> > QEMU's S3 support is broken 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 is
> > used.
> >
> > To reap terminated children, a new signal handler is installed in
> > the parent to catch SIGCHLD signals and a non-blocking call to
> > waitpid() is done to collect their exit statuses. The statuses,
> > however, are discarded.
> >
> > 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 resulting 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. The approach explained
> > above avoids that complexity.
> >
> > Signed-off-by: Luiz Capitulino<address@hidden>
> > ---
> > qapi-schema-guest.json | 32 ++++++
> > qemu-ga.c | 18 +++-
> > qga/guest-agent-commands.c | 263
> > ++++++++++++++++++++++++++++++++++++++++++++
> > 3 files changed, 312 insertions(+), 1 deletions(-)
> >
> > diff --git a/qapi-schema-guest.json b/qapi-schema-guest.json
> > index 5f8a18d..7dd9267 100644
> > --- a/qapi-schema-guest.json
> > +++ b/qapi-schema-guest.json
> > @@ -219,3 +219,35 @@
> > ##
> > { '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
> > +#
> > +# Returns: nothing on success
> > +# If @mode is not supported by the guest, Unsupported
> > +#
> > +# 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<getopt.h>
> > #include<termios.h>
> > #include<syslog.h>
> > +#include<sys/wait.h>
> > #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..e64b0e0 100644
> > --- a/qga/guest-agent-commands.c
> > +++ b/qga/guest-agent-commands.c
> > @@ -23,6 +23,7 @@
> >
> > #include<sys/types.h>
> > #include<sys/ioctl.h>
> > +#include<sys/wait.h>
> > #include "qga/guest-agent-core.h"
> > #include "qga-qmp-commands.h"
> > #include "qerror.h"
> > @@ -44,6 +45,54 @@ static void slog(const char *fmt, ...)
> > va_end(ap);
> > }
> >
> > +static void reopen_fd_to_null(int fd)
> > +{
> > + int nullfd;
> > +
> > + nullfd = open("/dev/null", O_RDWR);
> > + if (nullfd< 0) {
> > + return;
> > + }
> > +
> > + dup2(nullfd, fd);
> > +
> > + if (nullfd != fd) {
> > + close(nullfd);
> > + }
> > +}
> > +
> > +/* Try to find executable file 'file'. If it's found, its absolute path is
> > + returned in 'abs_path' and the function returns true. If it's not found,
> > + the function will return false and 'abs_path' will contain zeros */
> > +static bool find_executable_file(const char *file, char *abs_path, size_t
> > len)
> > +{
> > + char *path, *saveptr;
> > + const char *token, *delim = ":";
> > +
> > + if (!getenv("PATH")) {
> > + return false;
> > + }
> > +
> > + path = g_strdup(getenv("PATH"));
> > + if (!path) {
> > + return false;
> > + }
> > +
> > + for (token = strtok_r(path, delim,&saveptr); token;
> > + token = strtok_r(NULL, delim,&saveptr)) {
> > + snprintf(abs_path, len, "%s/%s", token, file);
> > + if (access(abs_path, X_OK) == 0) {
> > + g_free(path);
> > + return true;
> > + }
> > + }
> > +
> > + g_free(path);
> > + memset(abs_path, 0, len);
> > +
> > + return false;
> > +}
> > +
> > int64_t qmp_guest_sync(int64_t id, Error **errp)
> > {
> > return id;
> > @@ -574,6 +623,220 @@ 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;
> > + bool has_pmutils;
> > + int status, pipefds[2];
> > + char pmutils_path[PATH_MAX];
> > + const char *pmutils_bin = "pm-is-supported";
> > +
> > + if (pipe(pipefds)< 0) {
> > + error_set(err, QERR_UNDEFINED_ERROR);
> > + return false;
> > + }
> > +
> > + has_pmutils = find_executable_file(pmutils_bin, pmutils_path,
> > + sizeof(pmutils_path));
> > +
> > + 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) {
> > + int fd;
> > + char buf[32]; /* hopefully big enough */
> > + const char *arg;
> > +
> > + 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);
> > + }
> > +
> > + if (has_pmutils) {
> > + execle(pmutils_path, pmutils_bin, arg, NULL, environ);
> > + }
> > +
> > + /*
> > + * If we get here either pm-utils is not installed or
> > execle() has
> > + * failed. Let's try the manual approach if mode is not hybrid
> > (as
> > + * it's only supported by pm-utils)
> > + */
> > +
> > + if (strcmp(mode, "hybrid") == 0) {
> > + _exit(SUS_MODE_NOT_SUPPORTED);
> > + }
> > +
> > + fd = open(LINUX_SYS_STATE_FILE, O_RDONLY);
> > + if (fd< 0) {
> > + _exit(SUS_MODE_NOT_SUPPORTED);
> > + }
> > +
> > + ret = read(fd, buf, sizeof(buf));
> > + if (ret<= 0) {
> > + _exit(SUS_MODE_NOT_SUPPORTED);
> > + }
> > +
> > + buf[ret] = '\0';
> > + 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(EXIT_FAILURE);
> > + }
> > +
> > + _exit(EXIT_SUCCESS);
> > + }
> > +
> > + close(pipefds[1]);
> > +
> > + if (pid> 0) {
> > + ret = read(pipefds[0],&status, sizeof(status));
> > + if (ret == sizeof(status)&& WIFEXITED(status)&&
> > + WEXITSTATUS(status) == SUS_MODE_SUPPORTED) {
> > + close(pipefds[0]);
> > + return true;
> > + }
> > + }
> > +
> > + close(pipefds[0]);
> > + 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;
> > + bool has_pmutils;
> > + Error *local_err = NULL;
> > + const char *pmutils_bin;
> > + char pmutils_path[PATH_MAX];
> > +
> > + 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;
> > + }
> > +
> > + has_pmutils = find_executable_file(pmutils_bin, pmutils_path,
> > + sizeof(pmutils_path));
> > +
> > + 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);
> > +
> > + if (has_pmutils) {
> > + execle(pmutils_path, pmutils_bin, NULL, environ);
> > + }
> > +
> > + /*
> > + * If we get here either pm-utils is not installed or execle() has
> > + * failed. Let's try the manual approach if mode is not hybrid (as
> > + * it's only supported by pm-utils)
> > + */
> > +
> > + slog("could not execute %s\n", pmutils_bin);
>
> slog() is actually a wrapper around syslog(), so I guess we need to
> scrap these in light of the previous discussion. shutdown() has the same
> problem though...
>
> So maybe, just leave these in, and I'll follow up with a patch that logs
> to a separate file or something if pid != PARENT_PID.
Yes, what I had in mind was to leave them there and fix slog() in parallel.
But as we're putting a big effort on making qmp_guest_suspend() as safe as
possible, I think I'll drop them for now and only add them back when they
get fixed.
>
> > + if (strcmp(mode, "hybrid") == 0) {
> > + _exit(EXIT_FAILURE);
> > + }
> > +
> > + 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(EXIT_FAILURE);
> > + }
> > +
> > + cmd = strcmp(mode, "sleep") == 0 ? "mem" : "disk";
> > + if (write(fd, cmd, strlen(cmd))< 0) {
> > + slog("failued to write to %s\n", LINUX_SYS_STATE_FILE);
> > + _exit(EXIT_FAILURE);
> > + }
> > +
> > + _exit(EXIT_SUCCESS);
> > + } 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)
> > {
>