1 /* Copyright (C) 2010-2011 Monty Program Ab & Vladislav Vaintroub
2
3 This program is free software; you can redistribute it and/or modify
4 it under the terms of the GNU General Public License as published by
5 the Free Software Foundation; version 2 of the License.
6
7 This program is distributed in the hope that it will be useful,
8 but WITHOUT ANY WARRANTY; without even the implied warranty of
9 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 GNU General Public License for more details.
11
12 You should have received a copy of the GNU General Public License
13 along with this program; if not, write to the Free Software
14 Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335 USA */
15
16 /*
17 mysql_upgrade_service upgrades mysql service on Windows.
18 It changes service definition to point to the new mysqld.exe, restarts the
19 server and runs mysql_upgrade
20 */
21
22 #define DONT_DEFINE_VOID
23 #include "mariadb.h"
24 #include <process.h>
25 #include <my_getopt.h>
26 #include <my_sys.h>
27 #include <m_string.h>
28 #include <mysql_version.h>
29 #include <winservice.h>
30
31 #include <windows.h>
32 #include <string>
33
34 extern int upgrade_config_file(const char *myini_path);
35
36 /* We're using version APIs */
37 #pragma comment(lib, "version")
38
39 #define USAGETEXT \
40 "mysql_upgrade_service.exe Ver 1.00 for Windows\n" \
41 "Copyright (C) 2010-2011 Monty Program Ab & Vladislav Vaintroub" \
42 "This software comes with ABSOLUTELY NO WARRANTY. This is free software,\n" \
43 "and you are welcome to modify and redistribute it under the GPL v2 license\n" \
44 "Usage: mysql_upgrade_service.exe [OPTIONS]\n" \
45 "OPTIONS:"
46
47 static char mysqld_path[MAX_PATH];
48 static char mysqladmin_path[MAX_PATH];
49 static char mysqlupgrade_path[MAX_PATH];
50
51 static char defaults_file_param[MAX_PATH + 16]; /*--defaults-file=<path> */
52 static char logfile_path[MAX_PATH];
53 char my_ini_bck[MAX_PATH];
54 mysqld_service_properties service_properties;
55 static char *opt_service;
56 static SC_HANDLE service;
57 static SC_HANDLE scm;
58 HANDLE mysqld_process; // mysqld.exe started for upgrade
59 DWORD initial_service_state= UINT_MAX; // initial state of the service
60 HANDLE logfile_handle;
61
62 /*
63 Startup and shutdown timeouts, in seconds.
64 Maybe,they can be made parameters
65 */
66 static unsigned int startup_timeout= 60;
67 static unsigned int shutdown_timeout= 60*60;
68
69 static struct my_option my_long_options[]=
70 {
71 {"help", '?', "Display this help message and exit.", 0, 0, 0, GET_NO_ARG,
72 NO_ARG, 0, 0, 0, 0, 0, 0},
73 {"service", 'S', "Name of the existing Windows service",
74 &opt_service, &opt_service, 0, GET_STR, REQUIRED_ARG, 0, 0, 0, 0, 0, 0},
75 {0, 0, 0, 0, 0, 0, GET_NO_ARG, NO_ARG, 0, 0, 0, 0, 0, 0}
76 };
77
78
79
80 static my_bool
get_one_option(const struct my_option * opt,const char *,const char *)81 get_one_option(const struct my_option *opt, const char *, const char *)
82 {
83 DBUG_ENTER("get_one_option");
84 switch (opt->id) {
85 case '?':
86 printf("%s\n", USAGETEXT);
87 my_print_help(my_long_options);
88 exit(0);
89 break;
90 }
91 DBUG_RETURN(0);
92 }
93
94
95
log(const char * fmt,...)96 static void log(const char *fmt, ...)
97 {
98 va_list args;
99 /* Print the error message */
100 va_start(args, fmt);
101 vfprintf(stdout,fmt, args);
102 va_end(args);
103 fputc('\n', stdout);
104 fflush(stdout);
105 }
106
107
die(const char * fmt,...)108 static void die(const char *fmt, ...)
109 {
110 va_list args;
111 DBUG_ENTER("die");
112
113 /* Print the error message */
114 va_start(args, fmt);
115
116 fprintf(stderr, "FATAL ERROR: ");
117 vfprintf(stderr, fmt, args);
118 fputc('\n', stderr);
119 if (logfile_path[0])
120 {
121 fprintf(stderr, "Additional information can be found in the log file %s",
122 logfile_path);
123 }
124 va_end(args);
125 fputc('\n', stderr);
126 fflush(stdout);
127 /* Cleanup */
128
129 if (my_ini_bck[0])
130 {
131 MoveFileEx(my_ini_bck, service_properties.inifile,MOVEFILE_REPLACE_EXISTING);
132 }
133
134 /*
135 Stop service that we started, if it was not initially running at
136 program start.
137 */
138 if (initial_service_state != UINT_MAX && initial_service_state != SERVICE_RUNNING)
139 {
140 SERVICE_STATUS service_status;
141 ControlService(service, SERVICE_CONTROL_STOP, &service_status);
142 }
143
144 if (scm)
145 CloseServiceHandle(scm);
146 if (service)
147 CloseServiceHandle(service);
148 /* Stop mysqld.exe, if it was started for upgrade */
149 if (mysqld_process)
150 TerminateProcess(mysqld_process, 3);
151 if (logfile_handle)
152 CloseHandle(logfile_handle);
153 my_end(0);
154
155 exit(1);
156 }
157
158 #define WRITE_LOG(fmt,...) {\
159 char log_buf[1024]; \
160 DWORD nbytes; \
161 snprintf(log_buf,sizeof(log_buf), fmt, __VA_ARGS__);\
162 WriteFile(logfile_handle,log_buf, (DWORD)strlen(log_buf), &nbytes , 0);\
163 }
164
165 /*
166 spawn-like function to run subprocesses.
167 We also redirect the full output to the log file.
168
169 Typical usage could be something like
170 run_tool(P_NOWAIT, "cmd.exe", "/c" , "echo", "foo", NULL)
171
172 @param wait_flag (P_WAIT or P_NOWAIT)
173 @program program to run
174
175 Rest of the parameters is NULL terminated strings building command line.
176
177 @return intptr containing either process handle, if P_NOWAIT is used
178 or return code of the process (if P_WAIT is used)
179 */
180
run_tool(int wait_flag,const char * program,...)181 static intptr_t run_tool(int wait_flag, const char *program,...)
182 {
183 static char cmdline[32*1024];
184 char *end;
185 va_list args;
186 va_start(args, program);
187 if (!program)
188 die("Invalid call to run_tool");
189 end= strxmov(cmdline, "\"", program, "\"", NullS);
190
191 for(;;)
192 {
193 char *param= va_arg(args,char *);
194 if(!param)
195 break;
196 end= strxmov(end, " \"", param, "\"", NullS);
197 }
198 va_end(args);
199
200 /* Create output file if not alredy done */
201 if (!logfile_handle)
202 {
203 char tmpdir[FN_REFLEN];
204 GetTempPath(FN_REFLEN, tmpdir);
205 sprintf_s(logfile_path, "%smysql_upgrade_service.%s.log", tmpdir,
206 opt_service);
207 SECURITY_ATTRIBUTES attr= {0};
208 attr.nLength= sizeof(SECURITY_ATTRIBUTES);
209 attr.bInheritHandle= TRUE;
210 logfile_handle= CreateFile(logfile_path, FILE_APPEND_DATA,
211 FILE_SHARE_READ|FILE_SHARE_WRITE, &attr, CREATE_ALWAYS, 0, NULL);
212 if (logfile_handle == INVALID_HANDLE_VALUE)
213 {
214 die("Cannot open log file %s, windows error %u",
215 logfile_path, GetLastError());
216 }
217 }
218
219 WRITE_LOG("Executing %s\r\n", cmdline);
220
221 /* Start child process */
222 STARTUPINFO si= {0};
223 si.cb= sizeof(si);
224 si.hStdInput= GetStdHandle(STD_INPUT_HANDLE);
225 si.hStdError= logfile_handle;
226 si.hStdOutput= logfile_handle;
227 si.dwFlags= STARTF_USESTDHANDLES;
228 PROCESS_INFORMATION pi;
229 if (!CreateProcess(NULL, cmdline, NULL,
230 NULL, TRUE, NULL, NULL, NULL, &si, &pi))
231 {
232 die("CreateProcess failed (commandline %s)", cmdline);
233 }
234 CloseHandle(pi.hThread);
235
236 if (wait_flag == P_NOWAIT)
237 {
238 /* Do not wait for process to complete, return handle. */
239 return (intptr_t)pi.hProcess;
240 }
241
242 /* Wait for process to complete. */
243 if (WaitForSingleObject(pi.hProcess, INFINITE) != WAIT_OBJECT_0)
244 {
245 die("WaitForSingleObject() failed");
246 }
247 DWORD exit_code;
248 if (!GetExitCodeProcess(pi.hProcess, &exit_code))
249 {
250 die("GetExitCodeProcess() failed");
251 }
252 return (intptr_t)exit_code;
253 }
254
255
stop_mysqld_service()256 void stop_mysqld_service()
257 {
258 DWORD needed;
259 SERVICE_STATUS_PROCESS ssp;
260 int timeout= shutdown_timeout*1000;
261 for(;;)
262 {
263 if (!QueryServiceStatusEx(service, SC_STATUS_PROCESS_INFO,
264 (LPBYTE)&ssp,
265 sizeof(SERVICE_STATUS_PROCESS),
266 &needed))
267 {
268 die("QueryServiceStatusEx failed (%u)\n", GetLastError());
269 }
270
271 /*
272 Remember initial state of the service, we will restore it on
273 exit.
274 */
275 if(initial_service_state == UINT_MAX)
276 initial_service_state= ssp.dwCurrentState;
277
278 switch(ssp.dwCurrentState)
279 {
280 case SERVICE_STOPPED:
281 return;
282 case SERVICE_RUNNING:
283 if(!ControlService(service, SERVICE_CONTROL_STOP,
284 (SERVICE_STATUS *)&ssp))
285 die("ControlService failed, error %u\n", GetLastError());
286 case SERVICE_START_PENDING:
287 case SERVICE_STOP_PENDING:
288 if(timeout < 0)
289 die("Service does not stop after %d seconds timeout",shutdown_timeout);
290 Sleep(100);
291 timeout -= 100;
292 break;
293 default:
294 die("Unexpected service state %d",ssp.dwCurrentState);
295 }
296 }
297 }
298
299
300 /*
301 Shutdown mysql server. Not using mysqladmin, since
302 our --skip-grant-tables do not work anymore after mysql_upgrade
303 that does "flush privileges". Instead, the shutdown event is set.
304 */
initiate_mysqld_shutdown()305 void initiate_mysqld_shutdown()
306 {
307 char event_name[32];
308 DWORD pid= GetProcessId(mysqld_process);
309 sprintf_s(event_name, "MySQLShutdown%d", pid);
310 HANDLE shutdown_handle= OpenEvent(EVENT_MODIFY_STATE, FALSE, event_name);
311 if(!shutdown_handle)
312 {
313 die("OpenEvent() failed for shutdown event");
314 }
315
316 if(!SetEvent(shutdown_handle))
317 {
318 die("SetEvent() failed");
319 }
320 }
321
get_service_config()322 static void get_service_config()
323 {
324 scm = OpenSCManager(NULL, NULL, SC_MANAGER_ALL_ACCESS);
325 if (!scm)
326 die("OpenSCManager failed with %u", GetLastError());
327 service = OpenService(scm, opt_service, SERVICE_ALL_ACCESS);
328 if (!service)
329 die("OpenService failed with %u", GetLastError());
330
331 BYTE config_buffer[8 * 1024];
332 LPQUERY_SERVICE_CONFIGW config = (LPQUERY_SERVICE_CONFIGW)config_buffer;
333 DWORD size = sizeof(config_buffer);
334 DWORD needed;
335 if (!QueryServiceConfigW(service, config, size, &needed))
336 die("QueryServiceConfig failed with %u", GetLastError());
337
338 if (get_mysql_service_properties(config->lpBinaryPathName, &service_properties))
339 {
340 die("Not a valid MySQL service");
341 }
342
343 int my_major = MYSQL_VERSION_ID / 10000;
344 int my_minor = (MYSQL_VERSION_ID % 10000) / 100;
345 int my_patch = MYSQL_VERSION_ID % 100;
346
347 if (my_major < service_properties.version_major ||
348 (my_major == service_properties.version_major && my_minor < service_properties.version_minor))
349 {
350 die("Can not downgrade, the service is currently running as version %d.%d.%d"
351 ", my version is %d.%d.%d", service_properties.version_major, service_properties.version_minor,
352 service_properties.version_patch, my_major, my_minor, my_patch);
353 }
354 if (service_properties.inifile[0] == 0)
355 {
356 /*
357 Weird case, no --defaults-file in service definition, need to create one.
358 */
359 sprintf_s(service_properties.inifile, MAX_PATH, "%s\\my.ini", service_properties.datadir);
360 }
361 sprintf(defaults_file_param, "--defaults-file=%s", service_properties.inifile);
362 }
363 /*
364 Change service configuration (binPath) to point to mysqld from
365 this installation.
366 */
change_service_config()367 static void change_service_config()
368 {
369 char buf[MAX_PATH];
370 char commandline[3 * MAX_PATH + 19];
371 int i;
372
373 /*
374 Write datadir to my.ini, after converting backslashes to
375 unix style slashes.
376 */
377 strcpy_s(buf, MAX_PATH, service_properties.datadir);
378 for(i= 0; buf[i]; i++)
379 {
380 if (buf[i] == '\\')
381 buf[i]= '/';
382 }
383 WritePrivateProfileString("mysqld", "datadir",buf, service_properties.inifile);
384
385 /*
386 Remove basedir from defaults file, otherwise the service wont come up in
387 the new version, and will complain about mismatched message file.
388 */
389 WritePrivateProfileString("mysqld", "basedir",NULL, service_properties.inifile);
390
391 sprintf(defaults_file_param,"--defaults-file=%s", service_properties.inifile);
392 sprintf_s(commandline, "\"%s\" \"%s\" \"%s\"", mysqld_path,
393 defaults_file_param, opt_service);
394 if (!ChangeServiceConfig(service, SERVICE_NO_CHANGE, SERVICE_NO_CHANGE,
395 SERVICE_NO_CHANGE, commandline, NULL, NULL, NULL, NULL, NULL, NULL))
396 {
397 die("ChangeServiceConfig failed with %u", GetLastError());
398 }
399
400 }
401
402
main(int argc,char ** argv)403 int main(int argc, char **argv)
404 {
405 int error;
406 MY_INIT(argv[0]);
407 char bindir[FN_REFLEN];
408 char *p;
409
410 /* Parse options */
411 if ((error= handle_options(&argc, &argv, my_long_options, get_one_option)))
412 die("");
413 if (!opt_service)
414 die("--service=# parameter is mandatory");
415
416 /*
417 Get full path to mysqld, we need it when changing service configuration.
418 Assume installation layout, i.e mysqld.exe, mysqladmin.exe, mysqlupgrade.exe
419 and mysql_upgrade_service.exe are in the same directory.
420 */
421 GetModuleFileName(NULL, bindir, FN_REFLEN);
422 p= strrchr(bindir, FN_LIBCHAR);
423 if(p)
424 {
425 *p= 0;
426 }
427 sprintf_s(mysqld_path, "%s\\mysqld.exe", bindir);
428 sprintf_s(mysqladmin_path, "%s\\mysqladmin.exe", bindir);
429 sprintf_s(mysqlupgrade_path, "%s\\mysql_upgrade.exe", bindir);
430
431 char *paths[]= {mysqld_path, mysqladmin_path, mysqlupgrade_path};
432 for(int i= 0; i< 3;i++)
433 {
434 if(GetFileAttributes(paths[i]) == INVALID_FILE_ATTRIBUTES)
435 die("File %s does not exist", paths[i]);
436 }
437
438 /*
439 Messages written on stdout should not be buffered, GUI upgrade program
440 reads them from pipe and uses as progress indicator.
441 */
442 setvbuf(stdout, NULL, _IONBF, 0);
443 int phase = 0;
444 int max_phases=10;
445 get_service_config();
446
447 bool my_ini_exists;
448 bool old_mysqld_exe_exists;
449
450 log("Phase %d/%d: Stopping service", ++phase,max_phases);
451 stop_mysqld_service();
452
453 my_ini_exists = (GetFileAttributes(service_properties.inifile) != INVALID_FILE_ATTRIBUTES);
454 if (!my_ini_exists)
455 {
456 HANDLE h = CreateFile(service_properties.inifile, GENERIC_WRITE, FILE_SHARE_READ|FILE_SHARE_WRITE,
457 0, CREATE_NEW, 0 ,0);
458 if (h != INVALID_HANDLE_VALUE)
459 {
460 CloseHandle(h);
461 }
462 else if (GetLastError() != ERROR_FILE_EXISTS)
463 {
464 die("Can't create ini file %s, last error %u", service_properties.inifile, GetLastError());
465 }
466 }
467
468 old_mysqld_exe_exists = (GetFileAttributes(service_properties.mysqld_exe) != INVALID_FILE_ATTRIBUTES);
469 log("Phase %d/%d: Fixing server config file%s", ++phase, max_phases, my_ini_exists ? "" : "(skipped)");
470
471 snprintf(my_ini_bck, sizeof(my_ini_bck), "%s.BCK", service_properties.inifile);
472 CopyFile(service_properties.inifile, my_ini_bck, FALSE);
473 upgrade_config_file(service_properties.inifile);
474
475 bool do_start_stop_server = old_mysqld_exe_exists && initial_service_state != SERVICE_RUNNING;
476
477 log("Phase %d/%d: Start and stop server in the old version, to avoid crash recovery %s", ++phase, max_phases,
478 do_start_stop_server?",this can take some time":"(skipped)");
479
480 char socket_param[FN_REFLEN];
481 sprintf_s(socket_param, "--socket=mysql_upgrade_service_%u",
482 GetCurrentProcessId());
483
484 DWORD start_duration_ms = 0;
485
486 if (do_start_stop_server)
487 {
488 /* Start/stop server with --loose-innodb-fast-shutdown=1 */
489 mysqld_process = (HANDLE)run_tool(P_NOWAIT, service_properties.mysqld_exe,
490 defaults_file_param, "--loose-innodb-fast-shutdown=1", "--skip-networking",
491 "--enable-named-pipe", socket_param, "--skip-slave-start", NULL);
492
493 if (mysqld_process == INVALID_HANDLE_VALUE)
494 {
495 die("Cannot start mysqld.exe process, last error =%u", GetLastError());
496 }
497 char pipe_name[64];
498 snprintf(pipe_name, sizeof(pipe_name), "\\\\.\\pipe\\mysql_upgrade_service_%lu",
499 GetCurrentProcessId());
500 for (;;)
501 {
502 if (WaitForSingleObject(mysqld_process, 0) != WAIT_TIMEOUT)
503 die("mysqld.exe did not start");
504
505 if (WaitNamedPipe(pipe_name, 0))
506 {
507 // Server started, shut it down.
508 initiate_mysqld_shutdown();
509 if (WaitForSingleObject((HANDLE)mysqld_process, shutdown_timeout * 1000) != WAIT_OBJECT_0)
510 {
511 die("Could not shutdown server started with '--innodb-fast-shutdown=0'");
512 }
513 DWORD exit_code;
514 if (!GetExitCodeProcess((HANDLE)mysqld_process, &exit_code))
515 {
516 die("Could not get mysqld's exit code");
517 }
518 if (exit_code)
519 {
520 die("Could not get successfully shutdown mysqld");
521 }
522 CloseHandle(mysqld_process);
523 break;
524 }
525 Sleep(500);
526 start_duration_ms += 500;
527 }
528 }
529 /*
530 Start mysqld.exe as non-service skipping privileges (so we do not
531 care about the password). But disable networking and enable pipe
532 for communication, for security reasons.
533 */
534
535 log("Phase %d/%d: Starting mysqld for upgrade",++phase,max_phases);
536 mysqld_process= (HANDLE)run_tool(P_NOWAIT, mysqld_path,
537 defaults_file_param, "--skip-networking", "--skip-grant-tables",
538 "--enable-named-pipe", socket_param,"--skip-slave-start", NULL);
539
540 if (mysqld_process == INVALID_HANDLE_VALUE)
541 {
542 die("Cannot start mysqld.exe process, errno=%d", errno);
543 }
544
545 log("Phase %d/%d: Waiting for startup to complete",++phase,max_phases);
546 start_duration_ms= 0;
547 for(;;)
548 {
549 if (WaitForSingleObject(mysqld_process, 0) != WAIT_TIMEOUT)
550 die("mysqld.exe did not start");
551
552 if (run_tool(P_WAIT, mysqladmin_path, "--protocol=pipe", socket_param,
553 "ping", "--no-beep", NULL) == 0)
554 {
555 break;
556 }
557 if (start_duration_ms > startup_timeout*1000)
558 die("Server did not come up in %d seconds",startup_timeout);
559 Sleep(500);
560 start_duration_ms+= 500;
561 }
562
563 log("Phase %d/%d: Running mysql_upgrade",++phase,max_phases);
564 int upgrade_err= (int) run_tool(P_WAIT, mysqlupgrade_path,
565 "--protocol=pipe", "--force", socket_param,
566 NULL);
567
568 if (upgrade_err)
569 die("mysql_upgrade failed with error code %d\n", upgrade_err);
570
571 log("Phase %d/%d: Changing service configuration", ++phase, max_phases);
572 change_service_config();
573
574 log("Phase %d/%d: Initiating server shutdown",++phase, max_phases);
575 initiate_mysqld_shutdown();
576
577 log("Phase %d/%d: Waiting for shutdown to complete",++phase, max_phases);
578 if (WaitForSingleObject(mysqld_process, shutdown_timeout*1000)
579 != WAIT_OBJECT_0)
580 {
581 /* Shutdown takes too long */
582 die("mysqld does not shutdown.");
583 }
584 CloseHandle(mysqld_process);
585 mysqld_process= NULL;
586
587 log("Phase %d/%d: Starting service%s",++phase,max_phases,
588 (initial_service_state == SERVICE_RUNNING)?"":" (skipped)");
589 if (initial_service_state == SERVICE_RUNNING)
590 {
591 StartService(service, NULL, NULL);
592 }
593
594 log("Service '%s' successfully upgraded.\nLog file is written to %s",
595 opt_service, logfile_path);
596 CloseServiceHandle(service);
597 CloseServiceHandle(scm);
598 if (logfile_handle)
599 CloseHandle(logfile_handle);
600 if(my_ini_bck[0])
601 {
602 DeleteFile(my_ini_bck);
603 }
604 my_end(0);
605 exit(0);
606 }
607