net.oneandone.stool.Start Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of main Show documentation
Show all versions of main Show documentation
Stool's main component. Java Library, cli, setup code.
/**
* Copyright 1&1 Internet AG, https://github.com/1and1/
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.oneandone.stool;
import net.oneandone.stool.configuration.Until;
import net.oneandone.stool.locking.Mode;
import net.oneandone.stool.stage.Stage;
import net.oneandone.stool.util.Files;
import net.oneandone.stool.util.Ports;
import net.oneandone.stool.util.ServerXml;
import net.oneandone.stool.util.Session;
import net.oneandone.sushi.cli.ArgumentException;
import net.oneandone.sushi.cli.Option;
import net.oneandone.sushi.fs.GetLastModifiedException;
import net.oneandone.sushi.fs.Node;
import net.oneandone.sushi.fs.file.FileNode;
import net.oneandone.sushi.io.OS;
import net.oneandone.sushi.util.Separator;
import net.oneandone.sushi.util.Strings;
import net.oneandone.sushi.util.Substitution;
import net.oneandone.sushi.util.SubstitutionException;
import org.xml.sax.SAXException;
import java.io.IOException;
import java.io.InputStream;
import java.net.Socket;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class Start extends StageCommand {
@Option("debug")
private boolean debug = false;
@Option("suspend")
private boolean suspend = false;
@Option("tail")
private boolean tail = false;
public Start(Session session, boolean debug, boolean suspend) {
super(session, Mode.EXCLUSIVE, Mode.EXCLUSIVE, Mode.SHARED);
this.debug = debug;
this.suspend = suspend;
}
public static String tomcatName(String version) {
return "apache-tomcat-" + version;
}
@Override
public void doInvoke(Stage stage) throws Exception {
FileNode download;
Ports ports;
// to avoid running into a ping timeout below:
stage.session.configuration.verfiyHostname();
serviceWrapperOpt(stage.config().tomcatService);
download = tomcatOpt(stage.config().tomcatVersion);
checkUntil(stage.config().until);
checkCommitted(stage);
checkNotStarted(stage);
ports = session.pool().allocate(stage, Collections.emptyMap());
copyTemplate(stage, ports);
copyTomcatBaseOpt(download, stage.shared(), stage.config().tomcatVersion);
if (session.bedroom.stages().contains(stage.getName())) {
console.info.println("leaving sleeping state");
session.bedroom.remove(session.gson, stage.getName());
}
if (debug || suspend) {
console.info.println("debugging enabled on port " + ports.debug());
}
stage.start(console, ports);
ping(stage);
if (tail) {
doTail(stage);
}
}
private void doTail(Stage stage) throws IOException {
List logs;
int c;
Node log;
logs = stage.shared().find("tomcat/logs/catalina*.log");
if (logs.size() == 0) {
throw new IOException("no log files found");
}
Collections.sort(logs, (left, right) -> {
try {
return (int) (right.getLastModified() - left.getLastModified());
} catch (GetLastModifiedException e) {
throw new IllegalStateException(e);
}
});
log = logs.get(0);
console.info.println("tail " + log);
console.info.println("Press Ctrl-C to abort.");
try (InputStream src = log.createInputStream()) {
while (true) {
if (src.available() == 0) {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
console.info.println("[interrupted]");
break;
}
continue;
}
c = src.read();
if (c == -1) {
console.info.println("[closed]");
break;
}
console.info.print((char) c);
}
}
}
private void checkNotStarted(Stage stage) throws IOException {
if (stage.state().equals(Stage.State.UP)) {
throw new IOException("Stage is already running.");
}
}
private void ping(Stage stage) throws IOException, URISyntaxException, InterruptedException {
URI uri;
Socket socket;
InputStream in;
int count;
console.info.println("Ping'n Applications.");
for (String url : stage.urlMap().values()) {
if (url.startsWith("http://")) {
uri = new URI(url);
console.verbose.println("Ping'n " + url);
count = 0;
while (true) {
try {
socket = new Socket(uri.getHost(), uri.getPort());
in = socket.getInputStream();
in.close();
break;
} catch (IOException e) {
console.verbose.println("port not ready yet: " + e.getCause());
Thread.sleep(100);
count++;
if (count > 10 * 60 * 5) {
throw new IOException(url + ": ping timed out");
}
}
}
}
}
}
private void copyTemplate(Stage stage, Ports ports) throws Exception {
FileNode shared;
shared = stage.shared();
Files.template(console.verbose, world.resource("templates/stage"), shared, variables(stage, ports));
// manually create empty subdirectories, because git doesn't know them
// CAUTION: the log directory is created by "stool create" (because it contains log files)
for (String dir : new String[] {"ssl", "run" }) {
Files.createStoolDirectoryOpt(console.verbose, shared.join(dir));
}
}
private FileNode tomcatOpt(String version) throws IOException {
FileNode download;
String name;
FileNode base;
name = tomcatName(version);
download = session.downloadCache().join(name + ".tar.gz");
if (!download.exists()) {
downloadFile(subst(session.configuration.downloadTomcat, version), download);
download.checkFile();
}
base = session.lib.join("tomcat", name);
if (!base.exists()) {
tar(base.getParent(), "zxf", download.getAbsolute(), name + "/lib", name + "/bin");
base.checkDirectory();
}
return download;
}
private static String subst(String pattern, String version) {
Map variables;
variables = new HashMap<>();
variables.put("version", version);
variables.put("major", version.substring(0, version.indexOf('.')));
try {
return Substitution.ant().apply(pattern, variables);
} catch (SubstitutionException e) {
throw new ArgumentException("invalid url pattern: " + pattern, e);
}
}
private void serviceWrapperOpt(String version) throws IOException {
FileNode download;
String name;
FileNode base;
name = serviceWrapperName(version);
download = session.downloadCache().join(name + ".tar.gz");
if (!download.exists()) {
downloadFile(subst(session.configuration.downloadServiceWrapper, version), download);
download.checkFile();
}
base = session.lib.join("service-wrapper", name);
if (!base.exists()) {
tar(base.getParent(), "zxf", download.getAbsolute());
base.checkDirectory();
}
}
//TODO: work-around for sushi http problem with proxies
// TODO: race condition for simultaneous downloads by different users
private void downloadFile(String url, FileNode dest) throws IOException {
console.info.println("downloading " + url + " ...");
try {
doDownload(url, dest);
} catch (IOException e) {
throw new IOException("download failed: " + url
+ "\nAs a work-around, you can download it manually an place it at " + dest.getAbsolute()
+ "\nDetails: " + e.getMessage(), e);
}
}
//TODO: work-around for sushi http problem with proxies
// TODO: race condition for simultaneous downloads by different users
private static void doDownload(String url, FileNode dest) throws IOException {
if (OS.CURRENT != OS.MAC) {
// don't use sushi, it's not proxy-aware
dest.getParent().exec("wget", "--tries=1", "--connect-timeout=5", "-q", "-O", dest.getName(), url);
} else {
// wget not available on Mac, but Mac usually have no proxy
dest.getWorld().validNode(url).copyFile(dest);
}
}
public static String serviceWrapperName(String version) {
String platform;
String name;
platform = (OS.CURRENT == OS.LINUX) ? "linux-x86-64" : "macosx-universal-64";
name = "wrapper-" + platform + "-" + version;
return name;
}
private void tar(FileNode directory, String... args) throws IOException {
String output;
output = directory.exec(Strings.cons("tar", args));
if (!output.trim().isEmpty()) {
throw new IOException("unexpected output by tar command: " + output);
}
}
private void copyTomcatBaseOpt(FileNode download, FileNode shared, String version) throws IOException, SAXException {
String name;
FileNode src;
FileNode dest;
ServerXml serverXml;
FileNode file;
name = tomcatName(version);
dest = shared.join("tomcat");
if (!dest.exists()) {
tar(shared, "zxf",
download.getAbsolute(), "--exclude", name + "/lib", "--exclude", name + "/bin", "--exclude", name + "/webapps");
src = shared.join(name);
src.move(dest);
file = dest.join("conf/server.xml");
serverXml = ServerXml.load(file, session.configuration.hostname);
serverXml.stripComments();
serverXml.save(dest.join("conf/server.xml.template"));
file.deleteFile();
dest.join("conf/logging.properties").appendLines(
"",
"# appended by Stool: make sure we see application output in catalina.out",
"org.apache.catalina.core.ContainerBase.[Catalina].level = INFO",
"org.apache.catalina.core.ContainerBase.[Catalina].handlers = 1catalina.org.apache.juli.FileHandler"
);
Files.stoolTree(console.verbose, dest);
}
}
private Map variables(Stage stage, Ports ports) {
Map result;
result = new HashMap<>();
result.put("java.home", stage.config().javaHome);
result.put("wrapper.port", Integer.toString(ports.wrapper()));
result.put("wrapper.java.additional", wrapperJavaAdditional(ports, stage));
result.put("wrapper.timeouts", wrapperTimeouts());
return result;
}
private String wrapperTimeouts() {
StringBuilder result;
// because I know if a debugger is present, and I want special timeout settings
result = new StringBuilder("wrapper.java.detect_debug_jvm=FALSE\n");
if (debug) {
// long timeouts to give developers time for debugging;
// however: not infinite to avoid hanging stool validate runs.
result.append("wrapper.startup.timeout=3600\n");
result.append("wrapper.ping.timeout=3600\n");
result.append("wrapper.shutdown.timeout=3600\n");
result.append("wrapper.jvm_exit.timeout=3600\n");
} else {
// wait 5 minutes to make shutdown problem visible to users
result.append("wrapper.shutdown.timeout=300\n");
result.append("wrapper.jvm_exit.timeout=300\n");
// stick to defaults for other timeouts
}
return result.toString();
}
private String wrapperJavaAdditional(Ports ports, Stage stage) {
String tomcatOpts;
List opts;
StringBuilder result;
int i;
opts = new ArrayList<>();
// for tomcat
opts.add("-Djava.endorsed.dirs=%CATALINA_HOME%/endorsed");
opts.add("-Djava.io.tmpdir=%CATALINA_BASE%/temp");
opts.add("-Djava.util.logging.config.file=%CATALINA_BASE%/conf/logging.properties");
opts.add("-Djava.util.logging.manager=org.apache.juli.ClassLoaderLogManager");
opts.add("-Dcatalina.base=%CATALINA_BASE%");
opts.add("-Dcatalina.home=%CATALINA_HOME%");
// this is a marker to indicate they are launched by stool; and this is used by the dashboard to locate stool
opts.add("-Dstool.bin=" + session.bin.getAbsolute());
opts.add("-Dstool.backstage=" + stage.backstage.getAbsolute());
tomcatOpts = stage.macros().replace(stage.config().tomcatOpts);
opts.addAll(Separator.SPACE.split(tomcatOpts));
opts.add("-Xmx" + stage.config().tomcatHeap + "m");
opts.add("-XX:MaxPermSize=" + stage.config().tomcatPerm + "m");
// see http://docs.oracle.com/javase/7/docs/technotes/guides/management/agent.html
opts.add("-Dcom.sun.management.jmxremote.authenticate=false");
opts.add("-Dcom.sun.management.jmxremote.port=" + ports.jmx());
opts.add("-Dcom.sun.management.jmxremote.rmi.port=" + ports.jmx());
opts.add("-Dcom.sun.management.jmxremote.ssl=false");
if (debug || suspend) {
opts.add("-Xdebug");
opts.add("-Xnoagent");
opts.add("-Djava.compiler=NONE");
opts.add("-Xrunjdwp:transport=dt_socket,server=y,address=" + ports.debug() + ",suspend=" + (suspend ? "y" : "n"));
}
i = 1;
result = new StringBuilder();
for (String opt : opts) {
result.append("wrapper.java.additional.");
result.append(i);
result.append('=');
result.append(opt);
result.append('\n');
i++;
}
return result.toString();
}
private void checkUntil(Until until) {
if (until.isExpired()) {
throw new ArgumentException("Stage expired " + until + ". To start it, you have to adjust the 'until' date.");
}
}
@Override
protected void checkCommitted(Stage stage) throws IOException {
if (session.configuration.committed) {
try {
super.checkCommitted(stage);
} catch (IOException e) {
throw new IOException(
"It's not allowed to start stages with local modifications.\n"
+ "Please commit your modified files in order to start the stage.");
}
}
}
}