diff options
author | Vladislav Vaintroub <wlad@montyprogram.com> | 2011-01-29 19:00:05 +0100 |
---|---|---|
committer | Vladislav Vaintroub <wlad@montyprogram.com> | 2011-01-29 19:00:05 +0100 |
commit | 2bc6032c994a7f8f72d4c2ba064cf3e109df2487 (patch) | |
tree | 3c7b0383d461604a78cf962430958ff1bdbbf427 /sql/mysql_upgrade_service.cc | |
parent | 1a3115dbb25fbffd998284dd3617eb637fa44970 (diff) | |
download | mariadb-git-2bc6032c994a7f8f72d4c2ba064cf3e109df2487.tar.gz |
MWL#55 - mysql_upgrade_service.exe
New utility to upgrade Windows service to higher MariaDB version.
Its functionality includes changing service definition as well as
running mysql_upgrade.
Diffstat (limited to 'sql/mysql_upgrade_service.cc')
-rw-r--r-- | sql/mysql_upgrade_service.cc | 550 |
1 files changed, 550 insertions, 0 deletions
diff --git a/sql/mysql_upgrade_service.cc b/sql/mysql_upgrade_service.cc new file mode 100644 index 00000000000..c688bb8061d --- /dev/null +++ b/sql/mysql_upgrade_service.cc @@ -0,0 +1,550 @@ +/* Copyright (C) 2010 Monty Program Ab + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; version 2 of the License. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ + +/* + mysql_upgrade_service upgrades mysql service on Windows. + It changes service definition to point to the new mysqld.exe, restarts the + server and runs mysql_upgrade +*/ + +#define DONT_DEFINE_VOID +#include <process.h> +#include <my_global.h> +#include <my_getopt.h> +#include <my_sys.h> +#include <m_string.h> +#include <mysql_version.h> + +#include <stdlib.h> +#include <stdio.h> +#include <windows.h> + +/* We're using version APIs */ +#pragma comment(lib, "version") + +static char mysqld_path[MAX_PATH]; +static char mysqladmin_path[MAX_PATH]; +static char mysqlupgrade_path[MAX_PATH]; + +static char defaults_file_param[FN_REFLEN]; +static char logfile_path[FN_REFLEN]; +static char *opt_service; +static SC_HANDLE service; +static SC_HANDLE scm; +HANDLE mysqld_process; // mysqld.exe started for upgrade +DWORD initial_service_state= -1; // initial state of the service +HANDLE logfile_handle; + +/* + Startup and shutdown timeouts, in seconds. + Maybe,they can be made parameters +*/ +static unsigned int startup_timeout= 60; +static unsigned int shutdown_timeout= 60; + +static struct my_option my_long_options[]= +{ + {"help", '?', "Display this help message and exit.", 0, 0, 0, GET_NO_ARG, + NO_ARG, 0, 0, 0, 0, 0, 0}, + {"service", 's', "Name of the existing Windows service", + &opt_service, &opt_service, 0, GET_STR, REQUIRED_ARG, 0, 0, 0, 0, 0, 0}, +}; + + + +static my_bool +get_one_option(int optid, + const struct my_option *opt __attribute__ ((unused)), + char *argument __attribute__ ((unused))) +{ + DBUG_ENTER("get_one_option"); + switch (optid) { + case '?': + my_print_help(my_long_options); + exit(0); + break; + } + DBUG_RETURN(0); +} + + + +static void log(const char *fmt, ...) +{ + va_list args; + char buf[4096]; + + /* Print the error message */ + va_start(args, fmt); + if (fmt) + { + vsprintf_s(buf, fmt, args); + fprintf(stdout, "%s\n", buf); + } + va_end(args); + my_end(0); +} + + +static void die(const char *fmt, ...) +{ + va_list args; + DBUG_ENTER("die"); + char buf[4096]; + + /* Print the error message */ + va_start(args, fmt); + if (fmt) + { + fprintf(stderr, "FATAL ERROR: "); + int count= vsprintf_s(buf, fmt, args); + fprintf(stderr, "%s.", buf); + if(logfile_path[0]) + { + fprintf(stderr, "Additional information can be found in the log file %s", + logfile_path); + } + } + va_end(args); + + /* Cleanup */ + if(service && initial_service_state != -1) + { + /* Stop service if it was not running */ + if(initial_service_state != SERVICE_RUNNING) + { + SERVICE_STATUS service_status; + ControlService(service, SERVICE_CONTROL_STOP, &service_status); + } + CloseServiceHandle(service); + } + if(scm) + CloseServiceHandle(scm); + + /* Stop mysqld.exe if it was started for upgrade */ + if(mysqld_process) + TerminateProcess(mysqld_process, 3); + if(logfile_handle) + CloseHandle(logfile_handle); + my_end(0); + exit(1); +} + +/* + spawn-like function to run subprocesses. + We also redirect the full output to the log file. + + Typical usage could be something like + run_tool(P_NOWAIT, "cmd.exe", "/c" , "echo", "foo", NULL) + + @param wait_flag (P_WAIT or P_NOWAIT) + @program program to run + + Rest of the parameters is NULL terminated strings building command line. + + @return intptr containing either process handle, if P_NOWAIT is used + or return code of the process (if P_WAIT is used) +*/ +static intptr_t run_tool(int wait_flag, const char *program,...) +{ + static char cmdline[32*1024]; + va_list args; + va_start(args, program); + if(!program) + die("Invalid call to run_tool"); + + strcpy_s(cmdline, "\""); + strcat_s(cmdline, program); + strcat_s(cmdline, "\""); + for(;;) + { + char *param= va_arg(args,char *); + if(!param) + break; + strcat_s(cmdline, " \""); + strcat_s(cmdline, param); + strcat_s(cmdline, "\""); + } + va_end(args); + + /* Create output file if not alredy done */ + if(!logfile_handle) + { + char tmpdir[FN_REFLEN]; + GetTempPath(FN_REFLEN, tmpdir); + sprintf_s(logfile_path, "%s\\mysql_upgrade_service.%s.log", tmpdir, + opt_service); + logfile_handle = CreateFile(logfile_path, GENERIC_WRITE, FILE_SHARE_READ, + NULL, TRUNCATE_EXISTING, 0, NULL); + if(!logfile_handle) + die("Cannot open log file %s", logfile_path); + } + + /* Start child process */ + STARTUPINFO si={0}; + si.cb= sizeof(si); + si.hStdInput= GetStdHandle(STD_INPUT_HANDLE); + si.hStdError= logfile_handle; + si.hStdOutput= logfile_handle; + si.dwFlags= STARTF_USESTDHANDLES; + PROCESS_INFORMATION pi; + if (!CreateProcess(NULL, cmdline, NULL, + NULL, TRUE, NULL, NULL, NULL, &si, &pi)) + { + die("CreateProcess failed (commandline %s)", cmdline); + } + CloseHandle(pi.hThread); + + if(wait_flag == P_NOWAIT) + { + /* Do not wait for process to complete, return handle */ + return (intptr_t)pi.hProcess; + } + + /* Eait for process to complete */ + if (WaitForSingleObject(pi.hProcess, INFINITE) != WAIT_OBJECT_0) + { + die("WaitForSingleObject() failed"); + } + DWORD exit_code; + if (!GetExitCodeProcess(pi.hProcess, &exit_code)) + { + die("GetExitCodeProcess() failed"); + } + return (intptr_t)exit_code; +} + + + +void stop_mysqld_service() +{ + DWORD needed; + SERVICE_STATUS_PROCESS ssp; + int timeout= shutdown_timeout*1000; + for(;;) + { + if (!QueryServiceStatusEx(service, SC_STATUS_PROCESS_INFO, + (LPBYTE)&ssp, + sizeof(SERVICE_STATUS_PROCESS), + &needed)) + { + die("QueryServiceStatusEx failed (%d)\n", GetLastError()); + } + + /* + Remeber initial state of the service, we will restore it on + exit. + */ + if(initial_service_state == -1) + initial_service_state =ssp.dwCurrentState; + + switch(ssp.dwCurrentState) + { + case SERVICE_STOPPED: + return; + case SERVICE_RUNNING: + if(!ControlService(service, SERVICE_CONTROL_STOP, + (SERVICE_STATUS *)&ssp)) + die("ControlService failed, error %d\n", GetLastError()); + case SERVICE_START_PENDING: + case SERVICE_STOP_PENDING: + if(timeout < 0) + die("Service does not stop after 1 minute timeout"); + Sleep(100); + break; + default: + die("Unexpected service state %d",ssp.dwCurrentState); + } + } +} + + +/* Helper routine. Used to prevent downgrades by mysql_upgrade_service */ +void get_file_version(const wchar_t *path, int *major, int *minor) +{ + *major= *minor=0; + DWORD version_handle; + char *ver= 0; + VS_FIXEDFILEINFO info; + UINT len; + void *p; + + DWORD size = GetFileVersionInfoSizeW(path, &version_handle); + if (size == 0) + return; + ver = new char[size]; + if(!GetFileVersionInfoW(path, version_handle, size, ver)) + goto end; + + if(!VerQueryValue(ver,"\\",&p,&len)) + goto end; + memcpy(&info,p ,sizeof(VS_FIXEDFILEINFO)); + + *major = (info.dwFileVersionMS & 0xFFFF0000) >> 16; + *minor = (info.dwFileVersionMS & 0x0000FFFF); +end: + delete []ver; +} + +/* + Shutdown mysql server. Not using mysqladmin, since + our --skip-grant-tables do not work anymore after mysql_upgrade + that does "flush privileges". Instead, the shutdown handle is set. +*/ +void initiate_mysqld_shutdown() +{ + char event_name[32]; + DWORD pid= GetProcessId(mysqld_process); + sprintf_s(event_name, "MySQLShutdown%d", pid); + HANDLE shutdown_handle= OpenEvent(EVENT_MODIFY_STATE, FALSE, event_name); + if(!shutdown_handle) + { + die("OpenEvent() failed for shutdown event"); + } + + if(!SetEvent(shutdown_handle)) + { + die("SetEvent() failed"); + } +} + + +/* + Change service configuration (binPath) to point to mysqld from + this installation. +*/ +static void change_service_config() +{ + wchar_t old_mysqld_path[MAX_PATH]; + wchar_t *file_part; + char defaults_file[MAX_PATH]; + char default_character_set[64]; + + scm = OpenSCManager(NULL, NULL, SC_MANAGER_ALL_ACCESS); + if(!scm) + die("OpenSCManager failed with %d", GetLastError()); + service= OpenService(scm, opt_service, SERVICE_ALL_ACCESS); + if (!service) + die("OpenService failed with %d", GetLastError()); + + BYTE config_buffer[8*1024]; + LPQUERY_SERVICE_CONFIGW config= (LPQUERY_SERVICE_CONFIGW)config_buffer; + DWORD size=sizeof(config_buffer); + DWORD needed; + if (!QueryServiceConfigW(service, config, size, &needed)) + die("QueryServiceConfig failed with %d\n", GetLastError()); + + int numargs; + LPWSTR *args= CommandLineToArgvW(config->lpBinaryPathName, &numargs); + + char commandline[3*FN_REFLEN +32]; + + /* Run some checks to ensure we're really upgrading mysql service */ + + if(numargs != 3) + { + die("Expected 3 parameters in service configuration binPath," + "got %d parameters instead\n. binPath: %S", numargs, + config->lpBinaryPathName); + } + if(wcsncmp(args[1], L"--defaults-file=", 16) != 0) + { + die("Unexpected service configuration, second parameter must start with " + "--defaults-file. binPath= %S", config->lpBinaryPathName); + } + GetFullPathNameW(args[0], MAX_PATH, old_mysqld_path, &file_part); + + if(wcsicmp(file_part, L"mysqld.exe") != 0 && + wcsicmp(file_part, L"mysqld") != 0) + { + die("The service executable is not mysqld. binPath: %S", + config->lpBinaryPathName); + } + + if(wcsicmp(file_part, L"mysqld") == 0) + wcscat_s(old_mysqld_path, L".exe"); + + int old_mysqld_major, old_mysqld_minor; + get_file_version(old_mysqld_path, &old_mysqld_major, &old_mysqld_minor); + int my_major= MYSQL_VERSION_ID/10000; + int my_minor= (MYSQL_VERSION_ID - 10000*my_major)/100; + + if(my_major < old_mysqld_major || + (my_major == old_mysqld_major && my_minor < old_mysqld_minor)) + { + die("Can not downgrade, the service is currently running as version %d.%d" + ", my version is %d.%d", old_mysqld_major, old_mysqld_minor, my_major, + my_minor); + } + + wcstombs(defaults_file, args[1] + 16, MAX_PATH); + /* + Remove basedir from defaults file, otherwise the service wont come up in + the new version, and will complain about mismatched message file. + */ + WritePrivateProfileString("mysqld", "basedir",NULL, defaults_file); + +#ifdef _WIN64 + /* Currently, pbxt is non-functional on x64 */ + WritePrivateProfileString("mysqld", "loose-skip-pbxt","1", defaults_file); +#endif + /* + Replace default-character-set with character-set-server, to avoid + "default-character-set is deprecated and will be replaced ..." + message. + */ + default_character_set[0]=0; + GetPrivateProfileStringA("mysqld", "default-character-set", NULL, + default_character_set, sizeof(default_character_set), defaults_file); + if(default_character_set[0]) + { + WritePrivateProfileString("mysqld", "default-character-set", NULL, + defaults_file); + WritePrivateProfileString("mysqld", "character-set-server", + default_character_set, defaults_file); + } + + sprintf_s(commandline, "\"%s\" \"%S\" \"%S\"", mysqld_path, args[1], args[2]); + if (!ChangeServiceConfig(service, SERVICE_NO_CHANGE, SERVICE_NO_CHANGE, + SERVICE_NO_CHANGE, commandline, NULL, NULL, NULL, NULL, NULL, NULL)) + { + die("ChangeServiceConfigW failed with %d", GetLastError()); + } + + sprintf_s(defaults_file_param, "%S", args[1]); + LocalFree(args); +} + + + +int main(int argc, char **argv) +{ + int error; + MY_INIT(argv[0]); + char bindir[FN_REFLEN]; + char *p; + + /* + Get full path to mysqld, we need it when changing service configuration. + Assume installation layout, i.e mysqld.exe, mysqladmin.exe, mysqlupgrade.exe + and mysql_upgrade_service.exe are in the same directory. + */ + GetModuleFileName(NULL, bindir, FN_REFLEN); + p = strrchr(bindir, FN_LIBCHAR); + if(p) + { + *p=0; + } + sprintf_s(mysqld_path, "%s\\mysqld.exe", bindir); + sprintf_s(mysqladmin_path, "%s\\mysqladmin.exe", bindir); + sprintf_s(mysqlupgrade_path, "%s\\mysql_upgrade.exe", bindir); + + char *paths[]= {mysqld_path, mysqladmin_path, mysqlupgrade_path}; + for(int i=0; i< 3;i++) + { + if(GetFileAttributes(paths[i]) == INVALID_FILE_ATTRIBUTES) + die("File %s does not exist", paths[i]); + } + + + /* Parse options */ + if ((error= handle_options(&argc, &argv, my_long_options, get_one_option))) + die(""); + if(!opt_service) + die("service parameter is mandatory"); + + /* + Messages written on stdout should not be buffered, GUI upgrade program + read them from pipe and uses as progress indicator. + */ + setvbuf(stdout, NULL, _IONBF, 0); + + log("Phase 1/8: Changing service configuration"); + change_service_config(); + + log("Phase 2/8: Stopping service"); + stop_mysqld_service(); + + /* + Start mysqld.exe as non-service skipping privileges (so we do not + care about the password). But disable networking and enable pipe + for communication, for security reasons. + */ + char socket_param[FN_REFLEN]; + sprintf_s(socket_param,"--shared_memory_base_name=mysql_upgrade_service_%d", + GetCurrentProcessId()); + + log("Phase 3/8: Starting mysqld for upgrade"); + mysqld_process= (HANDLE)run_tool(P_NOWAIT, mysqld_path, + defaults_file_param, "--skip-networking", "--skip-grant-tables", + "--enable-shared-memory", socket_param, NULL); + + if(mysqld_process == INVALID_HANDLE_VALUE) + { + die("Cannot start mysqld.exe process, errno=%d", errno); + } + + log("Phase 4/8: Waiting for startup to complete"); + DWORD start_duration_ms= 0; + for (int i=0; ; i++) + { + if (WaitForSingleObject(mysqld_process, 0) != WAIT_TIMEOUT) + die("mysqld.exe did not start"); + + if (run_tool(P_WAIT, mysqladmin_path, "--protocol=memory", + socket_param, "ping", NULL) == 0) + { + break; + } + if (start_duration_ms > startup_timeout*1000) + die("Server did not come up in %d seconds",startup_timeout); + Sleep(500); + start_duration_ms+= 500; + } + + log("Phase 5/8: Running mysql_upgrade"); + int upgrade_err = (int) run_tool(P_WAIT, mysqlupgrade_path, + "--protocol=memory", "--force", socket_param, + NULL); + + log("Phase 6/8: Initiating server shutdown"); + initiate_mysqld_shutdown(); + + log("Phase 7/8: Waiting for shutdown to complete"); + if (WaitForSingleObject(mysqld_process, shutdown_timeout*1000) + != WAIT_OBJECT_0) + { + /* Shutdown takes too long */ + die("mysqld does not shutdown."); + } + CloseHandle(mysqld_process); + mysqld_process= NULL; + + log("Phase 8/8: Starting service%s", + (initial_service_state == SERVICE_RUNNING)?"":" (skipped)"); + if (initial_service_state == SERVICE_RUNNING) + { + StartService(service, NULL, NULL); + } + + log("Service '%s' successfully upgraded.\nLog file is written to %s", + opt_service, logfile_path); + CloseServiceHandle(service); + CloseServiceHandle(scm); + if(logfile_handle) + CloseHandle(logfile_handle); + my_end(0); + exit(0); +}
\ No newline at end of file |