All Downloads are FREE. Search and download functionalities are using the official Maven repository.

net.oneandone.stool.cli.Start Maven / Gradle / Ivy

There is a newer version: 4.0.3
Show newest version
/*
 * 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.cli;

import net.oneandone.inline.ArgumentException;
import net.oneandone.inline.Console;
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.stool.util.Vhost;
import net.oneandone.sushi.fs.GetLastModifiedException;
import net.oneandone.sushi.fs.Node;
import net.oneandone.sushi.fs.ReadLinkException;
import net.oneandone.sushi.fs.file.FileNode;
import net.oneandone.sushi.launcher.Launcher;
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.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 {
    private boolean fitnesse;
    private boolean debug;
    private boolean suspend;
    private boolean tail;

    private Launcher.Handle mainResult;

    public Start(Session session, boolean fitnesse, boolean debug, boolean suspend) {
        super(false, session, Mode.EXCLUSIVE, Mode.EXCLUSIVE, Mode.SHARED);
        this.fitnesse = fitnesse;
        this.debug = debug;
        this.suspend = suspend;
        this.tail = false;
    }

    public void setTail(boolean tail) {
        this.tail = tail;
    }

    public static String tomcatName(String version) {
        return "apache-tomcat-" + version;
    }

    @Override
    public boolean doBefore(List stages, int indent) throws IOException {
        int global;
        int reserved;

        global = session.configuration.quota;
        if (global != 0) {
            reserved = session.quotaReserved();
            if (reserved > global) {
                throw new IOException("Sum of all stage quotas exceeds global limit: " + reserved + " mb > " + global + " mb.\n"
                  + "Use 'stool list name disk quota' to see actual disk usage vs configured quota.");
            }
        }
        return super.doBefore(stages, indent);
    }

    @Override
    public void doMain(Stage stage) throws Exception {
        stage.modify();
        // to avoid running into a ping timeout below:
        stage.session.configuration.verfiyHostname();
        stage.checkConstraints();
        if (session.configuration.committed) {
            if (!stage.isCommitted()) {
                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.");
            }
        }
        checkNotStarted(stage);

        if (fitnesse) {
            doFitnesse(stage);
        } else {
            doNormal(stage);
        }
        if (session.bedroom.contains(stage.getId())) {
            console.info.println("leaving sleeping state");
            session.bedroom.remove(session.gson, stage.getId());
        }
    }

    @Override
    public void doFinish(Stage stage) throws Exception {
        int pid;

        if (fitnesse) {
            // nothing to finish
            return;
        }
        console.verbose.println(mainResult.awaitString());
        pid = stage.runningService();
        if (pid == 0) {
            throw new IOException("tomcat startup failed - no pid file found");
        }
        ping(stage);
        console.info.println("Applications available:");
        for (String app : stage.namedUrls()) {
            console.info.println("  " + app);
        }
        if (tail) {
            doTail(stage);
        }
    }

    //--

    public void doNormal(Stage stage) throws Exception {
        FileNode download;
        Ports ports;

        serviceWrapperOpt(stage);
        download = tomcatOpt(stage.config().tomcatVersion);
        ports = session.pool().allocate(stage, Collections.emptyMap());
        copyTemplate(stage, ports);
        createServiceLauncher(stage);
        copyCatalinaBaseOpt(download, stage.getBackstage(), stage.config().tomcatVersion);
        if (debug || suspend) {
            console.info.println("debugging enabled on port " + ports.debug());
        }
        mainResult = stage.start(console, ports);
    }

    private void doTail(Stage stage) throws IOException {
        List logs;
        int c;
        Node log;

        logs = stage.getBackstage().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.newInputStream()) {
            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;
        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 (!Stage.ping(uri)) {
                    console.verbose.println("port not ready yet");
                    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 backstage;

        backstage = stage.getBackstage();
        Files.template(world.resource("templates/stage"), backstage, 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" }) {
            backstage.join(dir).mkdirOpt();
        }
    }

    private void createServiceLauncher(Stage stage) throws IOException {
        FileNode base;
        String content;
        FileNode wrapper;

        base = stage.serviceWrapperBase();
        content = base.join("src/bin/sh.script.in").readString();
        content = Strings.replace(content, "@app.name@", "tomcat");
        content = Strings.replace(content, "@app.long.name@", "Stage " + stage.getName() + " Tomcat");
        content = Strings.replace(content, "@app.description@", "Tomcat for stage " + stage.getName() + " managed by Stool.");
        content = uncomment(content, "PASS_THROUGH=true");
        content = comment(content, "WRAPPER_CMD=\"./wrapper\"");
        content = comment(content, "WRAPPER_CONF=\"../conf/wrapper.conf\"");
        content = comment(content, "PIDDIR=\".\"");
        wrapper = stage.getBackstage().join("service/service-wrapper.sh");
        wrapper.writeString(content);
        Files.executable(wrapper);
    }

    private String comment(String str, String line) {
        return replace1(str, line, "# " + line);
    }

    private String uncomment(String str, String line) {
        return replace1(str, "#" + line, line);
    }

    private String replace1(String str, String in, String out) {
        if (Strings.count(str, in) != 1) {
            throw new IllegalStateException(str);
        }
        return Strings.replace(str, in, out);
    }

    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.home.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(Stage stage) throws IOException {
        FileNode download;
        FileNode base;

        base = stage.serviceWrapperBase();
        download = session.downloadCache().join(base.getName() + ".tar.gz");
        if (!download.exists()) {
            downloadFile(subst(session.configuration.downloadServiceWrapper, stage.config().tomcatService), download);
            download.checkFile();
        }
        if (!base.exists()) {
            tar(base.getParent(), "zxf", download.getAbsolute());
            base.checkDirectory();
        }
    }

    private void downloadFile(String url, FileNode dest) throws IOException {
        console.info.println("downloading " + url + " ...");
        try {
            dest.getWorld().validNode(url).copyFile(dest);
        } catch (IOException e) {
            dest.deleteFileOpt();
            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);
        }
    }

    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 copyCatalinaBaseOpt(FileNode download, FileNode backstage, String version) throws IOException, SAXException {
        String name;
        FileNode src;
        FileNode dest;
        ServerXml serverXml;
        FileNode file;

        name = tomcatName(version);
        dest = backstage.join("tomcat");
        if (!dest.exists()) {
            tar(backstage, "zxf",
                    download.getAbsolute(), "--exclude", name + "/lib", "--exclude", name + "/bin", "--exclude", name + "/webapps");
            src = backstage.join(name);
            src.move(dest);
            // TODO: work-around for a problem I have with tar: it applies the umask to the permissions stored in the file ...
            dest.execNoOutput("chmod", "-R", "g+rw", ".");
            dest.execNoOutput("chmod", "g+x", "conf"); // for Tomcat 8.5

            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"
            );
        }
    }

    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());
        result.put("wrapper.debug", Boolean.toString(console.getVerbose()));
        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 4 minutes to make shutdown problem visible to users
            // CAUTION: hat to be shorter than the systemctl shutdown timeout to avoid kill -9 and the resulting stage pid files.
            // (see systemctl show stool.service -p TimeoutStopUSec)
            result.append("wrapper.shutdown.timeout=240\n");
            result.append("wrapper.jvm_exit.timeout=240\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.cp=" + Main.stoolCp(session.world).getAbsolute());
        opts.add("-Dstool.home=" + session.home.getAbsolute());
        try {
            opts.add("-Dstool.idlink=" + session.backstageLink(stage.getId()).getAbsolute());
        } catch (ReadLinkException e) {
            throw new IllegalStateException(e);
        }

        tomcatOpts = stage.macros().replace(stage.config().tomcatOpts);
        opts.addAll(Separator.SPACE.split(tomcatOpts));

        opts.add("-Xmx" + stage.config().tomcatHeap + "m");

        for (Map.Entry entry : stage.extensions().tomcatOpts(stage).entrySet()) {
            opts.add("-D" + entry.getKey() + "=" + entry.getValue());
        }

        // 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();
    }

    //-- fitnesse

    /**
     * Launches Fitnesse Wiki (http://www.fitnesse.org).
     *
     * Fitnesse wiki does not implement the servlet interfaces, so I cannot use the normal startup code for tomcats.
     * Instead, I invoke fitnesse-launchner-maven-plugin (https://code.google.com/archive/p/fitnesse-launcher-maven-plugin/)
     * to launch the embedded web server.
     */
    public void doFitnesse(Stage stage) throws Exception {
        Console console;
        Ports ports;
        Vhost host;
        int port;
        String url;
        FileNode log;
        Launcher launcher;

        if (tail) {
            throw new ArgumentException("-fitness -tail is not supported");
        }
        if (debug) {
            throw new ArgumentException("-fitness -debug is not supported");
        }
        if (suspend) {
            throw new ArgumentException("-fitness -suspend is not supported");
        }
        console = stage.session.console;
        ports = session.pool().allocate(stage, Collections.emptyMap());
        for (String vhost : stage.vhostNames()) {
            host = ports.lookup(vhost);
            port = host.httpPort();
            url = findUrl(stage, host);
            launcher = stage.launcher("mvn",
                    "uk.co.javahelp.fitnesse:fitnesse-launcher-maven-plugin:wiki", "-Dfitnesse.port=" + port);
            launcher.dir(stage.session.world.file(findProjectDir(ports, host)));

            log = stage.getBackstage().join("tomcat/logs/fitness-" + port + ".log");
            log.getParent().mkdirsOpt();
            if (!log.exists()) {
                log.mkfile();
            }
            // no exec -- keeps running until stopped; no way to detect failures
            // no log.close!
            launcher.launch(log.newWriter());
            console.info.println(vhost + " fitnesse started: " + url);
        }

    }

    private String findProjectDir(Ports ports, Vhost fitnesseHost) {
        String path;

        path = ports.lookup(fitnesseHost.name).docBase();
        return path.substring(0, path.indexOf("/target"));
    }

    private String findUrl(Stage stage, Vhost host) {
        return host.httpUrl(stage.session.configuration.vhosts, stage.session.configuration.hostname);
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy