diff options
author | Junfeng Dong <junfeng.dong@intel.com> | 2013-11-19 17:45:23 +0800 |
---|---|---|
committer | Junfeng Dong <junfeng.dong@intel.com> | 2013-11-19 17:45:23 +0800 |
commit | 340f06c9eaee097e626c251bf7a013350649c091 (patch) | |
tree | 107e5705050a12da68fc80a56ae37afd50a2cc94 /qga | |
parent | 42bf3037d458a330856a0be584200c1e41c3f417 (diff) | |
download | qemu-340f06c9eaee097e626c251bf7a013350649c091.tar.gz qemu-340f06c9eaee097e626c251bf7a013350649c091.tar.bz2 qemu-340f06c9eaee097e626c251bf7a013350649c091.zip |
Import upstream 1.6.0.upstream/1.6.0
Change-Id: Icf52b556470cac8677297f2ef14ded16684f7887
Signed-off-by: Junfeng Dong <junfeng.dong@intel.com>
Diffstat (limited to 'qga')
-rw-r--r-- | qga/Makefile.objs | 2 | ||||
-rw-r--r-- | qga/channel-posix.c | 21 | ||||
-rw-r--r-- | qga/channel-win32.c | 4 | ||||
-rw-r--r-- | qga/commands-posix.c | 750 | ||||
-rw-r--r-- | qga/commands-win32.c | 75 | ||||
-rw-r--r-- | qga/commands.c | 4 | ||||
-rw-r--r-- | qga/guest-agent-core.h | 4 | ||||
-rw-r--r-- | qga/main.c | 1195 | ||||
-rw-r--r-- | qga/qapi-schema.json | 640 | ||||
-rw-r--r-- | qga/service-win32.c | 118 | ||||
-rw-r--r-- | qga/service-win32.h | 3 |
11 files changed, 2646 insertions, 170 deletions
diff --git a/qga/Makefile.objs b/qga/Makefile.objs index cd3e13516..b8d7cd0a4 100644 --- a/qga/Makefile.objs +++ b/qga/Makefile.objs @@ -1,4 +1,4 @@ -qga-obj-y = commands.o guest-agent-command-state.o +qga-obj-y = commands.o guest-agent-command-state.o main.o qga-obj-$(CONFIG_POSIX) += commands-posix.o channel-posix.o qga-obj-$(CONFIG_WIN32) += commands-win32.o channel-win32.o service-win32.o qga-obj-y += qapi-generated/qga-qapi-types.o qapi-generated/qga-qapi-visit.o diff --git a/qga/channel-posix.c b/qga/channel-posix.c index d152827bc..e65dda382 100644 --- a/qga/channel-posix.c +++ b/qga/channel-posix.c @@ -1,6 +1,12 @@ #include <glib.h> #include <termios.h> -#include "qemu_socket.h" +#include <errno.h> +#include <unistd.h> +#include <fcntl.h> +#include <stdlib.h> +#include <string.h> +#include "qemu/osdep.h" +#include "qemu/sockets.h" #include "qga/channel.h" #ifdef CONFIG_SOLARIS @@ -40,6 +46,7 @@ static gboolean ga_channel_listen_accept(GIOChannel *channel, ret = ga_channel_client_add(c, client_fd); if (ret) { g_warning("error setting up connection"); + close(client_fd); goto out; } accepted = true; @@ -134,19 +141,21 @@ static gboolean ga_channel_open(GAChannel *c, const gchar *path, GAChannelMethod ); if (fd == -1) { g_critical("error opening channel: %s", strerror(errno)); - exit(EXIT_FAILURE); + return false; } #ifdef CONFIG_SOLARIS ret = ioctl(fd, I_SETSIG, S_OUTPUT | S_INPUT | S_HIPRI); if (ret == -1) { g_critical("error setting event mask for channel: %s", strerror(errno)); - exit(EXIT_FAILURE); + close(fd); + return false; } #endif ret = ga_channel_client_add(c, fd); if (ret) { g_critical("error adding channel to main loop"); + close(fd); return false; } break; @@ -156,7 +165,7 @@ static gboolean ga_channel_open(GAChannel *c, const gchar *path, GAChannelMethod int fd = qemu_open(path, O_RDWR | O_NOCTTY | O_NONBLOCK); if (fd == -1) { g_critical("error opening channel: %s", strerror(errno)); - exit(EXIT_FAILURE); + return false; } tcgetattr(fd, &tio); /* set up serial port for non-canonical, dumb byte streaming */ @@ -176,7 +185,9 @@ static gboolean ga_channel_open(GAChannel *c, const gchar *path, GAChannelMethod tcsetattr(fd, TCSANOW, &tio); ret = ga_channel_client_add(c, fd); if (ret) { - g_error("error adding channel to main loop"); + g_critical("error adding channel to main loop"); + close(fd); + return false; } break; } diff --git a/qga/channel-win32.c b/qga/channel-win32.c index 16bf44a37..8a303f35e 100644 --- a/qga/channel-win32.c +++ b/qga/channel-win32.c @@ -268,7 +268,7 @@ static GIOStatus ga_channel_write(GAChannel *c, const char *buf, size_t size, GIOStatus ga_channel_write_all(GAChannel *c, const char *buf, size_t size) { - GIOStatus status = G_IO_STATUS_NORMAL;; + GIOStatus status = G_IO_STATUS_NORMAL; size_t count; while (size) { @@ -287,7 +287,7 @@ GIOStatus ga_channel_write_all(GAChannel *c, const char *buf, size_t size) static gboolean ga_channel_open(GAChannel *c, GAChannelMethod method, const gchar *path) { - if (!method == GA_CHANNEL_VIRTIO_SERIAL) { + if (method != GA_CHANNEL_VIRTIO_SERIAL) { g_critical("unsupported communication method"); return false; } diff --git a/qga/commands-posix.c b/qga/commands-posix.c index 726930a90..e199738c7 100644 --- a/qga/commands-posix.c +++ b/qga/commands-posix.c @@ -15,11 +15,18 @@ #include <sys/types.h> #include <sys/ioctl.h> #include <sys/wait.h> +#include <unistd.h> +#include <errno.h> +#include <fcntl.h> +#include <stdio.h> +#include <string.h> +#include <sys/stat.h> +#include <inttypes.h> #include "qga/guest-agent-core.h" #include "qga-qmp-commands.h" -#include "qerror.h" -#include "qemu-queue.h" -#include "host-utils.h" +#include "qapi/qmp/qerror.h" +#include "qemu/queue.h" +#include "qemu/host-utils.h" #ifndef CONFIG_HAS_ENVIRON #ifdef __APPLE__ @@ -46,10 +53,29 @@ extern char **environ; #endif #endif +static void ga_wait_child(pid_t pid, int *status, Error **err) +{ + pid_t rpid; + + *status = 0; + + do { + rpid = waitpid(pid, status, 0); + } while (rpid == -1 && errno == EINTR); + + if (rpid == -1) { + error_setg_errno(err, errno, "failed to wait for child (pid: %d)", pid); + return; + } + + g_assert(rpid == pid); +} + void qmp_guest_shutdown(bool has_mode, const char *mode, Error **err) { const char *shutdown_flag; - pid_t rpid, pid; + Error *local_err = NULL; + pid_t pid; int status; slog("guest-shutdown called, mode: %s", mode); @@ -60,8 +86,8 @@ void qmp_guest_shutdown(bool has_mode, const char *mode, Error **err) } else if (strcmp(mode, "reboot") == 0) { shutdown_flag = "-r"; } else { - error_set(err, QERR_INVALID_PARAMETER_VALUE, "mode", - "halt|powerdown|reboot"); + error_setg(err, + "mode is invalid (valid values are: halt|powerdown|reboot"); return; } @@ -77,18 +103,98 @@ void qmp_guest_shutdown(bool has_mode, const char *mode, Error **err) "hypervisor initiated shutdown", (char*)NULL, environ); _exit(EXIT_FAILURE); } else if (pid < 0) { - goto exit_err; + error_setg_errno(err, errno, "failed to create child process"); + return; } - do { - rpid = waitpid(pid, &status, 0); - } while (rpid == -1 && errno == EINTR); - if (rpid == pid && WIFEXITED(status) && !WEXITSTATUS(status)) { + ga_wait_child(pid, &status, &local_err); + if (error_is_set(&local_err)) { + error_propagate(err, local_err); + return; + } + + if (!WIFEXITED(status)) { + error_setg(err, "child process has terminated abnormally"); + return; + } + + if (WEXITSTATUS(status)) { + error_setg(err, "child process has failed to shutdown"); return; } -exit_err: - error_set(err, QERR_UNDEFINED_ERROR); + /* succeeded */ +} + +int64_t qmp_guest_get_time(Error **errp) +{ + int ret; + qemu_timeval tq; + int64_t time_ns; + + ret = qemu_gettimeofday(&tq); + if (ret < 0) { + error_setg_errno(errp, errno, "Failed to get time"); + return -1; + } + + time_ns = tq.tv_sec * 1000000000LL + tq.tv_usec * 1000; + return time_ns; +} + +void qmp_guest_set_time(int64_t time_ns, Error **errp) +{ + int ret; + int status; + pid_t pid; + Error *local_err = NULL; + struct timeval tv; + + /* year-2038 will overflow in case time_t is 32bit */ + if (time_ns / 1000000000 != (time_t)(time_ns / 1000000000)) { + error_setg(errp, "Time %" PRId64 " is too large", time_ns); + return; + } + + tv.tv_sec = time_ns / 1000000000; + tv.tv_usec = (time_ns % 1000000000) / 1000; + + ret = settimeofday(&tv, NULL); + if (ret < 0) { + error_setg_errno(errp, errno, "Failed to set time to guest"); + return; + } + + /* Set the Hardware Clock to the current System Time. */ + pid = fork(); + if (pid == 0) { + setsid(); + reopen_fd_to_null(0); + reopen_fd_to_null(1); + reopen_fd_to_null(2); + + execle("/sbin/hwclock", "hwclock", "-w", NULL, environ); + _exit(EXIT_FAILURE); + } else if (pid < 0) { + error_setg_errno(errp, errno, "failed to create child process"); + return; + } + + ga_wait_child(pid, &status, &local_err); + if (error_is_set(&local_err)) { + error_propagate(errp, local_err); + return; + } + + if (!WIFEXITED(status)) { + error_setg(errp, "child process has terminated abnormally"); + return; + } + + if (WEXITSTATUS(status)) { + error_setg(errp, "hwclock failed to set hardware clock to system time"); + return; + } } typedef struct GuestFileHandle { @@ -101,17 +207,25 @@ static struct { QTAILQ_HEAD(, GuestFileHandle) filehandles; } guest_file_state; -static void guest_file_handle_add(FILE *fh) +static int64_t guest_file_handle_add(FILE *fh, Error **errp) { GuestFileHandle *gfh; + int64_t handle; + + handle = ga_get_fd_handle(ga_state, errp); + if (error_is_set(errp)) { + return 0; + } gfh = g_malloc0(sizeof(GuestFileHandle)); - gfh->id = fileno(fh); + gfh->id = handle; gfh->fh = fh; QTAILQ_INSERT_TAIL(&guest_file_state.filehandles, gfh, next); + + return handle; } -static GuestFileHandle *guest_file_handle_find(int64_t id) +static GuestFileHandle *guest_file_handle_find(int64_t id, Error **err) { GuestFileHandle *gfh; @@ -122,22 +236,149 @@ static GuestFileHandle *guest_file_handle_find(int64_t id) } } + error_setg(err, "handle '%" PRId64 "' has not been found", id); + return NULL; +} + +typedef const char * const ccpc; + +#ifndef O_BINARY +#define O_BINARY 0 +#endif + +/* http://pubs.opengroup.org/onlinepubs/9699919799/functions/fopen.html */ +static const struct { + ccpc *forms; + int oflag_base; +} guest_file_open_modes[] = { + { (ccpc[]){ "r", NULL }, O_RDONLY }, + { (ccpc[]){ "rb", NULL }, O_RDONLY | O_BINARY }, + { (ccpc[]){ "w", NULL }, O_WRONLY | O_CREAT | O_TRUNC }, + { (ccpc[]){ "wb", NULL }, O_WRONLY | O_CREAT | O_TRUNC | O_BINARY }, + { (ccpc[]){ "a", NULL }, O_WRONLY | O_CREAT | O_APPEND }, + { (ccpc[]){ "ab", NULL }, O_WRONLY | O_CREAT | O_APPEND | O_BINARY }, + { (ccpc[]){ "r+", NULL }, O_RDWR }, + { (ccpc[]){ "rb+", "r+b", NULL }, O_RDWR | O_BINARY }, + { (ccpc[]){ "w+", NULL }, O_RDWR | O_CREAT | O_TRUNC }, + { (ccpc[]){ "wb+", "w+b", NULL }, O_RDWR | O_CREAT | O_TRUNC | O_BINARY }, + { (ccpc[]){ "a+", NULL }, O_RDWR | O_CREAT | O_APPEND }, + { (ccpc[]){ "ab+", "a+b", NULL }, O_RDWR | O_CREAT | O_APPEND | O_BINARY } +}; + +static int +find_open_flag(const char *mode_str, Error **err) +{ + unsigned mode; + + for (mode = 0; mode < ARRAY_SIZE(guest_file_open_modes); ++mode) { + ccpc *form; + + form = guest_file_open_modes[mode].forms; + while (*form != NULL && strcmp(*form, mode_str) != 0) { + ++form; + } + if (*form != NULL) { + break; + } + } + + if (mode == ARRAY_SIZE(guest_file_open_modes)) { + error_setg(err, "invalid file open mode '%s'", mode_str); + return -1; + } + return guest_file_open_modes[mode].oflag_base | O_NOCTTY | O_NONBLOCK; +} + +#define DEFAULT_NEW_FILE_MODE (S_IRUSR | S_IWUSR | \ + S_IRGRP | S_IWGRP | \ + S_IROTH | S_IWOTH) + +static FILE * +safe_open_or_create(const char *path, const char *mode, Error **err) +{ + Error *local_err = NULL; + int oflag; + + oflag = find_open_flag(mode, &local_err); + if (local_err == NULL) { + int fd; + + /* If the caller wants / allows creation of a new file, we implement it + * with a two step process: open() + (open() / fchmod()). + * + * First we insist on creating the file exclusively as a new file. If + * that succeeds, we're free to set any file-mode bits on it. (The + * motivation is that we want to set those file-mode bits independently + * of the current umask.) + * + * If the exclusive creation fails because the file already exists + * (EEXIST is not possible for any other reason), we just attempt to + * open the file, but in this case we won't be allowed to change the + * file-mode bits on the preexistent file. + * + * The pathname should never disappear between the two open()s in + * practice. If it happens, then someone very likely tried to race us. + * In this case just go ahead and report the ENOENT from the second + * open() to the caller. + * + * If the caller wants to open a preexistent file, then the first + * open() is decisive and its third argument is ignored, and the second + * open() and the fchmod() are never called. + */ + fd = open(path, oflag | ((oflag & O_CREAT) ? O_EXCL : 0), 0); + if (fd == -1 && errno == EEXIST) { + oflag &= ~(unsigned)O_CREAT; + fd = open(path, oflag); + } + + if (fd == -1) { + error_setg_errno(&local_err, errno, "failed to open file '%s' " + "(mode: '%s')", path, mode); + } else { + qemu_set_cloexec(fd); + + if ((oflag & O_CREAT) && fchmod(fd, DEFAULT_NEW_FILE_MODE) == -1) { + error_setg_errno(&local_err, errno, "failed to set permission " + "0%03o on new file '%s' (mode: '%s')", + (unsigned)DEFAULT_NEW_FILE_MODE, path, mode); + } else { + FILE *f; + + f = fdopen(fd, mode); + if (f == NULL) { + error_setg_errno(&local_err, errno, "failed to associate " + "stdio stream with file descriptor %d, " + "file '%s' (mode: '%s')", fd, path, mode); + } else { + return f; + } + } + + close(fd); + if (oflag & O_CREAT) { + unlink(path); + } + } + } + + error_propagate(err, local_err); return NULL; } int64_t qmp_guest_file_open(const char *path, bool has_mode, const char *mode, Error **err) { FILE *fh; + Error *local_err = NULL; int fd; - int64_t ret = -1; + int64_t ret = -1, handle; if (!has_mode) { mode = "r"; } slog("guest-file-open called, filepath: %s, mode: %s", path, mode); - fh = fopen(path, mode); - if (!fh) { - error_set(err, QERR_OPEN_FILE_FAILED, path); + fh = safe_open_or_create(path, mode, &local_err); + if (local_err != NULL) { + error_propagate(err, local_err); return -1; } @@ -148,30 +389,35 @@ int64_t qmp_guest_file_open(const char *path, bool has_mode, const char *mode, E ret = fcntl(fd, F_GETFL); ret = fcntl(fd, F_SETFL, ret | O_NONBLOCK); if (ret == -1) { - error_set(err, QERR_QGA_COMMAND_FAILED, "fcntl() failed"); + error_setg_errno(err, errno, "failed to make file '%s' non-blocking", + path); fclose(fh); return -1; } - guest_file_handle_add(fh); - slog("guest-file-open, handle: %d", fd); - return fd; + handle = guest_file_handle_add(fh, err); + if (error_is_set(err)) { + fclose(fh); + return -1; + } + + slog("guest-file-open, handle: %d", handle); + return handle; } void qmp_guest_file_close(int64_t handle, Error **err) { - GuestFileHandle *gfh = guest_file_handle_find(handle); + GuestFileHandle *gfh = guest_file_handle_find(handle, err); int ret; slog("guest-file-close called, handle: %ld", handle); if (!gfh) { - error_set(err, QERR_FD_NOT_FOUND, "handle"); return; } ret = fclose(gfh->fh); - if (ret == -1) { - error_set(err, QERR_QGA_COMMAND_FAILED, "fclose() failed"); + if (ret == EOF) { + error_setg_errno(err, errno, "failed to close handle"); return; } @@ -182,21 +428,21 @@ void qmp_guest_file_close(int64_t handle, Error **err) struct GuestFileRead *qmp_guest_file_read(int64_t handle, bool has_count, int64_t count, Error **err) { - GuestFileHandle *gfh = guest_file_handle_find(handle); + GuestFileHandle *gfh = guest_file_handle_find(handle, err); GuestFileRead *read_data = NULL; guchar *buf; FILE *fh; size_t read_count; if (!gfh) { - error_set(err, QERR_FD_NOT_FOUND, "handle"); return NULL; } if (!has_count) { count = QGA_READ_COUNT_DEFAULT; } else if (count < 0) { - error_set(err, QERR_INVALID_PARAMETER, "count"); + error_setg(err, "value '%" PRId64 "' is invalid for argument count", + count); return NULL; } @@ -204,8 +450,8 @@ struct GuestFileRead *qmp_guest_file_read(int64_t handle, bool has_count, buf = g_malloc0(count+1); read_count = fread(buf, 1, count, fh); if (ferror(fh)) { + error_setg_errno(err, errno, "failed to read file"); slog("guest-file-read failed, handle: %ld", handle); - error_set(err, QERR_QGA_COMMAND_FAILED, "fread() failed"); } else { buf[read_count] = 0; read_data = g_malloc0(sizeof(GuestFileRead)); @@ -228,11 +474,10 @@ GuestFileWrite *qmp_guest_file_write(int64_t handle, const char *buf_b64, guchar *buf; gsize buf_len; int write_count; - GuestFileHandle *gfh = guest_file_handle_find(handle); + GuestFileHandle *gfh = guest_file_handle_find(handle, err); FILE *fh; if (!gfh) { - error_set(err, QERR_FD_NOT_FOUND, "handle"); return NULL; } @@ -242,15 +487,16 @@ GuestFileWrite *qmp_guest_file_write(int64_t handle, const char *buf_b64, if (!has_count) { count = buf_len; } else if (count < 0 || count > buf_len) { + error_setg(err, "value '%" PRId64 "' is invalid for argument count", + count); g_free(buf); - error_set(err, QERR_INVALID_PARAMETER, "count"); return NULL; } write_count = fwrite(buf, 1, count, fh); if (ferror(fh)) { + error_setg_errno(err, errno, "failed to write to file"); slog("guest-file-write failed, handle: %ld", handle); - error_set(err, QERR_QGA_COMMAND_FAILED, "fwrite() error"); } else { write_data = g_malloc0(sizeof(GuestFileWrite)); write_data->count = write_count; @@ -265,20 +511,19 @@ GuestFileWrite *qmp_guest_file_write(int64_t handle, const char *buf_b64, struct GuestFileSeek *qmp_guest_file_seek(int64_t handle, int64_t offset, int64_t whence, Error **err) { - GuestFileHandle *gfh = guest_file_handle_find(handle); + GuestFileHandle *gfh = guest_file_handle_find(handle, err); GuestFileSeek *seek_data = NULL; FILE *fh; int ret; if (!gfh) { - error_set(err, QERR_FD_NOT_FOUND, "handle"); return NULL; } fh = gfh->fh; ret = fseek(fh, offset, whence); if (ret == -1) { - error_set(err, QERR_QGA_COMMAND_FAILED, strerror(errno)); + error_setg_errno(err, errno, "failed to seek file"); } else { seek_data = g_malloc0(sizeof(GuestFileRead)); seek_data->position = ftell(fh); @@ -291,19 +536,18 @@ struct GuestFileSeek *qmp_guest_file_seek(int64_t handle, int64_t offset, void qmp_guest_file_flush(int64_t handle, Error **err) { - GuestFileHandle *gfh = guest_file_handle_find(handle); + GuestFileHandle *gfh = guest_file_handle_find(handle, err); FILE *fh; int ret; if (!gfh) { - error_set(err, QERR_FD_NOT_FOUND, "handle"); return; } fh = gfh->fh; ret = fflush(fh); if (ret == EOF) { - error_set(err, QERR_QGA_COMMAND_FAILED, strerror(errno)); + error_setg_errno(err, errno, "failed to flush file"); } } @@ -343,7 +587,7 @@ static void free_fs_mount_list(FsMountList *mounts) /* * Walk the mount table and build a list of local file systems */ -static int build_fs_mount_list(FsMountList *mounts) +static void build_fs_mount_list(FsMountList *mounts, Error **err) { struct mntent *ment; FsMount *mount; @@ -352,8 +596,8 @@ static int build_fs_mount_list(FsMountList *mounts) fp = setmntent(mtab, "r"); if (!fp) { - g_warning("fsfreeze: unable to read mtab"); - return -1; + error_setg(err, "failed to open mtab file: '%s'", mtab); + return; } while ((ment = getmntent(fp))) { @@ -377,13 +621,71 @@ static int build_fs_mount_list(FsMountList *mounts) } endmntent(fp); - - return 0; } #endif #if defined(CONFIG_FSFREEZE) +typedef enum { + FSFREEZE_HOOK_THAW = 0, + FSFREEZE_HOOK_FREEZE, +} FsfreezeHookArg; + +const char *fsfreeze_hook_arg_string[] = { + "thaw", + "freeze", +}; + +static void execute_fsfreeze_hook(FsfreezeHookArg arg, Error **err) +{ + int status; + pid_t pid; + const char *hook; + const char *arg_str = fsfreeze_hook_arg_string[arg]; + Error *local_err = NULL; + + hook = ga_fsfreeze_hook(ga_state); + if (!hook) { + return; + } + if (access(hook, X_OK) != 0) { + error_setg_errno(err, errno, "can't access fsfreeze hook '%s'", hook); + return; + } + + slog("executing fsfreeze hook with arg '%s'", arg_str); + pid = fork(); + if (pid == 0) { + setsid(); + reopen_fd_to_null(0); + reopen_fd_to_null(1); + reopen_fd_to_null(2); + + execle(hook, hook, arg_str, NULL, environ); + _exit(EXIT_FAILURE); + } else if (pid < 0) { + error_setg_errno(err, errno, "failed to create child process"); + return; + } + + ga_wait_child(pid, &status, &local_err); + if (error_is_set(&local_err)) { + error_propagate(err, local_err); + return; + } + + if (!WIFEXITED(status)) { + error_setg(err, "fsfreeze hook has terminated abnormally"); + return; + } + + status = WEXITSTATUS(status); + if (status) { + error_setg(err, "fsfreeze hook has failed with status %d", status); + return; + } +} + /* * Return status of freeze/thaw */ @@ -405,15 +707,22 @@ int64_t qmp_guest_fsfreeze_freeze(Error **err) int ret = 0, i = 0; FsMountList mounts; struct FsMount *mount; + Error *local_err = NULL; int fd; - char err_msg[512]; slog("guest-fsfreeze called"); + execute_fsfreeze_hook(FSFREEZE_HOOK_FREEZE, &local_err); + if (error_is_set(&local_err)) { + error_propagate(err, local_err); + return -1; + } + QTAILQ_INIT(&mounts); - ret = build_fs_mount_list(&mounts); - if (ret < 0) { - return ret; + build_fs_mount_list(&mounts, &local_err); + if (error_is_set(&local_err)) { + error_propagate(err, local_err); + return -1; } /* cannot risk guest agent blocking itself on a write in this state */ @@ -422,9 +731,7 @@ int64_t qmp_guest_fsfreeze_freeze(Error **err) QTAILQ_FOREACH(mount, &mounts, next) { fd = qemu_open(mount->dirname, O_RDONLY); if (fd == -1) { - sprintf(err_msg, "failed to open %s, %s", mount->dirname, - strerror(errno)); - error_set(err, QERR_QGA_COMMAND_FAILED, err_msg); + error_setg_errno(err, errno, "failed to open %s", mount->dirname); goto error; } @@ -440,9 +747,8 @@ int64_t qmp_guest_fsfreeze_freeze(Error **err) ret = ioctl(fd, FIFREEZE); if (ret == -1) { if (errno != EOPNOTSUPP) { - sprintf(err_msg, "failed to freeze %s, %s", - mount->dirname, strerror(errno)); - error_set(err, QERR_QGA_COMMAND_FAILED, err_msg); + error_setg_errno(err, errno, "failed to freeze %s", + mount->dirname); close(fd); goto error; } @@ -470,12 +776,12 @@ int64_t qmp_guest_fsfreeze_thaw(Error **err) FsMountList mounts; FsMount *mount; int fd, i = 0, logged; + Error *local_err = NULL; QTAILQ_INIT(&mounts); - ret = build_fs_mount_list(&mounts); - if (ret) { - error_set(err, QERR_QGA_COMMAND_FAILED, - "failed to enumerate filesystems"); + build_fs_mount_list(&mounts, &local_err); + if (error_is_set(&local_err)) { + error_propagate(err, local_err); return 0; } @@ -513,18 +819,22 @@ int64_t qmp_guest_fsfreeze_thaw(Error **err) ga_unset_frozen(ga_state); free_fs_mount_list(&mounts); + + execute_fsfreeze_hook(FSFREEZE_HOOK_THAW, err); + return i; } static void guest_fsfreeze_cleanup(void) { - int64_t ret; Error *err = NULL; if (ga_is_frozen(ga_state) == GUEST_FSFREEZE_STATUS_FROZEN) { - ret = qmp_guest_fsfreeze_thaw(&err); - if (ret < 0 || err) { - slog("failed to clean up frozen filesystems"); + qmp_guest_fsfreeze_thaw(&err); + if (err) { + slog("failed to clean up frozen filesystems: %s", + error_get_pretty(err)); + error_free(err); } } } @@ -540,7 +850,7 @@ void qmp_guest_fstrim(bool has_minimum, int64_t minimum, Error **err) FsMountList mounts; struct FsMount *mount; int fd; - char err_msg[512]; + Error *local_err = NULL; struct fstrim_range r = { .start = 0, .len = -1, @@ -550,17 +860,16 @@ void qmp_guest_fstrim(bool has_minimum, int64_t minimum, Error **err) slog("guest-fstrim called"); QTAILQ_INIT(&mounts); - ret = build_fs_mount_list(&mounts); - if (ret < 0) { + build_fs_mount_list(&mounts, &local_err); + if (error_is_set(&local_err)) { + error_propagate(err, local_err); return; } QTAILQ_FOREACH(mount, &mounts, next) { fd = qemu_open(mount->dirname, O_RDONLY); if (fd == -1) { - sprintf(err_msg, "failed to open %s, %s", mount->dirname, - strerror(errno)); - error_set(err, QERR_QGA_COMMAND_FAILED, err_msg); + error_setg_errno(err, errno, "failed to open %s", mount->dirname); goto error; } @@ -573,9 +882,8 @@ void qmp_guest_fstrim(bool has_minimum, int64_t minimum, Error **err) ret = ioctl(fd, FITRIM, &r); if (ret == -1) { if (errno != ENOTTY && errno != EOPNOTSUPP) { - sprintf(err_msg, "failed to trim %s, %s", - mount->dirname, strerror(errno)); - error_set(err, QERR_QGA_COMMAND_FAILED, err_msg); + error_setg_errno(err, errno, "failed to trim %s", + mount->dirname); close(fd); goto error; } @@ -596,8 +904,9 @@ error: static void bios_supports_mode(const char *pmutils_bin, const char *pmutils_arg, const char *sysfile_str, Error **err) { + Error *local_err = NULL; char *pmutils_path; - pid_t pid, rpid; + pid_t pid; int status; pmutils_path = g_find_program_in_path(pmutils_bin); @@ -642,38 +951,46 @@ static void bios_supports_mode(const char *pmutils_bin, const char *pmutils_arg, } _exit(SUSPEND_NOT_SUPPORTED); + } else if (pid < 0) { + error_setg_errno(err, errno, "failed to create child process"); + goto out; } - g_free(pmutils_path); + ga_wait_child(pid, &status, &local_err); + if (error_is_set(&local_err)) { + error_propagate(err, local_err); + goto out; + } - if (pid < 0) { - goto undef_err; + if (!WIFEXITED(status)) { + error_setg(err, "child process has terminated abnormally"); + goto out; } - do { - rpid = waitpid(pid, &status, 0); - } while (rpid == -1 && errno == EINTR); - if (rpid == pid && WIFEXITED(status)) { - switch (WEXITSTATUS(status)) { - case SUSPEND_SUPPORTED: - return; - case SUSPEND_NOT_SUPPORTED: - error_set(err, QERR_UNSUPPORTED); - return; - default: - goto undef_err; - } + switch (WEXITSTATUS(status)) { + case SUSPEND_SUPPORTED: + goto out; + case SUSPEND_NOT_SUPPORTED: + error_setg(err, + "the requested suspend mode is not supported by the guest"); + goto out; + default: + error_setg(err, + "the helper program '%s' returned an unexpected exit status" + " code (%d)", pmutils_path, WEXITSTATUS(status)); + goto out; } -undef_err: - error_set(err, QERR_UNDEFINED_ERROR); +out: + g_free(pmutils_path); } static void guest_suspend(const char *pmutils_bin, const char *sysfile_str, Error **err) { + Error *local_err = NULL; char *pmutils_path; - pid_t rpid, pid; + pid_t pid; int status; pmutils_path = g_find_program_in_path(pmutils_bin); @@ -711,23 +1028,29 @@ static void guest_suspend(const char *pmutils_bin, const char *sysfile_str, } _exit(EXIT_SUCCESS); + } else if (pid < 0) { + error_setg_errno(err, errno, "failed to create child process"); + goto out; } - g_free(pmutils_path); + ga_wait_child(pid, &status, &local_err); + if (error_is_set(&local_err)) { + error_propagate(err, local_err); + goto out; + } - if (pid < 0) { - goto exit_err; + if (!WIFEXITED(status)) { + error_setg(err, "child process has terminated abnormally"); + goto out; } - do { - rpid = waitpid(pid, &status, 0); - } while (rpid == -1 && errno == EINTR); - if (rpid == pid && WIFEXITED(status) && !WEXITSTATUS(status)) { - return; + if (WEXITSTATUS(status)) { + error_setg(err, "child process has failed to suspend"); + goto out; } -exit_err: - error_set(err, QERR_UNDEFINED_ERROR); +out: + g_free(pmutils_path); } void qmp_guest_suspend_disk(Error **err) @@ -780,12 +1103,9 @@ GuestNetworkInterfaceList *qmp_guest_network_get_interfaces(Error **errp) { GuestNetworkInterfaceList *head = NULL, *cur_item = NULL; struct ifaddrs *ifap, *ifa; - char err_msg[512]; if (getifaddrs(&ifap) < 0) { - snprintf(err_msg, sizeof(err_msg), - "getifaddrs failed: %s", strerror(errno)); - error_set(errp, QERR_QGA_COMMAND_FAILED, err_msg); + error_setg_errno(errp, errno, "getifaddrs failed"); goto error; } @@ -821,53 +1141,43 @@ GuestNetworkInterfaceList *qmp_guest_network_get_interfaces(Error **errp) /* we haven't obtained HW address yet */ sock = socket(PF_INET, SOCK_STREAM, 0); if (sock == -1) { - snprintf(err_msg, sizeof(err_msg), - "failed to create socket: %s", strerror(errno)); - error_set(errp, QERR_QGA_COMMAND_FAILED, err_msg); + error_setg_errno(errp, errno, "failed to create socket"); goto error; } memset(&ifr, 0, sizeof(ifr)); pstrcpy(ifr.ifr_name, IF_NAMESIZE, info->value->name); if (ioctl(sock, SIOCGIFHWADDR, &ifr) == -1) { - snprintf(err_msg, sizeof(err_msg), - "failed to get MAC address of %s: %s", - ifa->ifa_name, - strerror(errno)); - error_set(errp, QERR_QGA_COMMAND_FAILED, err_msg); + error_setg_errno(errp, errno, + "failed to get MAC address of %s", + ifa->ifa_name); + close(sock); goto error; } + close(sock); mac_addr = (unsigned char *) &ifr.ifr_hwaddr.sa_data; - if (asprintf(&info->value->hardware_address, - "%02x:%02x:%02x:%02x:%02x:%02x", - (int) mac_addr[0], (int) mac_addr[1], - (int) mac_addr[2], (int) mac_addr[3], - (int) mac_addr[4], (int) mac_addr[5]) == -1) { - snprintf(err_msg, sizeof(err_msg), - "failed to format MAC: %s", strerror(errno)); - error_set(errp, QERR_QGA_COMMAND_FAILED, err_msg); - goto error; - } + info->value->hardware_address = + g_strdup_printf("%02x:%02x:%02x:%02x:%02x:%02x", + (int) mac_addr[0], (int) mac_addr[1], + (int) mac_addr[2], (int) mac_addr[3], + (int) mac_addr[4], (int) mac_addr[5]); info->value->has_hardware_address = true; - close(sock); } if (ifa->ifa_addr && ifa->ifa_addr->sa_family == AF_INET) { /* interface with IPv4 address */ - address_item = g_malloc0(sizeof(*address_item)); - address_item->value = g_malloc0(sizeof(*address_item->value)); p = &((struct sockaddr_in *)ifa->ifa_addr)->sin_addr; if (!inet_ntop(AF_INET, p, addr4, sizeof(addr4))) { - snprintf(err_msg, sizeof(err_msg), - "inet_ntop failed : %s", strerror(errno)); - error_set(errp, QERR_QGA_COMMAND_FAILED, err_msg); + error_setg_errno(errp, errno, "inet_ntop failed"); goto error; } + address_item = g_malloc0(sizeof(*address_item)); + address_item->value = g_malloc0(sizeof(*address_item->value)); address_item->value->ip_address = g_strdup(addr4); address_item->value->ip_address_type = GUEST_IP_ADDRESS_TYPE_IPV4; @@ -880,16 +1190,14 @@ GuestNetworkInterfaceList *qmp_guest_network_get_interfaces(Error **errp) } else if (ifa->ifa_addr && ifa->ifa_addr->sa_family == AF_INET6) { /* interface with IPv6 address */ - address_item = g_malloc0(sizeof(*address_item)); - address_item->value = g_malloc0(sizeof(*address_item->value)); p = &((struct sockaddr_in6 *)ifa->ifa_addr)->sin6_addr; if (!inet_ntop(AF_INET6, p, addr6, sizeof(addr6))) { - snprintf(err_msg, sizeof(err_msg), - "inet_ntop failed : %s", strerror(errno)); - error_set(errp, QERR_QGA_COMMAND_FAILED, err_msg); + error_setg_errno(errp, errno, "inet_ntop failed"); goto error; } + address_item = g_malloc0(sizeof(*address_item)); + address_item->value = g_malloc0(sizeof(*address_item->value)); address_item->value->ip_address = g_strdup(addr6); address_item->value->ip_address_type = GUEST_IP_ADDRESS_TYPE_IPV6; @@ -935,6 +1243,162 @@ error: return NULL; } +#define SYSCONF_EXACT(name, err) sysconf_exact((name), #name, (err)) + +static long sysconf_exact(int name, const char *name_str, Error **err) +{ + long ret; + + errno = 0; + ret = sysconf(name); + if (ret == -1) { + if (errno == 0) { + error_setg(err, "sysconf(%s): value indefinite", name_str); + } else { + error_setg_errno(err, errno, "sysconf(%s)", name_str); + } + } + return ret; +} + +/* Transfer online/offline status between @vcpu and the guest system. + * + * On input either @errp or *@errp must be NULL. + * + * In system-to-@vcpu direction, the following @vcpu fields are accessed: + * - R: vcpu->logical_id + * - W: vcpu->online + * - W: vcpu->can_offline + * + * In @vcpu-to-system direction, the following @vcpu fields are accessed: + * - R: vcpu->logical_id + * - R: vcpu->online + * + * Written members remain unmodified on error. + */ +static void transfer_vcpu(GuestLogicalProcessor *vcpu, bool sys2vcpu, + Error **errp) +{ + char *dirpath; + int dirfd; + + dirpath = g_strdup_printf("/sys/devices/system/cpu/cpu%" PRId64 "/", + vcpu->logical_id); + dirfd = open(dirpath, O_RDONLY | O_DIRECTORY); + if (dirfd == -1) { + error_setg_errno(errp, errno, "open(\"%s\")", dirpath); + } else { + static const char fn[] = "online"; + int fd; + int res; + + fd = openat(dirfd, fn, sys2vcpu ? O_RDONLY : O_RDWR); + if (fd == -1) { + if (errno != ENOENT) { + error_setg_errno(errp, errno, "open(\"%s/%s\")", dirpath, fn); + } else if (sys2vcpu) { + vcpu->online = true; + vcpu->can_offline = false; + } else if (!vcpu->online) { + error_setg(errp, "logical processor #%" PRId64 " can't be " + "offlined", vcpu->logical_id); + } /* otherwise pretend successful re-onlining */ + } else { + unsigned char status; + + res = pread(fd, &status, 1, 0); + if (res == -1) { + error_setg_errno(errp, errno, "pread(\"%s/%s\")", dirpath, fn); + } else if (res == 0) { + error_setg(errp, "pread(\"%s/%s\"): unexpected EOF", dirpath, + fn); + } else if (sys2vcpu) { + vcpu->online = (status != '0'); + vcpu->can_offline = true; + } else if (vcpu->online != (status != '0')) { + status = '0' + vcpu->online; + if (pwrite(fd, &status, 1, 0) == -1) { + error_setg_errno(errp, errno, "pwrite(\"%s/%s\")", dirpath, + fn); + } + } /* otherwise pretend successful re-(on|off)-lining */ + + res = close(fd); + g_assert(res == 0); + } + + res = close(dirfd); + g_assert(res == 0); + } + + g_free(dirpath); +} + +GuestLogicalProcessorList *qmp_guest_get_vcpus(Error **errp) +{ + int64_t current; + GuestLogicalProcessorList *head, **link; + long sc_max; + Error *local_err = NULL; + + current = 0; + head = NULL; + link = &head; + sc_max = SYSCONF_EXACT(_SC_NPROCESSORS_CONF, &local_err); + + while (local_err == NULL && current < sc_max) { + GuestLogicalProcessor *vcpu; + GuestLogicalProcessorList *entry; + + vcpu = g_malloc0(sizeof *vcpu); + vcpu->logical_id = current++; + vcpu->has_can_offline = true; /* lolspeak ftw */ + transfer_vcpu(vcpu, true, &local_err); + + entry = g_malloc0(sizeof *entry); + entry->value = vcpu; + + *link = entry; + link = &entry->next; + } + + if (local_err == NULL) { + /* there's no guest with zero VCPUs */ + g_assert(head != NULL); + return head; + } + + qapi_free_GuestLogicalProcessorList(head); + error_propagate(errp, local_err); + return NULL; +} + +int64_t qmp_guest_set_vcpus(GuestLogicalProcessorList *vcpus, Error **errp) +{ + int64_t processed; + Error *local_err = NULL; + + processed = 0; + while (vcpus != NULL) { + transfer_vcpu(vcpus->value, false, &local_err); + if (local_err != NULL) { + break; + } + ++processed; + vcpus = vcpus->next; + } + + if (local_err != NULL) { + if (processed == 0) { + error_propagate(errp, local_err); + } else { + error_free(local_err); + } + } + + return processed; +} + #else /* defined(__linux__) */ void qmp_guest_suspend_disk(Error **err) @@ -958,6 +1422,18 @@ GuestNetworkInterfaceList *qmp_guest_network_get_interfaces(Error **errp) return NULL; } +GuestLogicalProcessorList *qmp_guest_get_vcpus(Error **errp) +{ + error_set(errp, QERR_UNSUPPORTED); + return NULL; +} + +int64_t qmp_guest_set_vcpus(GuestLogicalProcessorList *vcpus, Error **errp) +{ + error_set(errp, QERR_UNSUPPORTED); + return -1; +} + #endif #if !defined(CONFIG_FSFREEZE) diff --git a/qga/commands-win32.c b/qga/commands-win32.c index 5bd8fb27f..24e4ad031 100644 --- a/qga/commands-win32.c +++ b/qga/commands-win32.c @@ -16,12 +16,18 @@ #include <powrprof.h> #include "qga/guest-agent-core.h" #include "qga-qmp-commands.h" -#include "qerror.h" +#include "qapi/qmp/qerror.h" #ifndef SHTDN_REASON_FLAG_PLANNED #define SHTDN_REASON_FLAG_PLANNED 0x80000000 #endif +/* multiple of 100 nanoseconds elapsed between windows baseline + * (1/1/1601) and Unix Epoch (1/1/1970), accounting for leap years */ +#define W32_FT_OFFSET (10000000ULL * 60 * 60 * 24 * \ + (365 * (1970 - 1601) + \ + (1970 - 1601) / 4 - 3)) + static void acquire_privilege(const char *name, Error **err) { HANDLE token; @@ -278,6 +284,73 @@ GuestNetworkInterfaceList *qmp_guest_network_get_interfaces(Error **err) return NULL; } +int64_t qmp_guest_get_time(Error **errp) +{ + SYSTEMTIME ts = {0}; + int64_t time_ns; + FILETIME tf; + + GetSystemTime(&ts); + if (ts.wYear < 1601 || ts.wYear > 30827) { + error_setg(errp, "Failed to get time"); + return -1; + } + + if (!SystemTimeToFileTime(&ts, &tf)) { + error_setg(errp, "Failed to convert system time: %d", (int)GetLastError()); + return -1; + } + + time_ns = ((((int64_t)tf.dwHighDateTime << 32) | tf.dwLowDateTime) + - W32_FT_OFFSET) * 100; + + return time_ns; +} + +void qmp_guest_set_time(int64_t time_ns, Error **errp) +{ + SYSTEMTIME ts; + FILETIME tf; + LONGLONG time; + + if (time_ns < 0 || time_ns / 100 > INT64_MAX - W32_FT_OFFSET) { + error_setg(errp, "Time %" PRId64 "is invalid", time_ns); + return; + } + + time = time_ns / 100 + W32_FT_OFFSET; + + tf.dwLowDateTime = (DWORD) time; + tf.dwHighDateTime = (DWORD) (time >> 32); + + if (!FileTimeToSystemTime(&tf, &ts)) { + error_setg(errp, "Failed to convert system time %d", (int)GetLastError()); + return; + } + + acquire_privilege(SE_SYSTEMTIME_NAME, errp); + if (error_is_set(errp)) { + return; + } + + if (!SetSystemTime(&ts)) { + error_setg(errp, "Failed to set time to guest: %d", (int)GetLastError()); + return; + } +} + +GuestLogicalProcessorList *qmp_guest_get_vcpus(Error **errp) +{ + error_set(errp, QERR_UNSUPPORTED); + return NULL; +} + +int64_t qmp_guest_set_vcpus(GuestLogicalProcessorList *vcpus, Error **errp) +{ + error_set(errp, QERR_UNSUPPORTED); + return -1; +} + /* register init/cleanup routines for stateful command groups */ void ga_command_state_init(GAState *s, GACommandState *cs) { diff --git a/qga/commands.c b/qga/commands.c index 46b0b083b..528b082fa 100644 --- a/qga/commands.c +++ b/qga/commands.c @@ -13,7 +13,7 @@ #include <glib.h> #include "qga/guest-agent-core.h" #include "qga-qmp-commands.h" -#include "qerror.h" +#include "qapi/qmp/qerror.h" /* Note: in some situations, like with the fsfreeze, logging may be * temporarilly disabled. if it is necessary that a command be able @@ -61,7 +61,7 @@ struct GuestAgentInfo *qmp_guest_info(Error **err) while (*cmd_list) { cmd_info = g_malloc0(sizeof(GuestAgentCommandInfo)); - cmd_info->name = strdup(*cmd_list); + cmd_info->name = g_strdup(*cmd_list); cmd_info->enabled = qmp_command_is_enabled(cmd_info->name); cmd_info_list = g_malloc0(sizeof(GuestAgentCommandInfoList)); diff --git a/qga/guest-agent-core.h b/qga/guest-agent-core.h index 49a7abee9..624a559d9 100644 --- a/qga/guest-agent-core.h +++ b/qga/guest-agent-core.h @@ -10,7 +10,7 @@ * This work is licensed under the terms of the GNU GPL, version 2 or later. * See the COPYING file in the top-level directory. */ -#include "qapi/qmp-core.h" +#include "qapi/qmp/dispatch.h" #include "qemu-common.h" #define QGA_READ_COUNT_DEFAULT 4096 @@ -34,6 +34,8 @@ void ga_set_response_delimited(GAState *s); bool ga_is_frozen(GAState *s); void ga_set_frozen(GAState *s); void ga_unset_frozen(GAState *s); +const char *ga_fsfreeze_hook(GAState *s); +int64_t ga_get_fd_handle(GAState *s, Error **errp); #ifndef _WIN32 void reopen_fd_to_null(int fd); diff --git a/qga/main.c b/qga/main.c new file mode 100644 index 000000000..0e04e7395 --- /dev/null +++ b/qga/main.c @@ -0,0 +1,1195 @@ +/* + * QEMU Guest Agent + * + * Copyright IBM Corp. 2011 + * + * Authors: + * Adam Litke <aglitke@linux.vnet.ibm.com> + * Michael Roth <mdroth@linux.vnet.ibm.com> + * + * This work is licensed under the terms of the GNU GPL, version 2 or later. + * See the COPYING file in the top-level directory. + */ +#include <stdlib.h> +#include <stdio.h> +#include <stdbool.h> +#include <glib.h> +#include <getopt.h> +#include <glib/gstdio.h> +#ifndef _WIN32 +#include <syslog.h> +#include <sys/wait.h> +#include <sys/stat.h> +#endif +#include "qapi/qmp/json-streamer.h" +#include "qapi/qmp/json-parser.h" +#include "qapi/qmp/qint.h" +#include "qapi/qmp/qjson.h" +#include "qga/guest-agent-core.h" +#include "qemu/module.h" +#include "signal.h" +#include "qapi/qmp/qerror.h" +#include "qapi/qmp/dispatch.h" +#include "qga/channel.h" +#include "qemu/bswap.h" +#ifdef _WIN32 +#include "qga/service-win32.h" +#include <windows.h> +#endif +#ifdef __linux__ +#include <linux/fs.h> +#ifdef FIFREEZE +#define CONFIG_FSFREEZE +#endif +#endif + +#ifndef _WIN32 +#define QGA_VIRTIO_PATH_DEFAULT "/dev/virtio-ports/org.qemu.guest_agent.0" +#define QGA_STATE_RELATIVE_DIR "run" +#else +#define QGA_VIRTIO_PATH_DEFAULT "\\\\.\\Global\\org.qemu.guest_agent.0" +#define QGA_STATE_RELATIVE_DIR "qemu-ga" +#endif +#ifdef CONFIG_FSFREEZE +#define QGA_FSFREEZE_HOOK_DEFAULT CONFIG_QEMU_CONFDIR "/fsfreeze-hook" +#endif +#define QGA_SENTINEL_BYTE 0xFF + +static struct { + const char *state_dir; + const char *pidfile; +} dfl_pathnames; + +typedef struct GAPersistentState { +#define QGA_PSTATE_DEFAULT_FD_COUNTER 1000 + int64_t fd_counter; +} GAPersistentState; + +struct GAState { + JSONMessageParser parser; + GMainLoop *main_loop; + GAChannel *channel; + bool virtio; /* fastpath to check for virtio to deal with poll() quirks */ + GACommandState *command_state; + GLogLevelFlags log_level; + FILE *log_file; + bool logging_enabled; +#ifdef _WIN32 + GAService service; +#endif + bool delimit_response; + bool frozen; + GList *blacklist; + const char *state_filepath_isfrozen; + struct { + const char *log_filepath; + const char *pid_filepath; + } deferred_options; +#ifdef CONFIG_FSFREEZE + const char *fsfreeze_hook; +#endif + const gchar *pstate_filepath; + GAPersistentState pstate; +}; + +struct GAState *ga_state; + +/* commands that are safe to issue while filesystems are frozen */ +static const char *ga_freeze_whitelist[] = { + "guest-ping", + "guest-info", + "guest-sync", + "guest-sync-delimited", + "guest-fsfreeze-status", + "guest-fsfreeze-thaw", + NULL +}; + +#ifdef _WIN32 +DWORD WINAPI service_ctrl_handler(DWORD ctrl, DWORD type, LPVOID data, + LPVOID ctx); +VOID WINAPI service_main(DWORD argc, TCHAR *argv[]); +#endif + +static void +init_dfl_pathnames(void) +{ + g_assert(dfl_pathnames.state_dir == NULL); + g_assert(dfl_pathnames.pidfile == NULL); + dfl_pathnames.state_dir = qemu_get_local_state_pathname( + QGA_STATE_RELATIVE_DIR); + dfl_pathnames.pidfile = qemu_get_local_state_pathname( + QGA_STATE_RELATIVE_DIR G_DIR_SEPARATOR_S "qemu-ga.pid"); +} + +static void quit_handler(int sig) +{ + /* if we're frozen, don't exit unless we're absolutely forced to, + * because it's basically impossible for graceful exit to complete + * unless all log/pid files are on unfreezable filesystems. there's + * also a very likely chance killing the agent before unfreezing + * the filesystems is a mistake (or will be viewed as one later). + */ + if (ga_is_frozen(ga_state)) { + return; + } + g_debug("received signal num %d, quitting", sig); + + if (g_main_loop_is_running(ga_state->main_loop)) { + g_main_loop_quit(ga_state->main_loop); + } +} + +#ifndef _WIN32 +static gboolean register_signal_handlers(void) +{ + struct sigaction sigact; + int ret; + + memset(&sigact, 0, sizeof(struct sigaction)); + sigact.sa_handler = quit_handler; + + ret = sigaction(SIGINT, &sigact, NULL); + if (ret == -1) { + g_error("error configuring signal handler: %s", strerror(errno)); + } + ret = sigaction(SIGTERM, &sigact, NULL); + if (ret == -1) { + g_error("error configuring signal handler: %s", strerror(errno)); + } + + return true; +} + +/* TODO: use this in place of all post-fork() fclose(std*) callers */ +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); + } +} +#endif + +static void usage(const char *cmd) +{ + printf( +"Usage: %s [-m <method> -p <path>] [<options>]\n" +"QEMU Guest Agent %s\n" +"\n" +" -m, --method transport method: one of unix-listen, virtio-serial, or\n" +" isa-serial (virtio-serial is the default)\n" +" -p, --path device/socket path (the default for virtio-serial is:\n" +" %s)\n" +" -l, --logfile set logfile path, logs to stderr by default\n" +" -f, --pidfile specify pidfile (default is %s)\n" +#ifdef CONFIG_FSFREEZE +" -F, --fsfreeze-hook\n" +" enable fsfreeze hook. Accepts an optional argument that\n" +" specifies script to run on freeze/thaw. Script will be\n" +" called with 'freeze'/'thaw' arguments accordingly.\n" +" (default is %s)\n" +" If using -F with an argument, do not follow -F with a\n" +" space.\n" +" (for example: -F/var/run/fsfreezehook.sh)\n" +#endif +" -t, --statedir specify dir to store state information (absolute paths\n" +" only, default is %s)\n" +" -v, --verbose log extra debugging information\n" +" -V, --version print version information and exit\n" +" -d, --daemonize become a daemon\n" +#ifdef _WIN32 +" -s, --service service commands: install, uninstall\n" +#endif +" -b, --blacklist comma-separated list of RPCs to disable (no spaces, \"?\"\n" +" to list available RPCs)\n" +" -h, --help display this help and exit\n" +"\n" +"Report bugs to <mdroth@linux.vnet.ibm.com>\n" + , cmd, QEMU_VERSION, QGA_VIRTIO_PATH_DEFAULT, dfl_pathnames.pidfile, +#ifdef CONFIG_FSFREEZE + QGA_FSFREEZE_HOOK_DEFAULT, +#endif + dfl_pathnames.state_dir); +} + +static const char *ga_log_level_str(GLogLevelFlags level) +{ + switch (level & G_LOG_LEVEL_MASK) { + case G_LOG_LEVEL_ERROR: + return "error"; + case G_LOG_LEVEL_CRITICAL: + return "critical"; + case G_LOG_LEVEL_WARNING: + return "warning"; + case G_LOG_LEVEL_MESSAGE: + return "message"; + case G_LOG_LEVEL_INFO: + return "info"; + case G_LOG_LEVEL_DEBUG: + return "debug"; + default: + return "user"; + } +} + +bool ga_logging_enabled(GAState *s) +{ + return s->logging_enabled; +} + +void ga_disable_logging(GAState *s) +{ + s->logging_enabled = false; +} + +void ga_enable_logging(GAState *s) +{ + s->logging_enabled = true; +} + +static void ga_log(const gchar *domain, GLogLevelFlags level, + const gchar *msg, gpointer opaque) +{ + GAState *s = opaque; + GTimeVal time; + const char *level_str = ga_log_level_str(level); + + if (!ga_logging_enabled(s)) { + return; + } + + level &= G_LOG_LEVEL_MASK; +#ifndef _WIN32 + if (domain && strcmp(domain, "syslog") == 0) { + syslog(LOG_INFO, "%s: %s", level_str, msg); + } else if (level & s->log_level) { +#else + if (level & s->log_level) { +#endif + g_get_current_time(&time); + fprintf(s->log_file, + "%lu.%lu: %s: %s\n", time.tv_sec, time.tv_usec, level_str, msg); + fflush(s->log_file); + } +} + +void ga_set_response_delimited(GAState *s) +{ + s->delimit_response = true; +} + +static FILE *ga_open_logfile(const char *logfile) +{ + FILE *f; + + f = fopen(logfile, "a"); + if (!f) { + return NULL; + } + + qemu_set_cloexec(fileno(f)); + return f; +} + +#ifndef _WIN32 +static bool ga_open_pidfile(const char *pidfile) +{ + int pidfd; + char pidstr[32]; + + pidfd = qemu_open(pidfile, O_CREAT|O_WRONLY, S_IRUSR|S_IWUSR); + if (pidfd == -1 || lockf(pidfd, F_TLOCK, 0)) { + g_critical("Cannot lock pid file, %s", strerror(errno)); + if (pidfd != -1) { + close(pidfd); + } + return false; + } + + if (ftruncate(pidfd, 0)) { + g_critical("Failed to truncate pid file"); + goto fail; + } + snprintf(pidstr, sizeof(pidstr), "%d\n", getpid()); + if (write(pidfd, pidstr, strlen(pidstr)) != strlen(pidstr)) { + g_critical("Failed to write pid file"); + goto fail; + } + + /* keep pidfile open & locked forever */ + return true; + +fail: + unlink(pidfile); + close(pidfd); + return false; +} +#else /* _WIN32 */ +static bool ga_open_pidfile(const char *pidfile) +{ + return true; +} +#endif + +static gint ga_strcmp(gconstpointer str1, gconstpointer str2) +{ + return strcmp(str1, str2); +} + +/* disable commands that aren't safe for fsfreeze */ +static void ga_disable_non_whitelisted(void) +{ + char **list_head, **list; + bool whitelisted; + int i; + + list_head = list = qmp_get_command_list(); + while (*list != NULL) { + whitelisted = false; + i = 0; + while (ga_freeze_whitelist[i] != NULL) { + if (strcmp(*list, ga_freeze_whitelist[i]) == 0) { + whitelisted = true; + } + i++; + } + if (!whitelisted) { + g_debug("disabling command: %s", *list); + qmp_disable_command(*list); + } + g_free(*list); + list++; + } + g_free(list_head); +} + +/* [re-]enable all commands, except those explicitly blacklisted by user */ +static void ga_enable_non_blacklisted(GList *blacklist) +{ + char **list_head, **list; + + list_head = list = qmp_get_command_list(); + while (*list != NULL) { + if (g_list_find_custom(blacklist, *list, ga_strcmp) == NULL && + !qmp_command_is_enabled(*list)) { + g_debug("enabling command: %s", *list); + qmp_enable_command(*list); + } + g_free(*list); + list++; + } + g_free(list_head); +} + +static bool ga_create_file(const char *path) +{ + int fd = open(path, O_CREAT | O_WRONLY, S_IWUSR | S_IRUSR); + if (fd == -1) { + g_warning("unable to open/create file %s: %s", path, strerror(errno)); + return false; + } + close(fd); + return true; +} + +static bool ga_delete_file(const char *path) +{ + int ret = unlink(path); + if (ret == -1) { + g_warning("unable to delete file: %s: %s", path, strerror(errno)); + return false; + } + + return true; +} + +bool ga_is_frozen(GAState *s) +{ + return s->frozen; +} + +void ga_set_frozen(GAState *s) +{ + if (ga_is_frozen(s)) { + return; + } + /* disable all non-whitelisted (for frozen state) commands */ + ga_disable_non_whitelisted(); + g_warning("disabling logging due to filesystem freeze"); + ga_disable_logging(s); + s->frozen = true; + if (!ga_create_file(s->state_filepath_isfrozen)) { + g_warning("unable to create %s, fsfreeze may not function properly", + s->state_filepath_isfrozen); + } +} + +void ga_unset_frozen(GAState *s) +{ + if (!ga_is_frozen(s)) { + return; + } + + /* if we delayed creation/opening of pid/log files due to being + * in a frozen state at start up, do it now + */ + if (s->deferred_options.log_filepath) { + s->log_file = ga_open_logfile(s->deferred_options.log_filepath); + if (!s->log_file) { + s->log_file = stderr; + } + s->deferred_options.log_filepath = NULL; + } + ga_enable_logging(s); + g_warning("logging re-enabled due to filesystem unfreeze"); + if (s->deferred_options.pid_filepath) { + if (!ga_open_pidfile(s->deferred_options.pid_filepath)) { + g_warning("failed to create/open pid file"); + } + s->deferred_options.pid_filepath = NULL; + } + + /* enable all disabled, non-blacklisted commands */ + ga_enable_non_blacklisted(s->blacklist); + s->frozen = false; + if (!ga_delete_file(s->state_filepath_isfrozen)) { + g_warning("unable to delete %s, fsfreeze may not function properly", + s->state_filepath_isfrozen); + } +} + +#ifdef CONFIG_FSFREEZE +const char *ga_fsfreeze_hook(GAState *s) +{ + return s->fsfreeze_hook; +} +#endif + +static void become_daemon(const char *pidfile) +{ +#ifndef _WIN32 + pid_t pid, sid; + + pid = fork(); + if (pid < 0) { + exit(EXIT_FAILURE); + } + if (pid > 0) { + exit(EXIT_SUCCESS); + } + + if (pidfile) { + if (!ga_open_pidfile(pidfile)) { + g_critical("failed to create pidfile"); + exit(EXIT_FAILURE); + } + } + + umask(S_IRWXG | S_IRWXO); + sid = setsid(); + if (sid < 0) { + goto fail; + } + if ((chdir("/")) < 0) { + goto fail; + } + + reopen_fd_to_null(STDIN_FILENO); + reopen_fd_to_null(STDOUT_FILENO); + reopen_fd_to_null(STDERR_FILENO); + return; + +fail: + if (pidfile) { + unlink(pidfile); + } + g_critical("failed to daemonize"); + exit(EXIT_FAILURE); +#endif +} + +static int send_response(GAState *s, QObject *payload) +{ + const char *buf; + QString *payload_qstr, *response_qstr; + GIOStatus status; + + g_assert(payload && s->channel); + + payload_qstr = qobject_to_json(payload); + if (!payload_qstr) { + return -EINVAL; + } + + if (s->delimit_response) { + s->delimit_response = false; + response_qstr = qstring_new(); + qstring_append_chr(response_qstr, QGA_SENTINEL_BYTE); + qstring_append(response_qstr, qstring_get_str(payload_qstr)); + QDECREF(payload_qstr); + } else { + response_qstr = payload_qstr; + } + + qstring_append_chr(response_qstr, '\n'); + buf = qstring_get_str(response_qstr); + status = ga_channel_write_all(s->channel, buf, strlen(buf)); + QDECREF(response_qstr); + if (status != G_IO_STATUS_NORMAL) { + return -EIO; + } + + return 0; +} + +static void process_command(GAState *s, QDict *req) +{ + QObject *rsp = NULL; + int ret; + + g_assert(req); + g_debug("processing command"); + rsp = qmp_dispatch(QOBJECT(req)); + if (rsp) { + ret = send_response(s, rsp); + if (ret) { + g_warning("error sending response: %s", strerror(ret)); + } + qobject_decref(rsp); + } +} + +/* handle requests/control events coming in over the channel */ +static void process_event(JSONMessageParser *parser, QList *tokens) +{ + GAState *s = container_of(parser, GAState, parser); + QObject *obj; + QDict *qdict; + Error *err = NULL; + int ret; + + g_assert(s && parser); + + g_debug("process_event: called"); + obj = json_parser_parse_err(tokens, NULL, &err); + if (err || !obj || qobject_type(obj) != QTYPE_QDICT) { + qobject_decref(obj); + qdict = qdict_new(); + if (!err) { + g_warning("failed to parse event: unknown error"); + error_set(&err, QERR_JSON_PARSING); + } else { + g_warning("failed to parse event: %s", error_get_pretty(err)); + } + qdict_put_obj(qdict, "error", qmp_build_error_object(err)); + error_free(err); + } else { + qdict = qobject_to_qdict(obj); + } + + g_assert(qdict); + + /* handle host->guest commands */ + if (qdict_haskey(qdict, "execute")) { + process_command(s, qdict); + } else { + if (!qdict_haskey(qdict, "error")) { + QDECREF(qdict); + qdict = qdict_new(); + g_warning("unrecognized payload format"); + error_set(&err, QERR_UNSUPPORTED); + qdict_put_obj(qdict, "error", qmp_build_error_object(err)); + error_free(err); + } + ret = send_response(s, QOBJECT(qdict)); + if (ret) { + g_warning("error sending error response: %s", strerror(ret)); + } + } + + QDECREF(qdict); +} + +/* false return signals GAChannel to close the current client connection */ +static gboolean channel_event_cb(GIOCondition condition, gpointer data) +{ + GAState *s = data; + gchar buf[QGA_READ_COUNT_DEFAULT+1]; + gsize count; + GError *err = NULL; + GIOStatus status = ga_channel_read(s->channel, buf, QGA_READ_COUNT_DEFAULT, &count); + if (err != NULL) { + g_warning("error reading channel: %s", err->message); + g_error_free(err); + return false; + } + switch (status) { + case G_IO_STATUS_ERROR: + g_warning("error reading channel"); + return false; + case G_IO_STATUS_NORMAL: + buf[count] = 0; + g_debug("read data, count: %d, data: %s", (int)count, buf); + json_message_parser_feed(&s->parser, (char *)buf, (int)count); + break; + case G_IO_STATUS_EOF: + g_debug("received EOF"); + if (!s->virtio) { + return false; + } + /* fall through */ + case G_IO_STATUS_AGAIN: + /* virtio causes us to spin here when no process is attached to + * host-side chardev. sleep a bit to mitigate this + */ + if (s->virtio) { + usleep(100*1000); + } + return true; + default: + g_warning("unknown channel read status, closing"); + return false; + } + return true; +} + +static gboolean channel_init(GAState *s, const gchar *method, const gchar *path) +{ + GAChannelMethod channel_method; + + if (method == NULL) { + method = "virtio-serial"; + } + + if (path == NULL) { + if (strcmp(method, "virtio-serial") != 0) { + g_critical("must specify a path for this channel"); + return false; + } + /* try the default path for the virtio-serial port */ + path = QGA_VIRTIO_PATH_DEFAULT; + } + + if (strcmp(method, "virtio-serial") == 0) { + s->virtio = true; /* virtio requires special handling in some cases */ + channel_method = GA_CHANNEL_VIRTIO_SERIAL; + } else if (strcmp(method, "isa-serial") == 0) { + channel_method = GA_CHANNEL_ISA_SERIAL; + } else if (strcmp(method, "unix-listen") == 0) { + channel_method = GA_CHANNEL_UNIX_LISTEN; + } else { + g_critical("unsupported channel method/type: %s", method); + return false; + } + + s->channel = ga_channel_new(channel_method, path, channel_event_cb, s); + if (!s->channel) { + g_critical("failed to create guest agent channel"); + return false; + } + + return true; +} + +#ifdef _WIN32 +DWORD WINAPI service_ctrl_handler(DWORD ctrl, DWORD type, LPVOID data, + LPVOID ctx) +{ + DWORD ret = NO_ERROR; + GAService *service = &ga_state->service; + + switch (ctrl) + { + case SERVICE_CONTROL_STOP: + case SERVICE_CONTROL_SHUTDOWN: + quit_handler(SIGTERM); + service->status.dwCurrentState = SERVICE_STOP_PENDING; + SetServiceStatus(service->status_handle, &service->status); + break; + + default: + ret = ERROR_CALL_NOT_IMPLEMENTED; + } + return ret; +} + +VOID WINAPI service_main(DWORD argc, TCHAR *argv[]) +{ + GAService *service = &ga_state->service; + + service->status_handle = RegisterServiceCtrlHandlerEx(QGA_SERVICE_NAME, + service_ctrl_handler, NULL); + + if (service->status_handle == 0) { + g_critical("Failed to register extended requests function!\n"); + return; + } + + service->status.dwServiceType = SERVICE_WIN32; + service->status.dwCurrentState = SERVICE_RUNNING; + service->status.dwControlsAccepted = SERVICE_ACCEPT_STOP | SERVICE_ACCEPT_SHUTDOWN; + service->status.dwWin32ExitCode = NO_ERROR; + service->status.dwServiceSpecificExitCode = NO_ERROR; + service->status.dwCheckPoint = 0; + service->status.dwWaitHint = 0; + SetServiceStatus(service->status_handle, &service->status); + + g_main_loop_run(ga_state->main_loop); + + service->status.dwCurrentState = SERVICE_STOPPED; + SetServiceStatus(service->status_handle, &service->status); +} +#endif + +static void set_persistent_state_defaults(GAPersistentState *pstate) +{ + g_assert(pstate); + pstate->fd_counter = QGA_PSTATE_DEFAULT_FD_COUNTER; +} + +static void persistent_state_from_keyfile(GAPersistentState *pstate, + GKeyFile *keyfile) +{ + g_assert(pstate); + g_assert(keyfile); + /* if any fields are missing, either because the file was tampered with + * by agents of chaos, or because the field wasn't present at the time the + * file was created, the best we can ever do is start over with the default + * values. so load them now, and ignore any errors in accessing key-value + * pairs + */ + set_persistent_state_defaults(pstate); + + if (g_key_file_has_key(keyfile, "global", "fd_counter", NULL)) { + pstate->fd_counter = + g_key_file_get_integer(keyfile, "global", "fd_counter", NULL); + } +} + +static void persistent_state_to_keyfile(const GAPersistentState *pstate, + GKeyFile *keyfile) +{ + g_assert(pstate); + g_assert(keyfile); + + g_key_file_set_integer(keyfile, "global", "fd_counter", pstate->fd_counter); +} + +static gboolean write_persistent_state(const GAPersistentState *pstate, + const gchar *path) +{ + GKeyFile *keyfile = g_key_file_new(); + GError *gerr = NULL; + gboolean ret = true; + gchar *data = NULL; + gsize data_len; + + g_assert(pstate); + + persistent_state_to_keyfile(pstate, keyfile); + data = g_key_file_to_data(keyfile, &data_len, &gerr); + if (gerr) { + g_critical("failed to convert persistent state to string: %s", + gerr->message); + ret = false; + goto out; + } + + g_file_set_contents(path, data, data_len, &gerr); + if (gerr) { + g_critical("failed to write persistent state to %s: %s", + path, gerr->message); + ret = false; + goto out; + } + +out: + if (gerr) { + g_error_free(gerr); + } + if (keyfile) { + g_key_file_free(keyfile); + } + g_free(data); + return ret; +} + +static gboolean read_persistent_state(GAPersistentState *pstate, + const gchar *path, gboolean frozen) +{ + GKeyFile *keyfile = NULL; + GError *gerr = NULL; + struct stat st; + gboolean ret = true; + + g_assert(pstate); + + if (stat(path, &st) == -1) { + /* it's okay if state file doesn't exist, but any other error + * indicates a permissions issue or some other misconfiguration + * that we likely won't be able to recover from. + */ + if (errno != ENOENT) { + g_critical("unable to access state file at path %s: %s", + path, strerror(errno)); + ret = false; + goto out; + } + + /* file doesn't exist. initialize state to default values and + * attempt to save now. (we could wait till later when we have + * modified state we need to commit, but if there's a problem, + * such as a missing parent directory, we want to catch it now) + * + * there is a potential scenario where someone either managed to + * update the agent from a version that didn't use a key store + * while qemu-ga thought the filesystem was frozen, or + * deleted the key store prior to issuing a fsfreeze, prior + * to restarting the agent. in this case we go ahead and defer + * initial creation till we actually have modified state to + * write, otherwise fail to recover from freeze. + */ + set_persistent_state_defaults(pstate); + if (!frozen) { + ret = write_persistent_state(pstate, path); + if (!ret) { + g_critical("unable to create state file at path %s", path); + ret = false; + goto out; + } + } + ret = true; + goto out; + } + + keyfile = g_key_file_new(); + g_key_file_load_from_file(keyfile, path, 0, &gerr); + if (gerr) { + g_critical("error loading persistent state from path: %s, %s", + path, gerr->message); + ret = false; + goto out; + } + + persistent_state_from_keyfile(pstate, keyfile); + +out: + if (keyfile) { + g_key_file_free(keyfile); + } + if (gerr) { + g_error_free(gerr); + } + + return ret; +} + +int64_t ga_get_fd_handle(GAState *s, Error **errp) +{ + int64_t handle; + + g_assert(s->pstate_filepath); + /* we blacklist commands and avoid operations that potentially require + * writing to disk when we're in a frozen state. this includes opening + * new files, so we should never get here in that situation + */ + g_assert(!ga_is_frozen(s)); + + handle = s->pstate.fd_counter++; + + /* This should never happen on a reasonable timeframe, as guest-file-open + * would have to be issued 2^63 times */ + if (s->pstate.fd_counter == INT64_MAX) { + abort(); + } + + if (!write_persistent_state(&s->pstate, s->pstate_filepath)) { + error_setg(errp, "failed to commit persistent state to disk"); + } + + return handle; +} + +int main(int argc, char **argv) +{ + const char *sopt = "hVvdm:p:l:f:F::b:s:t:"; + const char *method = NULL, *path = NULL; + const char *log_filepath = NULL; + const char *pid_filepath; +#ifdef CONFIG_FSFREEZE + const char *fsfreeze_hook = NULL; +#endif + const char *state_dir; +#ifdef _WIN32 + const char *service = NULL; +#endif + const struct option lopt[] = { + { "help", 0, NULL, 'h' }, + { "version", 0, NULL, 'V' }, + { "logfile", 1, NULL, 'l' }, + { "pidfile", 1, NULL, 'f' }, +#ifdef CONFIG_FSFREEZE + { "fsfreeze-hook", 2, NULL, 'F' }, +#endif + { "verbose", 0, NULL, 'v' }, + { "method", 1, NULL, 'm' }, + { "path", 1, NULL, 'p' }, + { "daemonize", 0, NULL, 'd' }, + { "blacklist", 1, NULL, 'b' }, +#ifdef _WIN32 + { "service", 1, NULL, 's' }, +#endif + { "statedir", 1, NULL, 't' }, + { NULL, 0, NULL, 0 } + }; + int opt_ind = 0, ch, daemonize = 0, i, j, len; + GLogLevelFlags log_level = G_LOG_LEVEL_ERROR | G_LOG_LEVEL_CRITICAL; + GList *blacklist = NULL; + GAState *s; + + module_call_init(MODULE_INIT_QAPI); + + init_dfl_pathnames(); + pid_filepath = dfl_pathnames.pidfile; + state_dir = dfl_pathnames.state_dir; + + while ((ch = getopt_long(argc, argv, sopt, lopt, &opt_ind)) != -1) { + switch (ch) { + case 'm': + method = optarg; + break; + case 'p': + path = optarg; + break; + case 'l': + log_filepath = optarg; + break; + case 'f': + pid_filepath = optarg; + break; +#ifdef CONFIG_FSFREEZE + case 'F': + fsfreeze_hook = optarg ? optarg : QGA_FSFREEZE_HOOK_DEFAULT; + break; +#endif + case 't': + state_dir = optarg; + break; + case 'v': + /* enable all log levels */ + log_level = G_LOG_LEVEL_MASK; + break; + case 'V': + printf("QEMU Guest Agent %s\n", QEMU_VERSION); + return 0; + case 'd': + daemonize = 1; + break; + case 'b': { + char **list_head, **list; + if (is_help_option(optarg)) { + list_head = list = qmp_get_command_list(); + while (*list != NULL) { + printf("%s\n", *list); + g_free(*list); + list++; + } + g_free(list_head); + return 0; + } + for (j = 0, i = 0, len = strlen(optarg); i < len; i++) { + if (optarg[i] == ',') { + optarg[i] = 0; + blacklist = g_list_append(blacklist, &optarg[j]); + j = i + 1; + } + } + if (j < i) { + blacklist = g_list_append(blacklist, &optarg[j]); + } + break; + } +#ifdef _WIN32 + case 's': + service = optarg; + if (strcmp(service, "install") == 0) { + const char *fixed_state_dir; + + /* If the user passed the "-t" option, we save that state dir + * in the service. Otherwise we let the service fetch the state + * dir from the environment when it starts. + */ + fixed_state_dir = (state_dir == dfl_pathnames.state_dir) ? + NULL : + state_dir; + return ga_install_service(path, log_filepath, fixed_state_dir); + } else if (strcmp(service, "uninstall") == 0) { + return ga_uninstall_service(); + } else { + printf("Unknown service command.\n"); + return EXIT_FAILURE; + } + break; +#endif + case 'h': + usage(argv[0]); + return 0; + case '?': + g_print("Unknown option, try '%s --help' for more information.\n", + argv[0]); + return EXIT_FAILURE; + } + } + +#ifdef _WIN32 + /* On win32 the state directory is application specific (be it the default + * or a user override). We got past the command line parsing; let's create + * the directory (with any intermediate directories). If we run into an + * error later on, we won't try to clean up the directory, it is considered + * persistent. + */ + if (g_mkdir_with_parents(state_dir, S_IRWXU) == -1) { + g_critical("unable to create (an ancestor of) the state directory" + " '%s': %s", state_dir, strerror(errno)); + return EXIT_FAILURE; + } +#endif + + s = g_malloc0(sizeof(GAState)); + s->log_level = log_level; + s->log_file = stderr; +#ifdef CONFIG_FSFREEZE + s->fsfreeze_hook = fsfreeze_hook; +#endif + g_log_set_default_handler(ga_log, s); + g_log_set_fatal_mask(NULL, G_LOG_LEVEL_ERROR); + ga_enable_logging(s); + s->state_filepath_isfrozen = g_strdup_printf("%s/qga.state.isfrozen", + state_dir); + s->pstate_filepath = g_strdup_printf("%s/qga.state", state_dir); + s->frozen = false; + +#ifndef _WIN32 + /* check if a previous instance of qemu-ga exited with filesystems' state + * marked as frozen. this could be a stale value (a non-qemu-ga process + * or reboot may have since unfrozen them), but better to require an + * uneeded unfreeze than to risk hanging on start-up + */ + struct stat st; + if (stat(s->state_filepath_isfrozen, &st) == -1) { + /* it's okay if the file doesn't exist, but if we can't access for + * some other reason, such as permissions, there's a configuration + * that needs to be addressed. so just bail now before we get into + * more trouble later + */ + if (errno != ENOENT) { + g_critical("unable to access state file at path %s: %s", + s->state_filepath_isfrozen, strerror(errno)); + return EXIT_FAILURE; + } + } else { + g_warning("previous instance appears to have exited with frozen" + " filesystems. deferring logging/pidfile creation and" + " disabling non-fsfreeze-safe commands until" + " guest-fsfreeze-thaw is issued, or filesystems are" + " manually unfrozen and the file %s is removed", + s->state_filepath_isfrozen); + s->frozen = true; + } +#endif + + if (ga_is_frozen(s)) { + if (daemonize) { + /* delay opening/locking of pidfile till filesystem are unfrozen */ + s->deferred_options.pid_filepath = pid_filepath; + become_daemon(NULL); + } + if (log_filepath) { + /* delay opening the log file till filesystems are unfrozen */ + s->deferred_options.log_filepath = log_filepath; + } + ga_disable_logging(s); + ga_disable_non_whitelisted(); + } else { + if (daemonize) { + become_daemon(pid_filepath); + } + if (log_filepath) { + FILE *log_file = ga_open_logfile(log_filepath); + if (!log_file) { + g_critical("unable to open specified log file: %s", + strerror(errno)); + goto out_bad; + } + s->log_file = log_file; + } + } + + /* load persistent state from disk */ + if (!read_persistent_state(&s->pstate, + s->pstate_filepath, + ga_is_frozen(s))) { + g_critical("failed to load persistent state"); + goto out_bad; + } + + if (blacklist) { + s->blacklist = blacklist; + do { + g_debug("disabling command: %s", (char *)blacklist->data); + qmp_disable_command(blacklist->data); + blacklist = g_list_next(blacklist); + } while (blacklist); + } + s->command_state = ga_command_state_new(); + ga_command_state_init(s, s->command_state); + ga_command_state_init_all(s->command_state); + json_message_parser_init(&s->parser, process_event); + ga_state = s; +#ifndef _WIN32 + if (!register_signal_handlers()) { + g_critical("failed to register signal handlers"); + goto out_bad; + } +#endif + + s->main_loop = g_main_loop_new(NULL, false); + if (!channel_init(ga_state, method, path)) { + g_critical("failed to initialize guest agent channel"); + goto out_bad; + } +#ifndef _WIN32 + g_main_loop_run(ga_state->main_loop); +#else + if (daemonize) { + SERVICE_TABLE_ENTRY service_table[] = { + { (char *)QGA_SERVICE_NAME, service_main }, { NULL, NULL } }; + StartServiceCtrlDispatcher(service_table); + } else { + g_main_loop_run(ga_state->main_loop); + } +#endif + + ga_command_state_cleanup_all(ga_state->command_state); + ga_channel_free(ga_state->channel); + + if (daemonize) { + unlink(pid_filepath); + } + return 0; + +out_bad: + if (daemonize) { + unlink(pid_filepath); + } + return EXIT_FAILURE; +} diff --git a/qga/qapi-schema.json b/qga/qapi-schema.json new file mode 100644 index 000000000..7155b7ab5 --- /dev/null +++ b/qga/qapi-schema.json @@ -0,0 +1,640 @@ +# *-*- Mode: Python -*-* + +## +# +# General note concerning the use of guest agent interfaces: +# +# "unsupported" is a higher-level error than the errors that individual +# commands might document. The caller should always be prepared to receive +# QERR_UNSUPPORTED, even if the given command doesn't specify it, or doesn't +# document any failure mode at all. +# +## + +## +# +# Echo back a unique integer value, and prepend to response a +# leading sentinel byte (0xFF) the client can check scan for. +# +# This is used by clients talking to the guest agent over the +# wire to ensure the stream is in sync and doesn't contain stale +# data from previous client. It must be issued upon initial +# connection, and after any client-side timeouts (including +# timeouts on receiving a response to this command). +# +# After issuing this request, all guest agent responses should be +# ignored until the response containing the unique integer value +# the client passed in is returned. Receival of the 0xFF sentinel +# byte must be handled as an indication that the client's +# lexer/tokenizer/parser state should be flushed/reset in +# preparation for reliably receiving the subsequent response. As +# an optimization, clients may opt to ignore all data until a +# sentinel value is receiving to avoid unnecessary processing of +# stale data. +# +# Similarly, clients should also precede this *request* +# with a 0xFF byte to make sure the guest agent flushes any +# partially read JSON data from a previous client connection. +# +# @id: randomly generated 64-bit integer +# +# Returns: The unique integer id passed in by the client +# +# Since: 1.1 +# ## +{ 'command': 'guest-sync-delimited', + 'data': { 'id': 'int' }, + 'returns': 'int' } + +## +# @guest-sync: +# +# Echo back a unique integer value +# +# This is used by clients talking to the guest agent over the +# wire to ensure the stream is in sync and doesn't contain stale +# data from previous client. All guest agent responses should be +# ignored until the provided unique integer value is returned, +# and it is up to the client to handle stale whole or +# partially-delivered JSON text in such a way that this response +# can be obtained. +# +# In cases where a partial stale response was previously +# received by the client, this cannot always be done reliably. +# One particular scenario being if qemu-ga responses are fed +# character-by-character into a JSON parser. In these situations, +# using guest-sync-delimited may be optimal. +# +# For clients that fetch responses line by line and convert them +# to JSON objects, guest-sync should be sufficient, but note that +# in cases where the channel is dirty some attempts at parsing the +# response may result in a parser error. +# +# Such clients should also precede this command +# with a 0xFF byte to make sure the guest agent flushes any +# partially read JSON data from a previous session. +# +# @id: randomly generated 64-bit integer +# +# Returns: The unique integer id passed in by the client +# +# Since: 0.15.0 +## +{ 'command': 'guest-sync', + 'data': { 'id': 'int' }, + 'returns': 'int' } + +## +# @guest-ping: +# +# Ping the guest agent, a non-error return implies success +# +# Since: 0.15.0 +## +{ 'command': 'guest-ping' } + +## +# @guest-get-time: +# +# Get the information about guest time relative to the Epoch +# of 1970-01-01 in UTC. +# +# Returns: Time in nanoseconds. +# +# Since 1.5 +## +{ 'command': 'guest-get-time', + 'returns': 'int' } + +## +# @guest-set-time: +# +# Set guest time. +# +# When a guest is paused or migrated to a file then loaded +# from that file, the guest OS has no idea that there +# was a big gap in the time. Depending on how long the +# gap was, NTP might not be able to resynchronize the +# guest. +# +# This command tries to set guest time to the given value, +# then sets the Hardware Clock to the current System Time. +# This will make it easier for a guest to resynchronize +# without waiting for NTP. +# +# @time: time of nanoseconds, relative to the Epoch of +# 1970-01-01 in UTC. +# +# Returns: Nothing on success. +# +# Since: 1.5 +## +{ 'command': 'guest-set-time', + 'data': { 'time': 'int' } } + +## +# @GuestAgentCommandInfo: +# +# Information about guest agent commands. +# +# @name: name of the command +# +# @enabled: whether command is currently enabled by guest admin +# +# Since 1.1.0 +## +{ 'type': 'GuestAgentCommandInfo', + 'data': { 'name': 'str', 'enabled': 'bool' } } + +## +# @GuestAgentInfo +# +# Information about guest agent. +# +# @version: guest agent version +# +# @supported_commands: Information about guest agent commands +# +# Since 0.15.0 +## +{ 'type': 'GuestAgentInfo', + 'data': { 'version': 'str', + 'supported_commands': ['GuestAgentCommandInfo'] } } +## +# @guest-info: +# +# Get some information about the guest agent. +# +# Returns: @GuestAgentInfo +# +# Since: 0.15.0 +## +{ 'command': 'guest-info', + 'returns': 'GuestAgentInfo' } + +## +# @guest-shutdown: +# +# Initiate guest-activated shutdown. Note: this is an asynchronous +# shutdown request, with no guarantee of successful shutdown. +# +# @mode: #optional "halt", "powerdown" (default), or "reboot" +# +# This command does NOT return a response on success. Success condition +# is indicated by the VM exiting with a zero exit status or, when +# running with --no-shutdown, by issuing the query-status QMP command +# to confirm the VM status is "shutdown". +# +# Since: 0.15.0 +## +{ 'command': 'guest-shutdown', 'data': { '*mode': 'str' }, + 'success-response': 'no' } + +## +# @guest-file-open: +# +# Open a file in the guest and retrieve a file handle for it +# +# @filepath: Full path to the file in the guest to open. +# +# @mode: #optional open mode, as per fopen(), "r" is the default. +# +# Returns: Guest file handle on success. +# +# Since: 0.15.0 +## +{ 'command': 'guest-file-open', + 'data': { 'path': 'str', '*mode': 'str' }, + 'returns': 'int' } + +## +# @guest-file-close: +# +# Close an open file in the guest +# +# @handle: filehandle returned by guest-file-open +# +# Returns: Nothing on success. +# +# Since: 0.15.0 +## +{ 'command': 'guest-file-close', + 'data': { 'handle': 'int' } } + +## +# @GuestFileRead +# +# Result of guest agent file-read operation +# +# @count: number of bytes read (note: count is *before* +# base64-encoding is applied) +# +# @buf-b64: base64-encoded bytes read +# +# @eof: whether EOF was encountered during read operation. +# +# Since: 0.15.0 +## +{ 'type': 'GuestFileRead', + 'data': { 'count': 'int', 'buf-b64': 'str', 'eof': 'bool' } } + +## +# @guest-file-read: +# +# Read from an open file in the guest. Data will be base64-encoded +# +# @handle: filehandle returned by guest-file-open +# +# @count: #optional maximum number of bytes to read (default is 4KB) +# +# Returns: @GuestFileRead on success. +# +# Since: 0.15.0 +## +{ 'command': 'guest-file-read', + 'data': { 'handle': 'int', '*count': 'int' }, + 'returns': 'GuestFileRead' } + +## +# @GuestFileWrite +# +# Result of guest agent file-write operation +# +# @count: number of bytes written (note: count is actual bytes +# written, after base64-decoding of provided buffer) +# +# @eof: whether EOF was encountered during write operation. +# +# Since: 0.15.0 +## +{ 'type': 'GuestFileWrite', + 'data': { 'count': 'int', 'eof': 'bool' } } + +## +# @guest-file-write: +# +# Write to an open file in the guest. +# +# @handle: filehandle returned by guest-file-open +# +# @buf-b64: base64-encoded string representing data to be written +# +# @count: #optional bytes to write (actual bytes, after base64-decode), +# default is all content in buf-b64 buffer after base64 decoding +# +# Returns: @GuestFileWrite on success. +# +# Since: 0.15.0 +## +{ 'command': 'guest-file-write', + 'data': { 'handle': 'int', 'buf-b64': 'str', '*count': 'int' }, + 'returns': 'GuestFileWrite' } + + +## +# @GuestFileSeek +# +# Result of guest agent file-seek operation +# +# @position: current file position +# +# @eof: whether EOF was encountered during file seek +# +# Since: 0.15.0 +## +{ 'type': 'GuestFileSeek', + 'data': { 'position': 'int', 'eof': 'bool' } } + +## +# @guest-file-seek: +# +# Seek to a position in the file, as with fseek(), and return the +# current file position afterward. Also encapsulates ftell()'s +# functionality, just Set offset=0, whence=SEEK_CUR. +# +# @handle: filehandle returned by guest-file-open +# +# @offset: bytes to skip over in the file stream +# +# @whence: SEEK_SET, SEEK_CUR, or SEEK_END, as with fseek() +# +# Returns: @GuestFileSeek on success. +# +# Since: 0.15.0 +## +{ 'command': 'guest-file-seek', + 'data': { 'handle': 'int', 'offset': 'int', 'whence': 'int' }, + 'returns': 'GuestFileSeek' } + +## +# @guest-file-flush: +# +# Write file changes bufferred in userspace to disk/kernel buffers +# +# @handle: filehandle returned by guest-file-open +# +# Returns: Nothing on success. +# +# Since: 0.15.0 +## +{ 'command': 'guest-file-flush', + 'data': { 'handle': 'int' } } + +## +# @GuestFsFreezeStatus +# +# An enumeration of filesystem freeze states +# +# @thawed: filesystems thawed/unfrozen +# +# @frozen: all non-network guest filesystems frozen +# +# Since: 0.15.0 +## +{ 'enum': 'GuestFsfreezeStatus', + 'data': [ 'thawed', 'frozen' ] } + +## +# @guest-fsfreeze-status: +# +# Get guest fsfreeze state. error state indicates +# +# Returns: GuestFsfreezeStatus ("thawed", "frozen", etc., as defined below) +# +# Note: This may fail to properly report the current state as a result of +# some other guest processes having issued an fs freeze/thaw. +# +# Since: 0.15.0 +## +{ 'command': 'guest-fsfreeze-status', + 'returns': 'GuestFsfreezeStatus' } + +## +# @guest-fsfreeze-freeze: +# +# Sync and freeze all freezable, local guest filesystems +# +# Returns: Number of file systems currently frozen. On error, all filesystems +# will be thawed. +# +# Since: 0.15.0 +## +{ 'command': 'guest-fsfreeze-freeze', + 'returns': 'int' } + +## +# @guest-fsfreeze-thaw: +# +# Unfreeze all frozen guest filesystems +# +# Returns: Number of file systems thawed by this call +# +# Note: if return value does not match the previous call to +# guest-fsfreeze-freeze, this likely means some freezable +# filesystems were unfrozen before this call, and that the +# filesystem state may have changed before issuing this +# command. +# +# Since: 0.15.0 +## +{ 'command': 'guest-fsfreeze-thaw', + 'returns': 'int' } + +## +# @guest-fstrim: +# +# Discard (or "trim") blocks which are not in use by the filesystem. +# +# @minimum: +# Minimum contiguous free range to discard, in bytes. Free ranges +# smaller than this may be ignored (this is a hint and the guest +# may not respect it). By increasing this value, the fstrim +# operation will complete more quickly for filesystems with badly +# fragmented free space, although not all blocks will be discarded. +# The default value is zero, meaning "discard every free block". +# +# Returns: Nothing. +# +# Since: 1.2 +## +{ 'command': 'guest-fstrim', + 'data': { '*minimum': 'int' } } + +## +# @guest-suspend-disk +# +# Suspend guest to disk. +# +# This command tries to execute the scripts provided by the pm-utils package. +# If it's not available, the suspend operation will be performed 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. +# +# This command does NOT return a response on success. There is a high chance +# the command succeeded if the VM exits with a zero exit status or, when +# running with --no-shutdown, by issuing the query-status QMP command to +# to confirm the VM status is "shutdown". However, the VM could also exit +# (or set its status to "shutdown") due to other reasons. +# +# The following errors may be returned: +# If suspend to disk is not supported, Unsupported +# +# Notes: It's strongly recommended to issue the guest-sync command before +# sending commands when the guest resumes +# +# Since: 1.1 +## +{ 'command': 'guest-suspend-disk', 'success-response': 'no' } + +## +# @guest-suspend-ram +# +# Suspend guest to ram. +# +# This command tries to execute the scripts provided by the pm-utils package. +# If it's not available, the suspend operation will be performed 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. +# +# IMPORTANT: guest-suspend-ram requires QEMU to support the 'system_wakeup' +# command. Thus, it's *required* to query QEMU for the presence of the +# 'system_wakeup' command before issuing guest-suspend-ram. +# +# This command does NOT return a response on success. There are two options +# to check for success: +# 1. Wait for the SUSPEND QMP event from QEMU +# 2. Issue the query-status QMP command to confirm the VM status is +# "suspended" +# +# The following errors may be returned: +# If suspend to ram is not supported, Unsupported +# +# Notes: It's strongly recommended to issue the guest-sync command before +# sending commands when the guest resumes +# +# Since: 1.1 +## +{ 'command': 'guest-suspend-ram', 'success-response': 'no' } + +## +# @guest-suspend-hybrid +# +# Save guest state to disk and suspend to ram. +# +# This command requires the pm-utils package to be installed in the guest. +# +# IMPORTANT: guest-suspend-hybrid requires QEMU to support the 'system_wakeup' +# command. Thus, it's *required* to query QEMU for the presence of the +# 'system_wakeup' command before issuing guest-suspend-hybrid. +# +# This command does NOT return a response on success. There are two options +# to check for success: +# 1. Wait for the SUSPEND QMP event from QEMU +# 2. Issue the query-status QMP command to confirm the VM status is +# "suspended" +# +# The following errors may be returned: +# If hybrid suspend is not supported, Unsupported +# +# Notes: It's strongly recommended to issue the guest-sync command before +# sending commands when the guest resumes +# +# Since: 1.1 +## +{ 'command': 'guest-suspend-hybrid', 'success-response': 'no' } + +## +# @GuestIpAddressType: +# +# An enumeration of supported IP address types +# +# @ipv4: IP version 4 +# +# @ipv6: IP version 6 +# +# Since: 1.1 +## +{ 'enum': 'GuestIpAddressType', + 'data': [ 'ipv4', 'ipv6' ] } + +## +# @GuestIpAddress: +# +# @ip-address: IP address +# +# @ip-address-type: Type of @ip-address (e.g. ipv4, ipv6) +# +# @prefix: Network prefix length of @ip-address +# +# Since: 1.1 +## +{ 'type': 'GuestIpAddress', + 'data': {'ip-address': 'str', + 'ip-address-type': 'GuestIpAddressType', + 'prefix': 'int'} } + +## +# @GuestNetworkInterface: +# +# @name: The name of interface for which info are being delivered +# +# @hardware-address: Hardware address of @name +# +# @ip-addresses: List of addresses assigned to @name +# +# Since: 1.1 +## +{ 'type': 'GuestNetworkInterface', + 'data': {'name': 'str', + '*hardware-address': 'str', + '*ip-addresses': ['GuestIpAddress'] } } + +## +# @guest-network-get-interfaces: +# +# Get list of guest IP addresses, MAC addresses +# and netmasks. +# +# Returns: List of GuestNetworkInfo on success. +# +# Since: 1.1 +## +{ 'command': 'guest-network-get-interfaces', + 'returns': ['GuestNetworkInterface'] } + +## +# @GuestLogicalProcessor: +# +# @logical-id: Arbitrary guest-specific unique identifier of the VCPU. +# +# @online: Whether the VCPU is enabled. +# +# @can-offline: #optional Whether offlining the VCPU is possible. This member +# is always filled in by the guest agent when the structure is +# returned, and always ignored on input (hence it can be omitted +# then). +# +# Since: 1.5 +## +{ 'type': 'GuestLogicalProcessor', + 'data': {'logical-id': 'int', + 'online': 'bool', + '*can-offline': 'bool'} } + +## +# @guest-get-vcpus: +# +# Retrieve the list of the guest's logical processors. +# +# This is a read-only operation. +# +# Returns: The list of all VCPUs the guest knows about. Each VCPU is put on the +# list exactly once, but their order is unspecified. +# +# Since: 1.5 +## +{ 'command': 'guest-get-vcpus', + 'returns': ['GuestLogicalProcessor'] } + +## +# @guest-set-vcpus: +# +# Attempt to reconfigure (currently: enable/disable) logical processors inside +# the guest. +# +# The input list is processed node by node in order. In each node @logical-id +# is used to look up the guest VCPU, for which @online specifies the requested +# state. The set of distinct @logical-id's is only required to be a subset of +# the guest-supported identifiers. There's no restriction on list length or on +# repeating the same @logical-id (with possibly different @online field). +# Preferably the input list should describe a modified subset of +# @guest-get-vcpus' return value. +# +# Returns: The length of the initial sublist that has been successfully +# processed. The guest agent maximizes this value. Possible cases: +# +# 0: if the @vcpus list was empty on input. Guest state +# has not been changed. Otherwise, +# +# Error: processing the first node of @vcpus failed for the +# reason returned. Guest state has not been changed. +# Otherwise, +# +# < length(@vcpus): more than zero initial nodes have been processed, +# but not the entire @vcpus list. Guest state has +# changed accordingly. To retrieve the error +# (assuming it persists), repeat the call with the +# successfully processed initial sublist removed. +# Otherwise, +# +# length(@vcpus): call successful. +# +# Since: 1.5 +## +{ 'command': 'guest-set-vcpus', + 'data': {'vcpus': ['GuestLogicalProcessor'] }, + 'returns': 'int' } diff --git a/qga/service-win32.c b/qga/service-win32.c index 09054565d..aef41f04f 100644 --- a/qga/service-win32.c +++ b/qga/service-win32.c @@ -29,58 +29,136 @@ static int printf_win_error(const char *text) MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (char *)&message, 0, NULL); - n = printf("%s. (Error: %d) %s", text, err, message); + n = fprintf(stderr, "%s. (Error: %d) %s", text, (int)err, message); LocalFree(message); return n; } -int ga_install_service(const char *path, const char *logfile) +/* Windows command line escaping. Based on + * <http://blogs.msdn.com/b/oldnewthing/archive/2010/09/17/10063629.aspx> and + * <http://msdn.microsoft.com/en-us/library/windows/desktop/17w5ykft%28v=vs.85%29.aspx>. + * + * The caller is responsible for initializing @buffer; prior contents are lost. + */ +static const char *win_escape_arg(const char *to_escape, GString *buffer) +{ + size_t backslash_count; + const char *c; + + /* open with a double quote */ + g_string_assign(buffer, "\""); + + backslash_count = 0; + for (c = to_escape; *c != '\0'; ++c) { + switch (*c) { + case '\\': + /* The meaning depends on the first non-backslash character coming + * up. + */ + ++backslash_count; + break; + + case '"': + /* We must escape each pending backslash, then escape the double + * quote. This creates a case of "odd number of backslashes [...] + * followed by a double quotation mark". + */ + while (backslash_count) { + --backslash_count; + g_string_append(buffer, "\\\\"); + } + g_string_append(buffer, "\\\""); + break; + + default: + /* Any pending backslashes are without special meaning, flush them. + * "Backslashes are interpreted literally, unless they immediately + * precede a double quotation mark." + */ + while (backslash_count) { + --backslash_count; + g_string_append_c(buffer, '\\'); + } + g_string_append_c(buffer, *c); + } + } + + /* We're about to close with a double quote in string delimiter role. + * Double all pending backslashes, creating a case of "even number of + * backslashes [...] followed by a double quotation mark". + */ + while (backslash_count) { + --backslash_count; + g_string_append(buffer, "\\\\"); + } + g_string_append_c(buffer, '"'); + + return buffer->str; +} + +int ga_install_service(const char *path, const char *logfile, + const char *state_dir) { + int ret = EXIT_FAILURE; SC_HANDLE manager; SC_HANDLE service; - TCHAR cmdline[MAX_PATH]; + TCHAR module_fname[MAX_PATH]; + GString *esc; + GString *cmdline; + SERVICE_DESCRIPTION desc = { (char *)QGA_SERVICE_DESCRIPTION }; - if (GetModuleFileName(NULL, cmdline, MAX_PATH) == 0) { + if (GetModuleFileName(NULL, module_fname, MAX_PATH) == 0) { printf_win_error("No full path to service's executable"); return EXIT_FAILURE; } - _snprintf(cmdline, MAX_PATH - strlen(cmdline), "%s -d", cmdline); + esc = g_string_new(""); + cmdline = g_string_new(""); + + g_string_append_printf(cmdline, "%s -d", + win_escape_arg(module_fname, esc)); if (path) { - _snprintf(cmdline, MAX_PATH - strlen(cmdline), "%s -p %s", cmdline, path); + g_string_append_printf(cmdline, " -p %s", win_escape_arg(path, esc)); } if (logfile) { - _snprintf(cmdline, MAX_PATH - strlen(cmdline), "%s -l %s -v", - cmdline, logfile); + g_string_append_printf(cmdline, " -l %s -v", + win_escape_arg(logfile, esc)); + } + if (state_dir) { + g_string_append_printf(cmdline, " -t %s", + win_escape_arg(state_dir, esc)); } - g_debug("service's cmdline: %s", cmdline); + g_debug("service's cmdline: %s", cmdline->str); manager = OpenSCManager(NULL, NULL, SC_MANAGER_ALL_ACCESS); if (manager == NULL) { printf_win_error("No handle to service control manager"); - return EXIT_FAILURE; + goto out_strings; } service = CreateService(manager, QGA_SERVICE_NAME, QGA_SERVICE_DISPLAY_NAME, SERVICE_ALL_ACCESS, SERVICE_WIN32_OWN_PROCESS, SERVICE_AUTO_START, - SERVICE_ERROR_NORMAL, cmdline, NULL, NULL, NULL, NULL, NULL); - - if (service) { - SERVICE_DESCRIPTION desc = { (char *)QGA_SERVICE_DESCRIPTION }; - ChangeServiceConfig2(service, SERVICE_CONFIG_DESCRIPTION, &desc); - - printf("Service was installed successfully.\n"); - } else { + SERVICE_ERROR_NORMAL, cmdline->str, NULL, NULL, NULL, NULL, NULL); + if (service == NULL) { printf_win_error("Failed to install service"); + goto out_manager; } + ChangeServiceConfig2(service, SERVICE_CONFIG_DESCRIPTION, &desc); + fprintf(stderr, "Service was installed successfully.\n"); + ret = EXIT_SUCCESS; CloseServiceHandle(service); + +out_manager: CloseServiceHandle(manager); - return (service == NULL); +out_strings: + g_string_free(cmdline, TRUE); + g_string_free(esc, TRUE); + return ret; } int ga_uninstall_service(void) @@ -104,7 +182,7 @@ int ga_uninstall_service(void) if (DeleteService(service) == FALSE) { printf_win_error("Failed to delete service"); } else { - printf("Service was deleted successfully.\n"); + fprintf(stderr, "Service was deleted successfully.\n"); } CloseServiceHandle(service); diff --git a/qga/service-win32.h b/qga/service-win32.h index 99dfc5334..3b9e87024 100644 --- a/qga/service-win32.h +++ b/qga/service-win32.h @@ -24,7 +24,8 @@ typedef struct GAService { SERVICE_STATUS_HANDLE status_handle; } GAService; -int ga_install_service(const char *path, const char *logfile); +int ga_install_service(const char *path, const char *logfile, + const char *state_dir); int ga_uninstall_service(void); #endif |