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