1 | /* |
2 | Copyright (C) 2004 MySQL AB |
3 | |
4 | This program is free software; you can redistribute it and/or modify |
5 | it under the terms of the GNU General Public License version 2 as |
6 | published by the Free Software Foundation. |
7 | |
8 | This program is distributed in the hope that it will be useful, |
9 | but WITHOUT ANY WARRANTY; without even the implied warranty of |
10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
11 | GNU General Public License for more details. |
12 | |
13 | You should have received a copy of the GNU General Public License |
14 | along with this program; if not, write to the Free Software |
15 | Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA |
16 | |
17 | */ |
18 | package com.mysql.management; |
19 | |
20 | import java.io.ByteArrayOutputStream; |
21 | import java.io.File; |
22 | import java.io.PrintStream; |
23 | import java.sql.Connection; |
24 | import java.sql.DriverManager; |
25 | import java.sql.SQLException; |
26 | import java.util.ArrayList; |
27 | import java.util.HashMap; |
28 | import java.util.Iterator; |
29 | import java.util.List; |
30 | import java.util.Map; |
31 | |
32 | import com.mysql.jdbc.Driver; |
33 | import com.mysql.jdbc.MysqlErrorNumbers; |
34 | import com.mysql.management.util.CommandLineOptionsParser; |
35 | import com.mysql.management.util.Files; |
36 | import com.mysql.management.util.ListToString; |
37 | import com.mysql.management.util.NullPrintStream; |
38 | import com.mysql.management.util.Platform; |
39 | import com.mysql.management.util.ProcessUtil; |
40 | import com.mysql.management.util.Shell; |
41 | import com.mysql.management.util.Streams; |
42 | import com.mysql.management.util.TeeOutputStream; |
43 | import com.mysql.management.util.Threads; |
44 | import com.mysql.management.util.Utils; |
45 | |
46 | /** |
47 | * This class is final simply as a hint to the compiler, it may be un-finalized |
48 | * safely. |
49 | * |
50 | * @author Eric Herman <eric@mysql.com> |
51 | * @version $Id: MysqldResource.java,v 1.66 2005/12/05 16:39:54 eherman Exp $ |
52 | */ |
53 | public final class MysqldResource implements MysqldResourceI { |
54 | public static final String MYSQL_C_MXJ = "mysql-c.mxj"; |
55 | |
56 | public static final String DATA = "data"; |
57 | |
58 | private String versionString; |
59 | |
60 | private Map options; |
61 | |
62 | private Shell shell; |
63 | |
64 | private File baseDir; |
65 | |
66 | private File dataDir; |
67 | |
68 | private File pidFile; |
69 | |
70 | private String msgPrefix; |
71 | |
72 | private String pid; |
73 | |
74 | private String osName; |
75 | |
76 | private String osArch; |
77 | |
78 | private PrintStream out; |
79 | |
80 | private PrintStream err; |
81 | |
82 | private Exception trace; |
83 | |
84 | private int killDelay; |
85 | |
86 | private List completionListensers; |
87 | |
88 | private boolean readyForConnections; |
89 | |
90 | // collaborators |
91 | private HelpOptionsParser optionParser; |
92 | |
93 | private Utils utils; |
94 | |
95 | public MysqldResource() { |
96 | this(new Files().nullFile()); |
97 | } |
98 | |
99 | public MysqldResource(File baseDir) { |
100 | this(baseDir, new Files().nullFile(), MysqldResourceI.DEFAULT_VERSION, |
101 | System.out, System.err); |
102 | } |
103 | |
104 | public MysqldResource(File baseDir, File dataDir) { |
105 | this(baseDir, dataDir, MysqldResourceI.DEFAULT_VERSION, System.out, |
106 | System.err); |
107 | } |
108 | |
109 | public MysqldResource(File baseDir, File dataDir, String mysqlVersionString) { |
110 | this(baseDir, dataDir, mysqlVersionString, System.out, System.err); |
111 | } |
112 | |
113 | public MysqldResource(File baseDir, File dataDir, |
114 | String mysqlVersionString, PrintStream out, PrintStream err) { |
115 | this(baseDir, dataDir, mysqlVersionString, out, err, new Utils()); |
116 | } |
117 | |
118 | MysqldResource(File baseDir, File dataDir, String mysqlVersionString, |
119 | PrintStream out, PrintStream err, Utils util) { |
120 | this.out = out; |
121 | this.err = err; |
122 | this.utils = util; |
123 | this.optionParser = new HelpOptionsParser(err, utils); |
124 | this.killDelay = 30000; |
125 | this.baseDir = utils.files().validCononicalDir(baseDir, |
126 | utils.files().tmp(MYSQL_C_MXJ)); |
127 | this.dataDir = utils.files().validCononicalDir(dataDir, |
128 | new File(this.baseDir, DATA)); |
129 | setVersion(false, mysqlVersionString); |
130 | this.pidFile = null; |
131 | this.msgPrefix = "[" + utils.str().shortClassName(getClass()) + "] "; |
132 | this.options = new HashMap(); |
133 | this.setShell(null); |
134 | setOsAndArch(System.getProperty(Platform.OS_NAME), System |
135 | .getProperty(Platform.OS_ARCH)); |
136 | this.completionListensers = new ArrayList(); |
137 | initTrace(); |
138 | } |
139 | |
140 | private void initTrace() { |
141 | this.trace = new Exception(); |
142 | } |
143 | |
144 | /** |
145 | * Starts mysqld passing it the parameters specified in the arguments map. |
146 | * No effect if MySQL is already running |
147 | */ |
148 | public void start(String threadName, Map mysqldArgs) { |
149 | if ((getShell() != null) || processRunning()) { |
150 | printMessage("mysqld already running (process: " + pid() + ")"); |
151 | return; |
152 | } |
153 | |
154 | mysqldArgs.remove(MysqldResourceI.MYSQLD_VERSION); |
155 | options = optionParser.getOptionsFromHelp(getHelp(mysqldArgs)); |
156 | |
157 | // printMessage("mysqld : " + |
158 | // services.str().toString(mysqldArgs.entrySet())); |
159 | out.flush(); |
160 | addCompletionListenser(new Runnable() { |
161 | public void run() { |
162 | setReadyForConnection(false); |
163 | setShell(null); |
164 | completionListensers.remove(this); |
165 | } |
166 | }); |
167 | setShell(exec(threadName, mysqldArgs, out, err, true)); |
168 | |
169 | reportPid(); |
170 | |
171 | int port = 3306; |
172 | Object portArg = mysqldArgs.get(MysqldResourceI.PORT); |
173 | if (portArg != null) { |
174 | port = Integer.parseInt(portArg.toString()); |
175 | } |
176 | int triesBeforeGivingUp = (killDelay / 1000) * 4; |
177 | boolean ready = canConnectToServer(port, triesBeforeGivingUp); |
178 | setReadyForConnection(ready); |
179 | } |
180 | |
181 | boolean canConnectToServer(int port, int triesBeforeGivingUp) { |
182 | utils.str().classForName(Driver.class.getName()); |
183 | Connection conn = null; |
184 | int timeoutMilis = 250; |
185 | String bogusUser = "Connector/MXJ"; |
186 | String password = "Bogus Password"; |
187 | String url = "jdbc:mysql://localhost:" + port + "/test" |
188 | + "?connectTimeout=" + timeoutMilis; |
189 | for (int i = 0; i < triesBeforeGivingUp; i++) { |
190 | try { |
191 | conn = DriverManager.getConnection(url, bogusUser, password); |
192 | return true; /* should never happen */ |
193 | } catch (SQLException e) { |
194 | if (e.getErrorCode() == MysqlErrorNumbers.ER_ACCESS_DENIED_ERROR) { |
195 | return true; |
196 | } |
197 | } finally { |
198 | try { |
199 | if (conn != null) { |
200 | conn.close(); |
201 | } |
202 | } catch (Exception e) { |
203 | e.printStackTrace(); |
204 | } |
205 | } |
206 | } |
207 | return false; |
208 | } |
209 | |
210 | private void setReadyForConnection(boolean ready) { |
211 | readyForConnections = ready; |
212 | } |
213 | |
214 | public synchronized boolean isReadyForConnections() { |
215 | return readyForConnections; |
216 | } |
217 | |
218 | private void reportPid() { |
219 | boolean printed = false; |
220 | for (int i = 0; !printed && i < 50; i++) { |
221 | if (pidFile().exists() && pidFile().length() > 0) { |
222 | utils.threads().pause(25); |
223 | printMessage("mysqld running as process: " + pid()); |
224 | |
225 | out.flush(); |
226 | printed = true; |
227 | } |
228 | utils.threads().pause(100); |
229 | } |
230 | |
231 | reportIfNoPidfile(printed); |
232 | } |
233 | |
234 | synchronized String pid() { |
235 | if (pid == null) { |
236 | if (!pidFile().exists()) { |
237 | return "No PID"; |
238 | } |
239 | |
240 | pid = utils.files().asString(pidFile()).trim(); |
241 | } |
242 | return pid; |
243 | } |
244 | |
245 | void reportIfNoPidfile(boolean pidFileFound) { |
246 | if (!pidFileFound) { |
247 | printWarning("mysqld pid-file not found: " + pidFile()); |
248 | } |
249 | } |
250 | |
251 | /** |
252 | * Kills the MySQL process. |
253 | */ |
254 | public synchronized void shutdown() { |
255 | boolean haveShell = (getShell() != null); |
256 | if (!pidFile().exists() && !haveShell) { |
257 | printMessage("Mysqld not running. No file: " + pidFile()); |
258 | return; |
259 | } |
260 | printMessage("stopping mysqld (process: " + pid() + ")"); |
261 | |
262 | issueNormalKill(); |
263 | |
264 | if (processRunning()) { |
265 | issueForceKill(); |
266 | } |
267 | |
268 | if (shellRunning()) { |
269 | destroyShell(); |
270 | } |
271 | setShell(null); |
272 | |
273 | if (processRunning()) { |
274 | printWarning("Process " + pid + "still running; not deleting " |
275 | + pidFile()); |
276 | } else { |
277 | utils.threads().pause(150); |
278 | System.gc(); |
279 | utils.threads().pause(150); |
280 | pidFile().deleteOnExit(); |
281 | pidFile().delete(); |
282 | pidFile = null; |
283 | pid = null; |
284 | } |
285 | |
286 | setReadyForConnection(false); |
287 | |
288 | printMessage("clearing options"); |
289 | options.clear(); |
290 | out.flush(); |
291 | |
292 | printMessage("shutdown complete"); |
293 | } |
294 | |
295 | void destroyShell() { |
296 | String shellName = getShell().getName(); |
297 | printWarning("attempting to destroy thread " + shellName); |
298 | getShell().destroyProcess(); |
299 | waitForShellToDie(); |
300 | String msg = (shellRunning() ? "not " : "") + "destroyed."; |
301 | printWarning(shellName + " " + msg); |
302 | } |
303 | |
304 | void issueForceKill() { |
305 | printWarning("attempting to \"force kill\" " + pid()); |
306 | new ProcessUtil(pid(), err, err, baseDir, utils).forceKill(); |
307 | |
308 | waitForProcessToDie(); |
309 | if (processRunning()) { |
310 | String msg = (processRunning() ? "not " : "") + "killed."; |
311 | printWarning(pid() + " " + msg); |
312 | } else { |
313 | printMessage("force kill " + pid() + " issued."); |
314 | } |
315 | } |
316 | |
317 | private void issueNormalKill() { |
318 | if (!pidFile().exists()) { |
319 | printWarning("Not running? File not found: " + pidFile()); |
320 | return; |
321 | } |
322 | |
323 | new ProcessUtil(pid(), err, err, baseDir, utils).killNoThrow(); |
324 | waitForProcessToDie(); |
325 | } |
326 | |
327 | private void waitForProcessToDie() { |
328 | long giveUp = System.currentTimeMillis() + killDelay; |
329 | while (processRunning() && System.currentTimeMillis() < giveUp) { |
330 | utils.threads().pause(250); |
331 | } |
332 | } |
333 | |
334 | private void waitForShellToDie() { |
335 | long giveUp = System.currentTimeMillis() + killDelay; |
336 | while (shellRunning() && System.currentTimeMillis() < giveUp) { |
337 | utils.threads().pause(250); |
338 | } |
339 | } |
340 | |
341 | public synchronized Map getServerOptions() { |
342 | if (options.isEmpty()) { |
343 | options = optionParser.getOptionsFromHelp(getHelp(new HashMap())); |
344 | } |
345 | return new HashMap(options); |
346 | } |
347 | |
348 | public synchronized boolean isRunning() { |
349 | return shellRunning() || processRunning(); |
350 | } |
351 | |
352 | private boolean processRunning() { |
353 | if (!pidFile().exists()) { |
354 | return false; |
355 | } |
356 | return new ProcessUtil(pid(), out, err, baseDir, utils).isRunning(); |
357 | } |
358 | |
359 | private boolean shellRunning() { |
360 | return (getShell() != null) && (getShell().isAlive()); |
361 | } |
362 | |
363 | public synchronized String getVersion() { |
364 | return versionString; |
365 | } |
366 | |
367 | private String getVersionDir() { |
368 | return getVersion().replaceAll("\\.", "-"); |
369 | } |
370 | |
371 | private synchronized void setVersion(boolean check, |
372 | String mysqlVersionString) { |
373 | if (check && isRunning()) { |
374 | throw new IllegalStateException("Already running"); |
375 | } |
376 | |
377 | if (mysqlVersionString == null) { |
378 | versionString = DEFAULT_VERSION; |
379 | } else { |
380 | versionString = mysqlVersionString; |
381 | } |
382 | } |
383 | |
384 | public synchronized void setVersion(String mysqlVersionString) { |
385 | setVersion(true, mysqlVersionString); |
386 | } |
387 | |
388 | private void printMessage(String msg) { |
389 | println(out, msg); |
390 | } |
391 | |
392 | private void printWarning(String msg) { |
393 | println(err, ""); |
394 | println(err, msg); |
395 | } |
396 | |
397 | private void println(PrintStream stream, String msg) { |
398 | stream.println(msgPrefix + msg); |
399 | } |
400 | |
401 | /* called from constructor, over-ride with care */ |
402 | final void setOsAndArch(String osName, String osArch) { |
403 | String name = osName; |
404 | if (osName.indexOf("Win") != -1) { |
405 | name = "Win"; |
406 | } |
407 | this.osName = stripUnwantedChars(name); |
408 | this.osArch = stripUnwantedChars(osArch); |
409 | } |
410 | |
411 | String stripUnwantedChars(String str) { |
412 | return str.replace(' ', '_').replace('/', '_').replace('\\', '_'); |
413 | } |
414 | |
415 | /** called from option parser as well */ |
416 | synchronized Shell exec(String threadName, Map mysqldArgs, |
417 | PrintStream outStream, PrintStream errStream, boolean withListeners) { |
418 | |
419 | makeMysqld(); |
420 | ensureEssentialFilesExist(); |
421 | |
422 | adjustParameterMap(mysqldArgs); |
423 | String[] args = constructArgs(mysqldArgs); |
424 | outStream.println(new ListToString().toString(args)); |
425 | |
426 | Shell launch = utils.shellFactory().newShell(args, threadName, |
427 | outStream, errStream); |
428 | if (withListeners) { |
429 | for (int i = 0; i < completionListensers.size(); i++) { |
430 | Runnable listener = (Runnable) completionListensers.get(i); |
431 | launch.addCompletionListener(listener); |
432 | } |
433 | } |
434 | launch.setDaemon(true); |
435 | |
436 | printMessage("launching mysqld (" + threadName + ")"); |
437 | |
438 | launch.start(); |
439 | return launch; |
440 | } |
441 | |
442 | private void adjustParameterMap(Map mysqldArgs) { |
443 | ensureDir(mysqldArgs, baseDir, MysqldResourceI.BASEDIR); |
444 | ensureDir(mysqldArgs, dataDir, MysqldResourceI.DATADIR); |
445 | mysqldArgs.put(MysqldResourceI.PID_FILE, utils.files().getPath( |
446 | pidFile())); |
447 | ensureSocket(mysqldArgs); |
448 | } |
449 | |
450 | File makeMysqld() { |
451 | final File mysqld = getMysqldFilePointer(); |
452 | if (!mysqld.exists()) { |
453 | mysqld.getParentFile().mkdirs(); |
454 | utils.streams().createFileFromResource(getResourceName(), mysqld); |
455 | } |
456 | utils.files().addExecutableRights(mysqld, out, err); |
457 | return mysqld; |
458 | } |
459 | |
460 | String getResourceName() { |
461 | String dir = osName + "-" + osArch; |
462 | String name = executableName(); |
463 | return getVersionDir() + Streams.RESOURCE_SEPARATOR + dir |
464 | + Streams.RESOURCE_SEPARATOR + name; |
465 | } |
466 | |
467 | private String executableName() { |
468 | String mysqld = "mysqld"; |
469 | return ((isWindows()) ? mysqld + "-nt.exe" : mysqld); |
470 | } |
471 | |
472 | boolean isWindows() { |
473 | return osName.equals("Win"); |
474 | } |
475 | |
476 | File getMysqldFilePointer() { |
477 | File bin = new File(baseDir, "bin"); |
478 | return new File(bin, executableName()); |
479 | } |
480 | |
481 | void ensureEssentialFilesExist() { |
482 | utils.streams().expandResourceJar(dataDir, |
483 | getVersionDir() + Streams.RESOURCE_SEPARATOR + "data_dir.jar"); |
484 | utils.streams().expandResourceJar(baseDir, |
485 | getVersionDir() + Streams.RESOURCE_SEPARATOR + shareJar()); |
486 | } |
487 | |
488 | void ensureSocket(Map mysqldArgs) { |
489 | String socketString = (String) mysqldArgs.get(MysqldResourceI.SOCKET); |
490 | if (socketString != null) { |
491 | return; |
492 | } |
493 | mysqldArgs.put(MysqldResourceI.SOCKET, "mysql.sock"); |
494 | } |
495 | |
496 | private void ensureDir(Map mysqldArgs, File expected, String key) { |
497 | String dirString = (String) mysqldArgs.get(key); |
498 | if (dirString != null) { |
499 | File asConnonical = utils.files().validCononicalDir( |
500 | new File(dirString)); |
501 | if (!expected.equals(asConnonical)) { |
502 | String msg = dirString + " not equal to " + expected; |
503 | throw new IllegalArgumentException(msg); |
504 | } |
505 | } |
506 | mysqldArgs.put(key, utils.files().getPath(expected)); |
507 | } |
508 | |
509 | String[] constructArgs(Map mysqldArgs) { |
510 | List strs = new ArrayList(); |
511 | strs.add(utils.files().getPath(getMysqldFilePointer())); |
512 | |
513 | strs.add("--no-defaults"); |
514 | if (isWindows()) { |
515 | strs.add("--console"); |
516 | } |
517 | Iterator it = mysqldArgs.entrySet().iterator(); |
518 | while (it.hasNext()) { |
519 | Map.Entry entry = (Map.Entry) it.next(); |
520 | String key = (String) entry.getKey(); |
521 | String value = (String) entry.getValue(); |
522 | StringBuffer buf = new StringBuffer("--"); |
523 | buf.append(key); |
524 | if (value != null) { |
525 | buf.append("="); |
526 | buf.append(value); |
527 | } |
528 | strs.add(buf.toString()); |
529 | } |
530 | |
531 | return utils.str().toStringArray(strs); |
532 | } |
533 | |
534 | protected void finalize() throws Throwable { |
535 | if (getShell() != null) { |
536 | printWarning("resource released without closure."); |
537 | trace.printStackTrace(err); |
538 | } |
539 | super.finalize(); |
540 | } |
541 | |
542 | String shareJar() { |
543 | String shareJar = "share_dir.jar"; |
544 | if (isWindows()) { |
545 | shareJar = "win_" + shareJar; |
546 | } |
547 | return shareJar; |
548 | } |
549 | |
550 | void setShell(Shell shell) { |
551 | this.shell = shell; |
552 | } |
553 | |
554 | Shell getShell() { |
555 | return shell; |
556 | } |
557 | |
558 | public File getBaseDir() { |
559 | return baseDir; |
560 | } |
561 | |
562 | public File getDataDir() { |
563 | return dataDir; |
564 | } |
565 | |
566 | public synchronized void setKillDelay(int millis) { |
567 | this.killDelay = millis; |
568 | } |
569 | |
570 | public synchronized void addCompletionListenser(Runnable listener) { |
571 | completionListensers.add(listener); |
572 | } |
573 | |
574 | private String getHelp(Map params) { |
575 | ByteArrayOutputStream bos = new ByteArrayOutputStream(); |
576 | PrintStream capturedOut = new PrintStream(bos); |
577 | |
578 | params.put("help", null); |
579 | params.put("verbose", null); |
580 | |
581 | exec("getOptions", params, capturedOut, capturedOut, false).join(); |
582 | |
583 | params.remove("help"); |
584 | params.remove("verbose"); |
585 | |
586 | utils.threads().pause(500); |
587 | capturedOut.flush(); |
588 | capturedOut.close(); // should flush(); |
589 | |
590 | return new String(bos.toByteArray()); |
591 | } |
592 | |
593 | synchronized private File pidFile() { |
594 | if (pidFile == null) { |
595 | String className = utils.str().shortClassName(getClass()); |
596 | pidFile = new File(this.dataDir, className + ".pid"); |
597 | } |
598 | return pidFile; |
599 | } |
600 | |
601 | // --------------------------------------------------------- |
602 | static void printUsage(PrintStream out) { |
603 | String command = "java " + MysqldResource.class.getName(); |
604 | String basedir = " --" + MysqldResourceI.BASEDIR; |
605 | String datadir = " --" + MysqldResourceI.DATADIR; |
606 | out.println("Usage to start: "); |
607 | out.println(command + " [ server options ]"); |
608 | out.println(); |
609 | out.println("Usage to shutdown: "); |
610 | out.println(command + " --shutdown [" + basedir |
611 | + "=/full/path/to/basedir ]"); |
612 | out.println(); |
613 | out.println("Common server options include:"); |
614 | out.println(basedir + "=/full/path/to/basedir"); |
615 | out.println(datadir + "=/full/path/to/datadir"); |
616 | out.println(" --" + MysqldResourceI.SOCKET |
617 | + "=/full/path/to/socketfile"); |
618 | out.println(); |
619 | out.println("Example:"); |
620 | out.println(command + basedir + "=/home/duke/dukeapp/db" + datadir |
621 | + "=/data/dukeapp/data" + " --max_allowed_packet=65000000"); |
622 | out.println(command + " --shutdown" + basedir |
623 | + "=/home/duke/dukeapp/db"); |
624 | out.println(); |
625 | } |
626 | |
627 | public static void main(String[] args) { |
628 | CommandLineOptionsParser clop = new CommandLineOptionsParser(args); |
629 | if (clop.containsKey("help")) { |
630 | printUsage(System.out); |
631 | return; |
632 | } |
633 | |
634 | PrintStream out = System.out; |
635 | PrintStream err = System.err; |
636 | if (clop.containsKey("silent")) { |
637 | clop.remove("silent"); |
638 | PrintStream devNull = new NullPrintStream(); |
639 | out = devNull; |
640 | err = devNull; |
641 | } |
642 | |
643 | MysqldResource mysqld = new MysqldResource(clop.getBaseDir(), clop |
644 | .getDataDir(), clop.getVersion(), out, err); |
645 | if (clop.isShutdown()) { |
646 | mysqld.shutdown(); |
647 | return; |
648 | } |
649 | |
650 | mysqld.start(new Threads().newName(), clop.asMap()); |
651 | } |
652 | } |