/*
Copyright (C) 2004 MySQL AB
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License version 2 as
published by the Free Software Foundation.
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
*/
package com.mysql.management;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.PrintStream;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import org.apache.log4j.Logger;
import com.mysql.jdbc.Driver;
import com.mysql.jdbc.MysqlErrorNumbers;
import com.mysql.management.util.ListToString;
import com.mysql.management.util.Platform;
import com.mysql.management.util.ProcessUtil;
import com.mysql.management.util.Shell;
import com.mysql.management.util.Streams;
import com.mysql.management.util.Utils;
/**
* This class is final simply as a hint to the compiler, it may be un-finalized
* safely.
*
* @author Eric Herman <eric@mysql.com>
*/
@SuppressWarnings({ "rawtypes", "unchecked" })
public class MysqldResource implements MysqldResourceI {
private static final Logger log = Logger.getLogger(MysqldResource.class);
public static final String MYSQL_C_MXJ = "mysql-c.mxj";
public static final String DATA = "data";
protected String versionString;
private Map options;
private Shell shell;
protected final File baseDir;
protected final File dataDir;
private final File pidFile;
private final File portFile;
private String msgPrefix;
private String pid;
private String osName;
private String osArch;
protected PrintStream out;
protected PrintStream err;
private Exception trace;
private int killDelay;
private List completionListensers;
private boolean readyForConnections;
// collaborators
private HelpOptionsParser optionParser;
protected Utils utils;
private String binDir;
// public MysqldResource() {
// this(new Files().nullFile());
// }
//
// public MysqldResource(File baseDir) {
// this(baseDir, new Files().nullFile());
// }
//
// public MysqldResource(File baseDir, File dataDir) {
// this(baseDir, dataDir, null);
// }
//
// public MysqldResource(File baseDir, File dataDir, String mysqlVersionString) {
// this(baseDir, dataDir, mysqlVersionString, true);
// }
//
// public MysqldResource(File baseDir, File dataDir,
// String mysqlVersionString, boolean guessArch) {
// this(baseDir, dataDir, mysqlVersionString, guessArch, System.out,
// System.err);
// }
//
// public MysqldResource(File baseDir, File dataDir,
// String mysqlVersionString, boolean guessArch, PrintStream out,
// PrintStream err) {
// this(baseDir, dataDir, mysqlVersionString, guessArch, out, err,
// new Utils());
// }
//
// public MysqldResource(File baseDir, File dataDir, String mysqlVersionString,
// boolean guessArch, PrintStream out, PrintStream err, Utils util) {
// }
public MysqldResource(File baseDir, File dataDir, String mysqlVersionString,
boolean guessArch, PrintStream out, PrintStream err, Utils util, String binDir) {
this.out = out;
this.err = err;
this.utils = util;
this.binDir = binDir;
this.optionParser = new HelpOptionsParser(err, utils);
this.killDelay = 30000;
this.baseDir = utils.files().validCononicalDir(baseDir,
utils.files().tmp(MYSQL_C_MXJ));
this.dataDir = utils.files().validCononicalDir(dataDir,
new File(this.baseDir, DATA));
String className = utils.str().shortClassName(getClass());
this.pidFile = utils.files().cononical(
new File(this.dataDir, className + ".pid"));
this.portFile = new File(dataDir, "port");
setVersion(false, mysqlVersionString);
this.msgPrefix = "[" + utils.str().shortClassName(getClass()) + "] ";
this.options = new HashMap();
this.setShell(null);
setOsAndArch(System.getProperty(Platform.OS_NAME), guessArch, System
.getProperty(Platform.OS_ARCH));
this.completionListensers = new ArrayList();
initTrace();
}
protected String binDir() {
return binDir;
}
private void initTrace() {
this.trace = new Exception();
}
/**
* Starts mysqld passing it the parameters specified in the arguments map.
* No effect if MySQL is already running
*/
@Override
public synchronized void start(String threadName, Map mysqldArgs) {
start(threadName, mysqldArgs, false);
}
@Override
public synchronized void start(String threadName, Map mysqldArgs,
boolean populateAllOptions) {
if ((getShell() != null) || processRunning()) {
printMessage("mysqld already running (process: " + pid() + ")");
return;
}
mysqldArgs = new HashMap(mysqldArgs);
int port = 3306;
Object portArg = mysqldArgs.get(MysqldResourceI.PORT);
if (portArg != null) {
port = Integer.parseInt(portArg.toString());
}
String portStr = "" + port;
mysqldArgs.put(MysqldResourceI.PORT, portStr);
mysqldArgs.remove(MysqldResourceI.MYSQLD_VERSION);
mysqldArgs.remove(MysqldResourceI.USE_DEFAULT_ARCHITECTURE);
if (populateAllOptions) {
options = optionParser.getOptionsFromHelp(getHelp(mysqldArgs));
} else {
options = new HashMap();
options.putAll(mysqldArgs);
}
// printMessage("mysqld : " +
// services.str().toString(mysqldArgs.entrySet()));
out.flush();
addCompletionListenser(new Runnable() {
@Override
public void run() {
setReadyForConnection(false);
setShell(null);
completionListensers.remove(this);
}
});
setShell(exec(threadName, mysqldArgs, out, err, true));
reportPid();
utils.files().writeString(portFile, portStr);
boolean ready = canConnectToServer(port, killDelay);
setReadyForConnection(ready);
}
// Will wait 250 miliseconds between each try.
boolean canConnectToServer(int port, int milisecondsBeforeGivingUp) {
int triesBeforeGivingUp = 1 + (milisecondsBeforeGivingUp / 1000) * 4;
utils.str().classForName(Driver.class.getName());
Connection conn = null;
String bogusUser = "Connector/MXJ";
String password = "Bogus Password";
String url = "jdbc:mysql://localhost:"
+ port
+ "/test"
+ "?connectTimeout=150"
+ "&socketFactory=com.mysql.management.util.PatchedStandardSocketFactory";
for (int i = 0; i < triesBeforeGivingUp; i++) {
try {
conn = DriverManager.getConnection(url, bogusUser, password);
return true; /* should never happen */
} catch (SQLException e) {
if (e.getErrorCode() == MysqlErrorNumbers.ER_ACCESS_DENIED_ERROR) {
return true;
}
} finally {
try {
if (conn != null) {
conn.close();
}
} catch (Exception e) {
e.printStackTrace();
}
}
utils.threads().pause(100);
}
return false;
}
private void setReadyForConnection(boolean ready) {
readyForConnections = ready;
}
@Override
public synchronized boolean isReadyForConnections() {
return readyForConnections;
}
private void reportPid() {
boolean printed = false;
for (int i = 0; !printed && i < 50; i++) {
if (pidFile.exists() && pidFile.length() > 0) {
utils.threads().pause(25);
printMessage("mysqld running as process: " + pid());
out.flush();
printed = true;
}
utils.threads().pause(100);
}
reportIfNoPidfile(printed);
}
synchronized String pid() {
if (pid == null) {
if (!pidFile.exists()) {
return "No PID";
}
pid = utils.files().asString(pidFile).trim();
}
return pid;
}
void reportIfNoPidfile(boolean pidFileFound) {
if (!pidFileFound) {
printWarning("mysqld pid-file not found: " + pidFile);
}
}
/**
* Kills the MySQL process.
*/
@Override
public synchronized void shutdown() {
boolean haveShell = (getShell() != null);
if (!pidFile.exists() && !haveShell) {
printMessage("Mysqld not running. No file: " + pidFile);
return;
}
printMessage("stopping mysqld (process: " + pid() + ")");
issueNormalKill();
if (processRunning()) {
issueForceKill();
}
if (shellRunning()) {
destroyShell();
}
setShell(null);
if (processRunning()) {
printWarning("Process " + pid + "still running; not deleting "
+ pidFile);
} else {
utils.threads().pause(150);
System.gc();
utils.threads().pause(150);
pidFile.deleteOnExit();
pidFile.delete();
pid = null;
}
setReadyForConnection(false);
printMessage("clearing options");
options.clear();
out.flush();
printMessage("shutdown complete");
}
void destroyShell() {
String shellName = getShell().getName();
printWarning("attempting to destroy thread " + shellName);
getShell().destroyProcess();
waitForShellToDie();
String msg = (shellRunning() ? "not " : "") + "destroyed.";
printWarning(shellName + " " + msg);
}
void issueForceKill() {
printWarning("attempting to \"force kill\" " + pid());
new ProcessUtil(pid(), err, err, baseDir, utils).forceKill();
waitForProcessToDie();
if (processRunning()) {
String msg = (processRunning() ? "not " : "") + "killed.";
printWarning(pid() + " " + msg);
} else {
printMessage("force kill " + pid() + " issued.");
}
}
private void issueNormalKill() {
if (!pidFile.exists()) {
printWarning("Not running? File not found: " + pidFile);
return;
}
new ProcessUtil(pid(), err, err, baseDir, utils).killNoThrow();
waitForProcessToDie();
}
private void waitForProcessToDie() {
long giveUp = System.currentTimeMillis() + killDelay;
while (processRunning() && System.currentTimeMillis() < giveUp) {
utils.threads().pause(250);
}
}
private void waitForShellToDie() {
long giveUp = System.currentTimeMillis() + killDelay;
while (shellRunning() && System.currentTimeMillis() < giveUp) {
utils.threads().pause(250);
}
}
@Override
public synchronized Map getServerOptions() {
if (options.isEmpty()) {
options = optionParser.getOptionsFromHelp(getHelp(new HashMap()));
}
return new HashMap(options);
}
@Override
public synchronized boolean isRunning() {
return shellRunning() || processRunning();
}
private boolean processRunning() {
if (!pidFile.exists()) {
return false;
}
return new ProcessUtil(pid(), out, err, baseDir, utils).isRunning();
}
private boolean shellRunning() {
return (getShell() != null) && (getShell().isAlive());
}
@Override
public synchronized String getVersion() {
return versionString;
}
private String getVersionDir() {
return getVersion().replaceAll("\\.", "-");
}
protected synchronized void setVersion(boolean checkRunning,
String mysqlVersionString) {
if (checkRunning && isRunning()) {
throw new IllegalStateException("Already running");
}
if (mysqlVersionString == null || mysqlVersionString.equals("")) {
versionString = utils.streams()
.getSystemPropertyWithDefaultFromResource(MYSQLD_VERSION,
"connector-mxj.properties", err);
} else {
versionString = mysqlVersionString;
}
versionString.trim();
}
@Override
public synchronized void setVersion(String mysqlVersionString) {
setVersion(true, mysqlVersionString);
}
private void printMessage(String msg) {
println(out, msg);
log.info(msg);
}
private void printWarning(String msg) {
println(err, "");
println(err, msg);
log.warn(msg);
}
private void println(PrintStream stream, String msg) {
stream.println(msgPrefix + msg);
}
/* called from constructor, over-ride with care */
final void setOsAndArch(String osName, boolean defaultArch, String osArch) {
/*
* to do: Remove use of defaultArch and "Win" shortcuts.
*
* PROBLEM: If on Linux-ppc, we shouldn't even try Linux-i386.
*
* SOLUTION: Replace current code with a resource table of os-arch
* combinations and only remap if an entry exists in the table. Consider
* wild-card matching. This resource table should be in a text editable
* file, not a class file.
*
* SOLUTION DOWN-SIDES: Long-term, we may wish to provide a way to get
* more information than simply os name and architecture. For instance,
* "SunOS 8 or SunOS 10" or maybe "sparc 32 or sparc 64". At that time
* we may wish to provide a real interface with user-plugablity, but
* there is no immediate demand.
*/
String name = osName;
if (osName.indexOf("Win") != -1) {
name = "Win";
osArch = defaultArch ? "x86" : stripUnwantedChars(osArch);
} else if (osName.indexOf("Linux") != -1) {
osArch = defaultArch ? "i386" : stripUnwantedChars(osArch);
}
this.osName = stripUnwantedChars(name);
this.osArch = stripUnwantedChars(osArch);
}
String stripUnwantedChars(String str) {
return str.replace(' ', '_').replace('/', '_').replace('\\', '_');
}
private Shell exec(String threadName, Map mysqldArgs,
PrintStream outStream, PrintStream errStream, boolean withListeners) {
makeMysqld();
ensureEssentialFilesExist();
adjustParameterMap(mysqldArgs);
String[] args = constructArgs(mysqldArgs);
outStream.println(new ListToString().toString(args));
log.info("starting mysqld: " + new ListToString().toString(args));
Shell launch = utils.shellFactory().newShell(args, threadName,
outStream, errStream);
if (withListeners) {
for (int i = 0; i < completionListensers.size(); i++) {
Runnable listener = (Runnable) completionListensers.get(i);
launch.addCompletionListener(listener);
}
}
launch.setDaemon(true);
printMessage("launching mysqld (" + threadName + ")");
launch.start();
return launch;
}
private void adjustParameterMap(Map mysqldArgs) {
ensureDir(mysqldArgs, baseDir, MysqldResourceI.BASEDIR);
ensureDir(mysqldArgs, dataDir, MysqldResourceI.DATADIR);
mysqldArgs.put(MysqldResourceI.PID_FILE, pidFile.getPath());
ensureSocket(mysqldArgs);
}
protected File makeMysqld() {
final File mysqld = getMysqldFilePointer();
if (!mysqld.exists()) {
mysqld.getParentFile().mkdirs();
utils.streams().createFileFromResource(getResourceName(), mysqld);
}
utils.files().addExecutableRights(mysqld, out, err);
return mysqld;
}
String getResourceName() {
String dir = os_arch();
String name = executableName();
return getVersionDir() + Streams.RESOURCE_SEPARATOR + dir
+ Streams.RESOURCE_SEPARATOR + name;
}
String os_arch() {
return osName + "-" + osArch;
}
private String executableName()
{
return "mysqld";
}
protected boolean isWindows()
{
return osName.equals("Win");
}
protected File getMysqldFilePointer() {
File path = new File(binDir);
return new File(path, executableName());
}
public void ensureEssentialFilesExist() {
utils.streams().expandResourceJar(dataDir,
getVersionDir() + Streams.RESOURCE_SEPARATOR + "data_dir.jar");
utils.streams().expandResourceJar(baseDir,
getVersionDir() + Streams.RESOURCE_SEPARATOR + shareJar());
}
void ensureSocket(Map mysqldArgs) {
String socketString = (String) mysqldArgs.get(MysqldResourceI.SOCKET);
if (socketString != null) {
return;
}
mysqldArgs.put(MysqldResourceI.SOCKET, "mysql.sock");
}
private void ensureDir(Map mysqldArgs, File expected, String key) {
String dirString = (String) mysqldArgs.get(key);
if (dirString != null) {
File asConnonical = utils.files().validCononicalDir(
new File(dirString));
if (!expected.equals(asConnonical)) {
String msg = dirString + " not equal to " + expected;
throw new IllegalArgumentException(msg);
}
}
mysqldArgs.put(key, utils.files().getPath(expected));
}
protected String[] constructArgs(Map mysqldArgs) {
List strs = new ArrayList();
strs.add(utils.files().getPath(getMysqldFilePointer()));
strs.add("--no-defaults");
if (isWindows()) {
strs.add("--console");
}
Iterator it = mysqldArgs.entrySet().iterator();
while (it.hasNext()) {
Map.Entry entry = (Map.Entry) it.next();
String key = (String) entry.getKey();
String value = (String) entry.getValue();
StringBuffer buf = new StringBuffer("--");
buf.append(key);
if (value != null) {
buf.append("=");
buf.append(value);
}
strs.add(buf.toString());
}
return utils.str().toStringArray(strs);
}
@Override
protected void finalize() throws Throwable {
if (getShell() != null) {
printWarning("resource released without closure.");
trace.printStackTrace(err);
}
super.finalize();
}
String shareJar() {
String shareJar = "share_dir.jar";
if (isWindows()) {
shareJar = "win_" + shareJar;
}
return shareJar;
}
void setShell(Shell shell) {
this.shell = shell;
}
Shell getShell() {
return shell;
}
@Override
public File getBaseDir() {
return baseDir;
}
@Override
public File getDataDir() {
return dataDir;
}
@Override
public synchronized void setKillDelay(int millis) {
this.killDelay = millis;
}
@Override
public synchronized void addCompletionListenser(Runnable listener) {
completionListensers.add(listener);
}
private String getHelp(Map params) {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
PrintStream capturedOut = new PrintStream(bos);
params.put("help", null);
params.put("verbose", null);
exec("getOptions", params, capturedOut, capturedOut, false).join();
params.remove("help");
params.remove("verbose");
utils.threads().pause(500);
capturedOut.flush();
capturedOut.close(); // should flush();
return new String(bos.toByteArray());
}
@Override
public synchronized int getPort() {
if (isRunning()) {
String portStr = utils.files().asString(portFile).trim();
return Integer.parseInt(portStr);
}
return 0;
}
// ---------------------------------------------------------
static void printUsage(PrintStream out) {
String command = "java " + MysqldResource.class.getName();
String basedir = " --" + MysqldResourceI.BASEDIR;
String datadir = " --" + MysqldResourceI.DATADIR;
out.println("Usage to start: ");
out.println(command + " [ server options ]");
out.println();
out.println("Usage to shutdown: ");
out.println(command + " --shutdown [" + basedir
+ "=/full/path/to/basedir ]");
out.println();
out.println("Common server options include:");
out.println(basedir + "=/full/path/to/basedir");
out.println(datadir + "=/full/path/to/datadir");
out.println(" --" + MysqldResourceI.SOCKET
+ "=/full/path/to/socketfile");
out.println();
out.println("Example:");
out.println(command + basedir + "=/home/duke/dukeapp/db" + datadir
+ "=/data/dukeapp/data" + " --max_allowed_packet=65000000");
out.println(command + " --shutdown" + basedir
+ "=/home/duke/dukeapp/db");
out.println();
}
}