1 /* 2 * QEMU Guest Agent win32-specific command implementations 3 * 4 * Copyright IBM Corp. 2012 5 * 6 * Authors: 7 * Michael Roth <mdroth@linux.vnet.ibm.com> 8 * Gal Hammer <ghammer@redhat.com> 9 * 10 * This work is licensed under the terms of the GNU GPL, version 2 or later. 11 * See the COPYING file in the top-level directory. 12 */ 13 14 #include <glib.h> 15 #include <wtypes.h> 16 #include <powrprof.h> 17 #include <stdio.h> 18 #include <string.h> 19 #include "qga/guest-agent-core.h" 20 #include "qga/vss-win32.h" 21 #include "qga-qmp-commands.h" 22 #include "qapi/qmp/qerror.h" 23 #include "qemu/queue.h" 24 25 #ifndef SHTDN_REASON_FLAG_PLANNED 26 #define SHTDN_REASON_FLAG_PLANNED 0x80000000 27 #endif 28 29 /* multiple of 100 nanoseconds elapsed between windows baseline 30 * (1/1/1601) and Unix Epoch (1/1/1970), accounting for leap years */ 31 #define W32_FT_OFFSET (10000000ULL * 60 * 60 * 24 * \ 32 (365 * (1970 - 1601) + \ 33 (1970 - 1601) / 4 - 3)) 34 35 #define INVALID_SET_FILE_POINTER ((DWORD)-1) 36 37 typedef struct GuestFileHandle { 38 int64_t id; 39 HANDLE fh; 40 QTAILQ_ENTRY(GuestFileHandle) next; 41 } GuestFileHandle; 42 43 static struct { 44 QTAILQ_HEAD(, GuestFileHandle) filehandles; 45 } guest_file_state; 46 47 48 typedef struct OpenFlags { 49 const char *forms; 50 DWORD desired_access; 51 DWORD creation_disposition; 52 } OpenFlags; 53 static OpenFlags guest_file_open_modes[] = { 54 {"r", GENERIC_READ, OPEN_EXISTING}, 55 {"rb", GENERIC_READ, OPEN_EXISTING}, 56 {"w", GENERIC_WRITE, CREATE_ALWAYS}, 57 {"wb", GENERIC_WRITE, CREATE_ALWAYS}, 58 {"a", GENERIC_WRITE, OPEN_ALWAYS }, 59 {"r+", GENERIC_WRITE|GENERIC_READ, OPEN_EXISTING}, 60 {"rb+", GENERIC_WRITE|GENERIC_READ, OPEN_EXISTING}, 61 {"r+b", GENERIC_WRITE|GENERIC_READ, OPEN_EXISTING}, 62 {"w+", GENERIC_WRITE|GENERIC_READ, CREATE_ALWAYS}, 63 {"wb+", GENERIC_WRITE|GENERIC_READ, CREATE_ALWAYS}, 64 {"w+b", GENERIC_WRITE|GENERIC_READ, CREATE_ALWAYS}, 65 {"a+", GENERIC_WRITE|GENERIC_READ, OPEN_ALWAYS }, 66 {"ab+", GENERIC_WRITE|GENERIC_READ, OPEN_ALWAYS }, 67 {"a+b", GENERIC_WRITE|GENERIC_READ, OPEN_ALWAYS } 68 }; 69 70 static OpenFlags *find_open_flag(const char *mode_str) 71 { 72 int mode; 73 Error **errp = NULL; 74 75 for (mode = 0; mode < ARRAY_SIZE(guest_file_open_modes); ++mode) { 76 OpenFlags *flags = guest_file_open_modes + mode; 77 78 if (strcmp(flags->forms, mode_str) == 0) { 79 return flags; 80 } 81 } 82 83 error_setg(errp, "invalid file open mode '%s'", mode_str); 84 return NULL; 85 } 86 87 static int64_t guest_file_handle_add(HANDLE fh, Error **errp) 88 { 89 GuestFileHandle *gfh; 90 int64_t handle; 91 92 handle = ga_get_fd_handle(ga_state, errp); 93 if (handle < 0) { 94 return -1; 95 } 96 gfh = g_malloc0(sizeof(GuestFileHandle)); 97 gfh->id = handle; 98 gfh->fh = fh; 99 QTAILQ_INSERT_TAIL(&guest_file_state.filehandles, gfh, next); 100 101 return handle; 102 } 103 104 static GuestFileHandle *guest_file_handle_find(int64_t id, Error **errp) 105 { 106 GuestFileHandle *gfh; 107 QTAILQ_FOREACH(gfh, &guest_file_state.filehandles, next) { 108 if (gfh->id == id) { 109 return gfh; 110 } 111 } 112 error_setg(errp, "handle '%" PRId64 "' has not been found", id); 113 return NULL; 114 } 115 116 int64_t qmp_guest_file_open(const char *path, bool has_mode, 117 const char *mode, Error **errp) 118 { 119 int64_t fd; 120 HANDLE fh; 121 HANDLE templ_file = NULL; 122 DWORD share_mode = FILE_SHARE_READ; 123 DWORD flags_and_attr = FILE_ATTRIBUTE_NORMAL; 124 LPSECURITY_ATTRIBUTES sa_attr = NULL; 125 OpenFlags *guest_flags; 126 127 if (!has_mode) { 128 mode = "r"; 129 } 130 slog("guest-file-open called, filepath: %s, mode: %s", path, mode); 131 guest_flags = find_open_flag(mode); 132 if (guest_flags == NULL) { 133 error_setg(errp, "invalid file open mode"); 134 return -1; 135 } 136 137 fh = CreateFile(path, guest_flags->desired_access, share_mode, sa_attr, 138 guest_flags->creation_disposition, flags_and_attr, 139 templ_file); 140 if (fh == INVALID_HANDLE_VALUE) { 141 error_setg_win32(errp, GetLastError(), "failed to open file '%s'", 142 path); 143 return -1; 144 } 145 146 fd = guest_file_handle_add(fh, errp); 147 if (fd < 0) { 148 CloseHandle(&fh); 149 error_setg(errp, "failed to add handle to qmp handle table"); 150 return -1; 151 } 152 153 slog("guest-file-open, handle: % " PRId64, fd); 154 return fd; 155 } 156 157 void qmp_guest_file_close(int64_t handle, Error **errp) 158 { 159 bool ret; 160 GuestFileHandle *gfh = guest_file_handle_find(handle, errp); 161 slog("guest-file-close called, handle: %" PRId64, handle); 162 if (gfh == NULL) { 163 return; 164 } 165 ret = CloseHandle(gfh->fh); 166 if (!ret) { 167 error_setg_win32(errp, GetLastError(), "failed close handle"); 168 return; 169 } 170 171 QTAILQ_REMOVE(&guest_file_state.filehandles, gfh, next); 172 g_free(gfh); 173 } 174 175 static void acquire_privilege(const char *name, Error **errp) 176 { 177 HANDLE token = NULL; 178 TOKEN_PRIVILEGES priv; 179 Error *local_err = NULL; 180 181 if (OpenProcessToken(GetCurrentProcess(), 182 TOKEN_ADJUST_PRIVILEGES|TOKEN_QUERY, &token)) 183 { 184 if (!LookupPrivilegeValue(NULL, name, &priv.Privileges[0].Luid)) { 185 error_set(&local_err, QERR_QGA_COMMAND_FAILED, 186 "no luid for requested privilege"); 187 goto out; 188 } 189 190 priv.PrivilegeCount = 1; 191 priv.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED; 192 193 if (!AdjustTokenPrivileges(token, FALSE, &priv, 0, NULL, 0)) { 194 error_set(&local_err, QERR_QGA_COMMAND_FAILED, 195 "unable to acquire requested privilege"); 196 goto out; 197 } 198 199 } else { 200 error_set(&local_err, QERR_QGA_COMMAND_FAILED, 201 "failed to open privilege token"); 202 } 203 204 out: 205 if (token) { 206 CloseHandle(token); 207 } 208 if (local_err) { 209 error_propagate(errp, local_err); 210 } 211 } 212 213 static void execute_async(DWORD WINAPI (*func)(LPVOID), LPVOID opaque, 214 Error **errp) 215 { 216 Error *local_err = NULL; 217 218 HANDLE thread = CreateThread(NULL, 0, func, opaque, 0, NULL); 219 if (!thread) { 220 error_set(&local_err, QERR_QGA_COMMAND_FAILED, 221 "failed to dispatch asynchronous command"); 222 error_propagate(errp, local_err); 223 } 224 } 225 226 void qmp_guest_shutdown(bool has_mode, const char *mode, Error **errp) 227 { 228 Error *local_err = NULL; 229 UINT shutdown_flag = EWX_FORCE; 230 231 slog("guest-shutdown called, mode: %s", mode); 232 233 if (!has_mode || strcmp(mode, "powerdown") == 0) { 234 shutdown_flag |= EWX_POWEROFF; 235 } else if (strcmp(mode, "halt") == 0) { 236 shutdown_flag |= EWX_SHUTDOWN; 237 } else if (strcmp(mode, "reboot") == 0) { 238 shutdown_flag |= EWX_REBOOT; 239 } else { 240 error_set(errp, QERR_INVALID_PARAMETER_VALUE, "mode", 241 "halt|powerdown|reboot"); 242 return; 243 } 244 245 /* Request a shutdown privilege, but try to shut down the system 246 anyway. */ 247 acquire_privilege(SE_SHUTDOWN_NAME, &local_err); 248 if (local_err) { 249 error_propagate(errp, local_err); 250 return; 251 } 252 253 if (!ExitWindowsEx(shutdown_flag, SHTDN_REASON_FLAG_PLANNED)) { 254 slog("guest-shutdown failed: %lu", GetLastError()); 255 error_set(errp, QERR_UNDEFINED_ERROR); 256 } 257 } 258 259 GuestFileRead *qmp_guest_file_read(int64_t handle, bool has_count, 260 int64_t count, Error **errp) 261 { 262 GuestFileRead *read_data = NULL; 263 guchar *buf; 264 HANDLE fh; 265 bool is_ok; 266 DWORD read_count; 267 GuestFileHandle *gfh = guest_file_handle_find(handle, errp); 268 269 if (!gfh) { 270 return NULL; 271 } 272 if (!has_count) { 273 count = QGA_READ_COUNT_DEFAULT; 274 } else if (count < 0) { 275 error_setg(errp, "value '%" PRId64 276 "' is invalid for argument count", count); 277 return NULL; 278 } 279 280 fh = gfh->fh; 281 buf = g_malloc0(count+1); 282 is_ok = ReadFile(fh, buf, count, &read_count, NULL); 283 if (!is_ok) { 284 error_setg_win32(errp, GetLastError(), "failed to read file"); 285 slog("guest-file-read failed, handle %" PRId64, handle); 286 } else { 287 buf[read_count] = 0; 288 read_data = g_malloc0(sizeof(GuestFileRead)); 289 read_data->count = (size_t)read_count; 290 read_data->eof = read_count == 0; 291 292 if (read_count != 0) { 293 read_data->buf_b64 = g_base64_encode(buf, read_count); 294 } 295 } 296 g_free(buf); 297 298 return read_data; 299 } 300 301 GuestFileWrite *qmp_guest_file_write(int64_t handle, const char *buf_b64, 302 bool has_count, int64_t count, 303 Error **errp) 304 { 305 GuestFileWrite *write_data = NULL; 306 guchar *buf; 307 gsize buf_len; 308 bool is_ok; 309 DWORD write_count; 310 GuestFileHandle *gfh = guest_file_handle_find(handle, errp); 311 HANDLE fh; 312 313 if (!gfh) { 314 return NULL; 315 } 316 fh = gfh->fh; 317 buf = g_base64_decode(buf_b64, &buf_len); 318 319 if (!has_count) { 320 count = buf_len; 321 } else if (count < 0 || count > buf_len) { 322 error_setg(errp, "value '%" PRId64 323 "' is invalid for argument count", count); 324 goto done; 325 } 326 327 is_ok = WriteFile(fh, buf, count, &write_count, NULL); 328 if (!is_ok) { 329 error_setg_win32(errp, GetLastError(), "failed to write to file"); 330 slog("guest-file-write-failed, handle: %" PRId64, handle); 331 } else { 332 write_data = g_malloc0(sizeof(GuestFileWrite)); 333 write_data->count = (size_t) write_count; 334 } 335 336 done: 337 g_free(buf); 338 return write_data; 339 } 340 341 GuestFileSeek *qmp_guest_file_seek(int64_t handle, int64_t offset, 342 int64_t whence, Error **errp) 343 { 344 GuestFileHandle *gfh; 345 GuestFileSeek *seek_data; 346 HANDLE fh; 347 LARGE_INTEGER new_pos, off_pos; 348 off_pos.QuadPart = offset; 349 BOOL res; 350 gfh = guest_file_handle_find(handle, errp); 351 if (!gfh) { 352 return NULL; 353 } 354 355 fh = gfh->fh; 356 res = SetFilePointerEx(fh, off_pos, &new_pos, whence); 357 if (!res) { 358 error_setg_win32(errp, GetLastError(), "failed to seek file"); 359 return NULL; 360 } 361 seek_data = g_new0(GuestFileSeek, 1); 362 seek_data->position = new_pos.QuadPart; 363 return seek_data; 364 } 365 366 void qmp_guest_file_flush(int64_t handle, Error **errp) 367 { 368 HANDLE fh; 369 GuestFileHandle *gfh = guest_file_handle_find(handle, errp); 370 if (!gfh) { 371 return; 372 } 373 374 fh = gfh->fh; 375 if (!FlushFileBuffers(fh)) { 376 error_setg_win32(errp, GetLastError(), "failed to flush file"); 377 } 378 } 379 380 static void guest_file_init(void) 381 { 382 QTAILQ_INIT(&guest_file_state.filehandles); 383 } 384 385 GuestFilesystemInfoList *qmp_guest_get_fsinfo(Error **errp) 386 { 387 error_set(errp, QERR_UNSUPPORTED); 388 return NULL; 389 } 390 391 /* 392 * Return status of freeze/thaw 393 */ 394 GuestFsfreezeStatus qmp_guest_fsfreeze_status(Error **errp) 395 { 396 if (!vss_initialized()) { 397 error_set(errp, QERR_UNSUPPORTED); 398 return 0; 399 } 400 401 if (ga_is_frozen(ga_state)) { 402 return GUEST_FSFREEZE_STATUS_FROZEN; 403 } 404 405 return GUEST_FSFREEZE_STATUS_THAWED; 406 } 407 408 /* 409 * Freeze local file systems using Volume Shadow-copy Service. 410 * The frozen state is limited for up to 10 seconds by VSS. 411 */ 412 int64_t qmp_guest_fsfreeze_freeze(Error **errp) 413 { 414 int i; 415 Error *local_err = NULL; 416 417 if (!vss_initialized()) { 418 error_set(errp, QERR_UNSUPPORTED); 419 return 0; 420 } 421 422 slog("guest-fsfreeze called"); 423 424 /* cannot risk guest agent blocking itself on a write in this state */ 425 ga_set_frozen(ga_state); 426 427 qga_vss_fsfreeze(&i, &local_err, true); 428 if (local_err) { 429 error_propagate(errp, local_err); 430 goto error; 431 } 432 433 return i; 434 435 error: 436 local_err = NULL; 437 qmp_guest_fsfreeze_thaw(&local_err); 438 if (local_err) { 439 g_debug("cleanup thaw: %s", error_get_pretty(local_err)); 440 error_free(local_err); 441 } 442 return 0; 443 } 444 445 int64_t qmp_guest_fsfreeze_freeze_list(bool has_mountpoints, 446 strList *mountpoints, 447 Error **errp) 448 { 449 error_set(errp, QERR_UNSUPPORTED); 450 451 return 0; 452 } 453 454 /* 455 * Thaw local file systems using Volume Shadow-copy Service. 456 */ 457 int64_t qmp_guest_fsfreeze_thaw(Error **errp) 458 { 459 int i; 460 461 if (!vss_initialized()) { 462 error_set(errp, QERR_UNSUPPORTED); 463 return 0; 464 } 465 466 qga_vss_fsfreeze(&i, errp, false); 467 468 ga_unset_frozen(ga_state); 469 return i; 470 } 471 472 static void guest_fsfreeze_cleanup(void) 473 { 474 Error *err = NULL; 475 476 if (!vss_initialized()) { 477 return; 478 } 479 480 if (ga_is_frozen(ga_state) == GUEST_FSFREEZE_STATUS_FROZEN) { 481 qmp_guest_fsfreeze_thaw(&err); 482 if (err) { 483 slog("failed to clean up frozen filesystems: %s", 484 error_get_pretty(err)); 485 error_free(err); 486 } 487 } 488 489 vss_deinit(true); 490 } 491 492 /* 493 * Walk list of mounted file systems in the guest, and discard unused 494 * areas. 495 */ 496 void qmp_guest_fstrim(bool has_minimum, int64_t minimum, Error **errp) 497 { 498 error_set(errp, QERR_UNSUPPORTED); 499 } 500 501 typedef enum { 502 GUEST_SUSPEND_MODE_DISK, 503 GUEST_SUSPEND_MODE_RAM 504 } GuestSuspendMode; 505 506 static void check_suspend_mode(GuestSuspendMode mode, Error **errp) 507 { 508 SYSTEM_POWER_CAPABILITIES sys_pwr_caps; 509 Error *local_err = NULL; 510 511 ZeroMemory(&sys_pwr_caps, sizeof(sys_pwr_caps)); 512 if (!GetPwrCapabilities(&sys_pwr_caps)) { 513 error_set(&local_err, QERR_QGA_COMMAND_FAILED, 514 "failed to determine guest suspend capabilities"); 515 goto out; 516 } 517 518 switch (mode) { 519 case GUEST_SUSPEND_MODE_DISK: 520 if (!sys_pwr_caps.SystemS4) { 521 error_set(&local_err, QERR_QGA_COMMAND_FAILED, 522 "suspend-to-disk not supported by OS"); 523 } 524 break; 525 case GUEST_SUSPEND_MODE_RAM: 526 if (!sys_pwr_caps.SystemS3) { 527 error_set(&local_err, QERR_QGA_COMMAND_FAILED, 528 "suspend-to-ram not supported by OS"); 529 } 530 break; 531 default: 532 error_set(&local_err, QERR_INVALID_PARAMETER_VALUE, "mode", 533 "GuestSuspendMode"); 534 } 535 536 out: 537 if (local_err) { 538 error_propagate(errp, local_err); 539 } 540 } 541 542 static DWORD WINAPI do_suspend(LPVOID opaque) 543 { 544 GuestSuspendMode *mode = opaque; 545 DWORD ret = 0; 546 547 if (!SetSuspendState(*mode == GUEST_SUSPEND_MODE_DISK, TRUE, TRUE)) { 548 slog("failed to suspend guest, %lu", GetLastError()); 549 ret = -1; 550 } 551 g_free(mode); 552 return ret; 553 } 554 555 void qmp_guest_suspend_disk(Error **errp) 556 { 557 Error *local_err = NULL; 558 GuestSuspendMode *mode = g_malloc(sizeof(GuestSuspendMode)); 559 560 *mode = GUEST_SUSPEND_MODE_DISK; 561 check_suspend_mode(*mode, &local_err); 562 acquire_privilege(SE_SHUTDOWN_NAME, &local_err); 563 execute_async(do_suspend, mode, &local_err); 564 565 if (local_err) { 566 error_propagate(errp, local_err); 567 g_free(mode); 568 } 569 } 570 571 void qmp_guest_suspend_ram(Error **errp) 572 { 573 Error *local_err = NULL; 574 GuestSuspendMode *mode = g_malloc(sizeof(GuestSuspendMode)); 575 576 *mode = GUEST_SUSPEND_MODE_RAM; 577 check_suspend_mode(*mode, &local_err); 578 acquire_privilege(SE_SHUTDOWN_NAME, &local_err); 579 execute_async(do_suspend, mode, &local_err); 580 581 if (local_err) { 582 error_propagate(errp, local_err); 583 g_free(mode); 584 } 585 } 586 587 void qmp_guest_suspend_hybrid(Error **errp) 588 { 589 error_set(errp, QERR_UNSUPPORTED); 590 } 591 592 GuestNetworkInterfaceList *qmp_guest_network_get_interfaces(Error **errp) 593 { 594 error_set(errp, QERR_UNSUPPORTED); 595 return NULL; 596 } 597 598 int64_t qmp_guest_get_time(Error **errp) 599 { 600 SYSTEMTIME ts = {0}; 601 int64_t time_ns; 602 FILETIME tf; 603 604 GetSystemTime(&ts); 605 if (ts.wYear < 1601 || ts.wYear > 30827) { 606 error_setg(errp, "Failed to get time"); 607 return -1; 608 } 609 610 if (!SystemTimeToFileTime(&ts, &tf)) { 611 error_setg(errp, "Failed to convert system time: %d", (int)GetLastError()); 612 return -1; 613 } 614 615 time_ns = ((((int64_t)tf.dwHighDateTime << 32) | tf.dwLowDateTime) 616 - W32_FT_OFFSET) * 100; 617 618 return time_ns; 619 } 620 621 void qmp_guest_set_time(bool has_time, int64_t time_ns, Error **errp) 622 { 623 Error *local_err = NULL; 624 SYSTEMTIME ts; 625 FILETIME tf; 626 LONGLONG time; 627 628 if (!has_time) { 629 /* Unfortunately, Windows libraries don't provide an easy way to access 630 * RTC yet: 631 * 632 * https://msdn.microsoft.com/en-us/library/aa908981.aspx 633 */ 634 error_setg(errp, "Time argument is required on this platform"); 635 return; 636 } 637 638 /* Validate time passed by user. */ 639 if (time_ns < 0 || time_ns / 100 > INT64_MAX - W32_FT_OFFSET) { 640 error_setg(errp, "Time %" PRId64 "is invalid", time_ns); 641 return; 642 } 643 644 time = time_ns / 100 + W32_FT_OFFSET; 645 646 tf.dwLowDateTime = (DWORD) time; 647 tf.dwHighDateTime = (DWORD) (time >> 32); 648 649 if (!FileTimeToSystemTime(&tf, &ts)) { 650 error_setg(errp, "Failed to convert system time %d", 651 (int)GetLastError()); 652 return; 653 } 654 655 acquire_privilege(SE_SYSTEMTIME_NAME, &local_err); 656 if (local_err) { 657 error_propagate(errp, local_err); 658 return; 659 } 660 661 if (!SetSystemTime(&ts)) { 662 error_setg(errp, "Failed to set time to guest: %d", (int)GetLastError()); 663 return; 664 } 665 } 666 667 GuestLogicalProcessorList *qmp_guest_get_vcpus(Error **errp) 668 { 669 error_set(errp, QERR_UNSUPPORTED); 670 return NULL; 671 } 672 673 int64_t qmp_guest_set_vcpus(GuestLogicalProcessorList *vcpus, Error **errp) 674 { 675 error_set(errp, QERR_UNSUPPORTED); 676 return -1; 677 } 678 679 void qmp_guest_set_user_password(const char *username, 680 const char *password, 681 bool crypted, 682 Error **errp) 683 { 684 error_set(errp, QERR_UNSUPPORTED); 685 } 686 687 GuestMemoryBlockList *qmp_guest_get_memory_blocks(Error **errp) 688 { 689 error_set(errp, QERR_UNSUPPORTED); 690 return NULL; 691 } 692 693 GuestMemoryBlockResponseList * 694 qmp_guest_set_memory_blocks(GuestMemoryBlockList *mem_blks, Error **errp) 695 { 696 error_set(errp, QERR_UNSUPPORTED); 697 return NULL; 698 } 699 700 GuestMemoryBlockInfo *qmp_guest_get_memory_block_info(Error **errp) 701 { 702 error_set(errp, QERR_UNSUPPORTED); 703 return NULL; 704 } 705 706 /* add unsupported commands to the blacklist */ 707 GList *ga_command_blacklist_init(GList *blacklist) 708 { 709 const char *list_unsupported[] = { 710 "guest-suspend-hybrid", "guest-network-get-interfaces", 711 "guest-get-vcpus", "guest-set-vcpus", 712 "guest-set-user-password", 713 "guest-get-memory-blocks", "guest-set-memory-blocks", 714 "guest-get-memory-block-size", 715 "guest-fsfreeze-freeze-list", "guest-get-fsinfo", 716 "guest-fstrim", NULL}; 717 char **p = (char **)list_unsupported; 718 719 while (*p) { 720 blacklist = g_list_append(blacklist, *p++); 721 } 722 723 if (!vss_init(true)) { 724 const char *list[] = { 725 "guest-get-fsinfo", "guest-fsfreeze-status", 726 "guest-fsfreeze-freeze", "guest-fsfreeze-thaw", NULL}; 727 p = (char **)list; 728 729 while (*p) { 730 blacklist = g_list_append(blacklist, *p++); 731 } 732 } 733 734 return blacklist; 735 } 736 737 /* register init/cleanup routines for stateful command groups */ 738 void ga_command_state_init(GAState *s, GACommandState *cs) 739 { 740 if (!vss_initialized()) { 741 ga_command_state_add(cs, NULL, guest_fsfreeze_cleanup); 742 } 743 ga_command_state_add(cs, guest_file_init, NULL); 744 } 745