You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

521 lines
14 KiB

  1. /* Copyright (C) 2010-2011 Monty Program Ab & Vladislav Vaintroub
  2. This program is free software; you can redistribute it and/or modify
  3. it under the terms of the GNU General Public License as published by
  4. the Free Software Foundation; version 2 of the License.
  5. This program is distributed in the hope that it will be useful,
  6. but WITHOUT ANY WARRANTY; without even the implied warranty of
  7. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  8. GNU General Public License for more details.
  9. You should have received a copy of the GNU General Public License
  10. along with this program; if not, write to the Free Software
  11. Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */
  12. /*
  13. mysql_upgrade_service upgrades mysql service on Windows.
  14. It changes service definition to point to the new mysqld.exe, restarts the
  15. server and runs mysql_upgrade
  16. */
  17. #define DONT_DEFINE_VOID
  18. #include <process.h>
  19. #include <my_global.h>
  20. #include <my_getopt.h>
  21. #include <my_sys.h>
  22. #include <m_string.h>
  23. #include <mysql_version.h>
  24. #include <winservice.h>
  25. #include <windows.h>
  26. /* We're using version APIs */
  27. #pragma comment(lib, "version")
  28. #define USAGETEXT \
  29. "mysql_upgrade_service.exe Ver 1.00 for Windows\n" \
  30. "Copyright (C) 2010-2011 Monty Program Ab & Vladislav Vaintroub" \
  31. "This software comes with ABSOLUTELY NO WARRANTY. This is free software,\n" \
  32. "and you are welcome to modify and redistribute it under the GPL v2 license\n" \
  33. "Usage: mysql_upgrade_service.exe [OPTIONS]\n" \
  34. "OPTIONS:"
  35. static char mysqld_path[MAX_PATH];
  36. static char mysqladmin_path[MAX_PATH];
  37. static char mysqlupgrade_path[MAX_PATH];
  38. static char defaults_file_param[MAX_PATH + 16]; /*--defaults-file=<path> */
  39. static char logfile_path[MAX_PATH];
  40. static char *opt_service;
  41. static SC_HANDLE service;
  42. static SC_HANDLE scm;
  43. HANDLE mysqld_process; // mysqld.exe started for upgrade
  44. DWORD initial_service_state= -1; // initial state of the service
  45. HANDLE logfile_handle;
  46. /*
  47. Startup and shutdown timeouts, in seconds.
  48. Maybe,they can be made parameters
  49. */
  50. static unsigned int startup_timeout= 60;
  51. static unsigned int shutdown_timeout= 60;
  52. static struct my_option my_long_options[]=
  53. {
  54. {"help", '?', "Display this help message and exit.", 0, 0, 0, GET_NO_ARG,
  55. NO_ARG, 0, 0, 0, 0, 0, 0},
  56. {"service", 'S', "Name of the existing Windows service",
  57. &opt_service, &opt_service, 0, GET_STR, REQUIRED_ARG, 0, 0, 0, 0, 0, 0},
  58. {0, 0, 0, 0, 0, 0, GET_NO_ARG, NO_ARG, 0, 0, 0, 0, 0, 0}
  59. };
  60. static my_bool
  61. get_one_option(int optid,
  62. const struct my_option *opt __attribute__ ((unused)),
  63. char *argument __attribute__ ((unused)))
  64. {
  65. DBUG_ENTER("get_one_option");
  66. switch (optid) {
  67. case '?':
  68. printf("%s\n", USAGETEXT);
  69. my_print_help(my_long_options);
  70. exit(0);
  71. break;
  72. }
  73. DBUG_RETURN(0);
  74. }
  75. static void log(const char *fmt, ...)
  76. {
  77. va_list args;
  78. /* Print the error message */
  79. va_start(args, fmt);
  80. vfprintf(stdout,fmt, args);
  81. va_end(args);
  82. fputc('\n', stdout);
  83. fflush(stdout);
  84. }
  85. static void die(const char *fmt, ...)
  86. {
  87. va_list args;
  88. DBUG_ENTER("die");
  89. /* Print the error message */
  90. va_start(args, fmt);
  91. fprintf(stderr, "FATAL ERROR: ");
  92. vfprintf(stderr, fmt, args);
  93. if (logfile_path[0])
  94. {
  95. fprintf(stderr, "Additional information can be found in the log file %s",
  96. logfile_path);
  97. }
  98. va_end(args);
  99. fputc('\n', stderr);
  100. fflush(stdout);
  101. /* Cleanup */
  102. /*
  103. Stop service that we started, if it was not initally running at
  104. program start.
  105. */
  106. if (initial_service_state != -1 && initial_service_state != SERVICE_RUNNING)
  107. {
  108. SERVICE_STATUS service_status;
  109. ControlService(service, SERVICE_CONTROL_STOP, &service_status);
  110. }
  111. if (scm)
  112. CloseServiceHandle(scm);
  113. if (service)
  114. CloseServiceHandle(service);
  115. /* Stop mysqld.exe, if it was started for upgrade */
  116. if (mysqld_process)
  117. TerminateProcess(mysqld_process, 3);
  118. if (logfile_handle)
  119. CloseHandle(logfile_handle);
  120. my_end(0);
  121. exit(1);
  122. }
  123. /*
  124. spawn-like function to run subprocesses.
  125. We also redirect the full output to the log file.
  126. Typical usage could be something like
  127. run_tool(P_NOWAIT, "cmd.exe", "/c" , "echo", "foo", NULL)
  128. @param wait_flag (P_WAIT or P_NOWAIT)
  129. @program program to run
  130. Rest of the parameters is NULL terminated strings building command line.
  131. @return intptr containing either process handle, if P_NOWAIT is used
  132. or return code of the process (if P_WAIT is used)
  133. */
  134. static intptr_t run_tool(int wait_flag, const char *program,...)
  135. {
  136. static char cmdline[32*1024];
  137. char *end;
  138. va_list args;
  139. va_start(args, program);
  140. if (!program)
  141. die("Invalid call to run_tool");
  142. end= strxmov(cmdline, "\"", program, "\"", NullS);
  143. for(;;)
  144. {
  145. char *param= va_arg(args,char *);
  146. if(!param)
  147. break;
  148. end= strxmov(end, " \"", param, "\"", NullS);
  149. }
  150. va_end(args);
  151. /* Create output file if not alredy done */
  152. if (!logfile_handle)
  153. {
  154. char tmpdir[FN_REFLEN];
  155. GetTempPath(FN_REFLEN, tmpdir);
  156. sprintf_s(logfile_path, "%s\\mysql_upgrade_service.%s.log", tmpdir,
  157. opt_service);
  158. logfile_handle= CreateFile(logfile_path, GENERIC_WRITE, FILE_SHARE_READ,
  159. NULL, TRUNCATE_EXISTING, 0, NULL);
  160. if (!logfile_handle)
  161. {
  162. die("Cannot open log file %s, windows error %u",
  163. logfile_path, GetLastError());
  164. }
  165. }
  166. /* Start child process */
  167. STARTUPINFO si= {0};
  168. si.cb= sizeof(si);
  169. si.hStdInput= GetStdHandle(STD_INPUT_HANDLE);
  170. si.hStdError= logfile_handle;
  171. si.hStdOutput= logfile_handle;
  172. si.dwFlags= STARTF_USESTDHANDLES;
  173. PROCESS_INFORMATION pi;
  174. if (!CreateProcess(NULL, cmdline, NULL,
  175. NULL, TRUE, NULL, NULL, NULL, &si, &pi))
  176. {
  177. die("CreateProcess failed (commandline %s)", cmdline);
  178. }
  179. CloseHandle(pi.hThread);
  180. if (wait_flag == P_NOWAIT)
  181. {
  182. /* Do not wait for process to complete, return handle. */
  183. return (intptr_t)pi.hProcess;
  184. }
  185. /* Wait for process to complete. */
  186. if (WaitForSingleObject(pi.hProcess, INFINITE) != WAIT_OBJECT_0)
  187. {
  188. die("WaitForSingleObject() failed");
  189. }
  190. DWORD exit_code;
  191. if (!GetExitCodeProcess(pi.hProcess, &exit_code))
  192. {
  193. die("GetExitCodeProcess() failed");
  194. }
  195. return (intptr_t)exit_code;
  196. }
  197. void stop_mysqld_service()
  198. {
  199. DWORD needed;
  200. SERVICE_STATUS_PROCESS ssp;
  201. int timeout= shutdown_timeout*1000;
  202. for(;;)
  203. {
  204. if (!QueryServiceStatusEx(service, SC_STATUS_PROCESS_INFO,
  205. (LPBYTE)&ssp,
  206. sizeof(SERVICE_STATUS_PROCESS),
  207. &needed))
  208. {
  209. die("QueryServiceStatusEx failed (%u)\n", GetLastError());
  210. }
  211. /*
  212. Remeber initial state of the service, we will restore it on
  213. exit.
  214. */
  215. if(initial_service_state == -1)
  216. initial_service_state= ssp.dwCurrentState;
  217. switch(ssp.dwCurrentState)
  218. {
  219. case SERVICE_STOPPED:
  220. return;
  221. case SERVICE_RUNNING:
  222. if(!ControlService(service, SERVICE_CONTROL_STOP,
  223. (SERVICE_STATUS *)&ssp))
  224. die("ControlService failed, error %u\n", GetLastError());
  225. case SERVICE_START_PENDING:
  226. case SERVICE_STOP_PENDING:
  227. if(timeout < 0)
  228. die("Service does not stop after %d seconds timeout",shutdown_timeout);
  229. Sleep(100);
  230. timeout -= 100;
  231. break;
  232. default:
  233. die("Unexpected service state %d",ssp.dwCurrentState);
  234. }
  235. }
  236. }
  237. /*
  238. Shutdown mysql server. Not using mysqladmin, since
  239. our --skip-grant-tables do not work anymore after mysql_upgrade
  240. that does "flush privileges". Instead, the shutdown event is set.
  241. */
  242. void initiate_mysqld_shutdown()
  243. {
  244. char event_name[32];
  245. DWORD pid= GetProcessId(mysqld_process);
  246. sprintf_s(event_name, "MySQLShutdown%d", pid);
  247. HANDLE shutdown_handle= OpenEvent(EVENT_MODIFY_STATE, FALSE, event_name);
  248. if(!shutdown_handle)
  249. {
  250. die("OpenEvent() failed for shutdown event");
  251. }
  252. if(!SetEvent(shutdown_handle))
  253. {
  254. die("SetEvent() failed");
  255. }
  256. }
  257. /*
  258. Change service configuration (binPath) to point to mysqld from
  259. this installation.
  260. */
  261. static void change_service_config()
  262. {
  263. char defaults_file[MAX_PATH];
  264. char default_character_set[64];
  265. char buf[MAX_PATH];
  266. char commandline[3*MAX_PATH + 19];
  267. int i;
  268. scm= OpenSCManager(NULL, NULL, SC_MANAGER_ALL_ACCESS);
  269. if(!scm)
  270. die("OpenSCManager failed with %u", GetLastError());
  271. service= OpenService(scm, opt_service, SERVICE_ALL_ACCESS);
  272. if (!service)
  273. die("OpenService failed with %u", GetLastError());
  274. BYTE config_buffer[8*1024];
  275. LPQUERY_SERVICE_CONFIGW config= (LPQUERY_SERVICE_CONFIGW)config_buffer;
  276. DWORD size= sizeof(config_buffer);
  277. DWORD needed;
  278. if (!QueryServiceConfigW(service, config, size, &needed))
  279. die("QueryServiceConfig failed with %u", GetLastError());
  280. mysqld_service_properties props;
  281. if (get_mysql_service_properties(config->lpBinaryPathName, &props))
  282. {
  283. die("Not a valid MySQL service");
  284. }
  285. int my_major= MYSQL_VERSION_ID/10000;
  286. int my_minor= (MYSQL_VERSION_ID %10000)/100;
  287. int my_patch= MYSQL_VERSION_ID%100;
  288. if(my_major < props.version_major ||
  289. (my_major == props.version_major && my_minor < props.version_minor))
  290. {
  291. die("Can not downgrade, the service is currently running as version %d.%d.%d"
  292. ", my version is %d.%d.%d", props.version_major, props.version_minor,
  293. props.version_patch, my_major, my_minor, my_patch);
  294. }
  295. if(props.inifile[0] == 0)
  296. {
  297. /*
  298. Weird case, no --defaults-file in service definition, need to create one.
  299. */
  300. sprintf_s(props.inifile, MAX_PATH, "%s\\my.ini", props.datadir);
  301. }
  302. /*
  303. Write datadir to my.ini, after converting backslashes to
  304. unix style slashes.
  305. */
  306. strcpy_s(buf, MAX_PATH, props.datadir);
  307. for(i= 0; buf[i]; i++)
  308. {
  309. if (buf[i] == '\\')
  310. buf[i]= '/';
  311. }
  312. WritePrivateProfileString("mysqld", "datadir",buf, props.inifile);
  313. /*
  314. Remove basedir from defaults file, otherwise the service wont come up in
  315. the new version, and will complain about mismatched message file.
  316. */
  317. WritePrivateProfileString("mysqld", "basedir",NULL, props.inifile);
  318. /*
  319. Replace default-character-set with character-set-server, to avoid
  320. "default-character-set is deprecated and will be replaced ..."
  321. message.
  322. */
  323. default_character_set[0]= 0;
  324. GetPrivateProfileString("mysqld", "default-character-set", NULL,
  325. default_character_set, sizeof(default_character_set), defaults_file);
  326. if (default_character_set[0])
  327. {
  328. WritePrivateProfileString("mysqld", "default-character-set", NULL,
  329. defaults_file);
  330. WritePrivateProfileString("mysqld", "character-set-server",
  331. default_character_set, defaults_file);
  332. }
  333. sprintf(defaults_file_param,"--defaults-file=%s", props.inifile);
  334. sprintf_s(commandline, "\"%s\" \"%s\" \"%s\"", mysqld_path,
  335. defaults_file_param, opt_service);
  336. if (!ChangeServiceConfig(service, SERVICE_NO_CHANGE, SERVICE_NO_CHANGE,
  337. SERVICE_NO_CHANGE, commandline, NULL, NULL, NULL, NULL, NULL, NULL))
  338. {
  339. die("ChangeServiceConfig failed with %u", GetLastError());
  340. }
  341. }
  342. int main(int argc, char **argv)
  343. {
  344. int error;
  345. MY_INIT(argv[0]);
  346. char bindir[FN_REFLEN];
  347. char *p;
  348. /* Parse options */
  349. if ((error= handle_options(&argc, &argv, my_long_options, get_one_option)))
  350. die("");
  351. if (!opt_service)
  352. die("--service=# parameter is mandatory");
  353. /*
  354. Get full path to mysqld, we need it when changing service configuration.
  355. Assume installation layout, i.e mysqld.exe, mysqladmin.exe, mysqlupgrade.exe
  356. and mysql_upgrade_service.exe are in the same directory.
  357. */
  358. GetModuleFileName(NULL, bindir, FN_REFLEN);
  359. p= strrchr(bindir, FN_LIBCHAR);
  360. if(p)
  361. {
  362. *p= 0;
  363. }
  364. sprintf_s(mysqld_path, "%s\\mysqld.exe", bindir);
  365. sprintf_s(mysqladmin_path, "%s\\mysqladmin.exe", bindir);
  366. sprintf_s(mysqlupgrade_path, "%s\\mysql_upgrade.exe", bindir);
  367. char *paths[]= {mysqld_path, mysqladmin_path, mysqlupgrade_path};
  368. for(int i= 0; i< 3;i++)
  369. {
  370. if(GetFileAttributes(paths[i]) == INVALID_FILE_ATTRIBUTES)
  371. die("File %s does not exist", paths[i]);
  372. }
  373. /*
  374. Messages written on stdout should not be buffered, GUI upgrade program
  375. reads them from pipe and uses as progress indicator.
  376. */
  377. setvbuf(stdout, NULL, _IONBF, 0);
  378. log("Phase 1/8: Changing service configuration");
  379. change_service_config();
  380. log("Phase 2/8: Stopping service");
  381. stop_mysqld_service();
  382. /*
  383. Start mysqld.exe as non-service skipping privileges (so we do not
  384. care about the password). But disable networking and enable pipe
  385. for communication, for security reasons.
  386. */
  387. char socket_param[FN_REFLEN];
  388. sprintf_s(socket_param,"--socket=mysql_upgrade_service_%d",
  389. GetCurrentProcessId());
  390. log("Phase 3/8: Starting mysqld for upgrade");
  391. mysqld_process= (HANDLE)run_tool(P_NOWAIT, mysqld_path,
  392. defaults_file_param, "--skip-networking", "--skip-grant-tables",
  393. "--enable-named-pipe", socket_param, NULL);
  394. if (mysqld_process == INVALID_HANDLE_VALUE)
  395. {
  396. die("Cannot start mysqld.exe process, errno=%d", errno);
  397. }
  398. log("Phase 4/8: Waiting for startup to complete");
  399. DWORD start_duration_ms= 0;
  400. for(;;)
  401. {
  402. if (WaitForSingleObject(mysqld_process, 0) != WAIT_TIMEOUT)
  403. die("mysqld.exe did not start");
  404. if (run_tool(P_WAIT, mysqladmin_path, "--protocol=pipe",
  405. socket_param, "ping", NULL) == 0)
  406. {
  407. break;
  408. }
  409. if (start_duration_ms > startup_timeout*1000)
  410. die("Server did not come up in %d seconds",startup_timeout);
  411. Sleep(500);
  412. start_duration_ms+= 500;
  413. }
  414. log("Phase 5/8: Running mysql_upgrade");
  415. int upgrade_err= (int) run_tool(P_WAIT, mysqlupgrade_path,
  416. "--protocol=pipe", "--force", socket_param,
  417. NULL);
  418. if (upgrade_err)
  419. die("mysql_upgrade failed with error code %d\n", upgrade_err);
  420. log("Phase 6/8: Initiating server shutdown");
  421. initiate_mysqld_shutdown();
  422. log("Phase 7/8: Waiting for shutdown to complete");
  423. if (WaitForSingleObject(mysqld_process, shutdown_timeout*1000)
  424. != WAIT_OBJECT_0)
  425. {
  426. /* Shutdown takes too long */
  427. die("mysqld does not shutdown.");
  428. }
  429. CloseHandle(mysqld_process);
  430. mysqld_process= NULL;
  431. log("Phase 8/8: Starting service%s",
  432. (initial_service_state == SERVICE_RUNNING)?"":" (skipped)");
  433. if (initial_service_state == SERVICE_RUNNING)
  434. {
  435. StartService(service, NULL, NULL);
  436. }
  437. log("Service '%s' successfully upgraded.\nLog file is written to %s",
  438. opt_service, logfile_path);
  439. CloseServiceHandle(service);
  440. CloseServiceHandle(scm);
  441. if (logfile_handle)
  442. CloseHandle(logfile_handle);
  443. my_end(0);
  444. exit(0);
  445. }