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

net.oneandone.stool.util.Session 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.util;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import net.oneandone.maven.embedded.Maven;
import net.oneandone.stool.EnumerationFailed;
import net.oneandone.stool.configuration.Bedroom;
import net.oneandone.stool.configuration.StageConfiguration;
import net.oneandone.stool.configuration.StoolConfiguration;
import net.oneandone.stool.configuration.Until;
import net.oneandone.stool.configuration.adapter.ExtensionsAdapter;
import net.oneandone.stool.configuration.adapter.FileNodeTypeAdapter;
import net.oneandone.stool.configuration.adapter.UntilTypeAdapter;
import net.oneandone.stool.extensions.ExtensionsFactory;
import net.oneandone.stool.locking.LockManager;
import net.oneandone.stool.setup.JavaSetup;
import net.oneandone.stool.stage.ArtifactStage;
import net.oneandone.stool.stage.Stage;
import net.oneandone.stool.users.User;
import net.oneandone.stool.users.UserNotFound;
import net.oneandone.stool.users.Users;
import net.oneandone.sushi.cli.ArgumentException;
import net.oneandone.sushi.cli.Console;
import net.oneandone.sushi.fs.ModeException;
import net.oneandone.sushi.fs.Node;
import net.oneandone.sushi.fs.ReadLinkException;
import net.oneandone.sushi.fs.World;
import net.oneandone.sushi.fs.file.FileNode;
import net.oneandone.sushi.launcher.Failure;
import net.oneandone.sushi.launcher.Launcher;
import net.oneandone.sushi.util.Strings;
import org.codehaus.plexus.DefaultPlexusContainer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.mail.MessagingException;
import javax.naming.NamingException;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.lang.management.ManagementFactory;
import java.lang.reflect.Modifier;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Map;

public class Session {
    public static Session load(Logging logging, String user, String command, Environment environment, Console console,
                               FileNode invocationFile, String svnuser, String svnpassword) throws IOException {
        Session session;

        session = loadWithoutBackstageWipe(logging, user, command, environment, console, invocationFile, svnuser, svnpassword);

        // Stale backstage wiping: how to detect backstages who's stage directory was removed.
        //
        // My first thought was to watch for filesystem events to trigger backstage wiping.
        // But there's quite a big delay and rmdif+mkdir is reported as modification. Plus the code is quite complex and
        // I don't know how to handle overflow events.
        // So I simple wipe them whenever I load stool lib. That's a well-defined timing and that's before stool might
        // use a stale stage.
        session.wipeStaleBackstages();
        return session;
    }

    public void wipeStaleBackstages() throws IOException {
        long s;
        String pid;
        Stage tmp;

        s = System.currentTimeMillis();
        for (FileNode backstage : backstages.list()) {
            if (backstage.isDirectory()) {
                FileNode anchor = backstage.join("anchor");
                if (!anchor.exists() || (!anchor.isDirectory() && anchor.isLink())) {  // anchor file has been removed or is a broken link
                    console.info.println("stale backstage detected: " + backstage);
                    for (Node pidfile : backstage.find("shared/run/tomcat.pid")) {
                        pid = pidfile.readString().trim();
                        console.verbose.println(pidfile.getName() + ": stopping pid " + pid);
                        try {
                            tmp = new ArtifactStage(this, "" /* no apps */, backstage, backstage /* used to determin owner */,
                                    loadStageConfiguration(backstage));
                            tmp.stop(console);
                        } catch (IOException e) {
                            console.error.println("cannot stop stale backstage: " + e.getMessage());
                            e.printStackTrace(console.verbose);
                            try {
                                pidfile.deleteFile(); // don't try again
                            } catch (IOException e2) {
                                e.addSuppressed(e2);
                            }
                            throw e;
                        }
                    }
                    try {
                        backstage.deleteTree();
                    } catch (IOException e) {
                        console.error.println(backstage + ": cannot delete stale backstage: " + e.getMessage());
                        e.printStackTrace(console.verbose);
                    }
                }
            }
        }
        console.verbose.println("wipeStaleBackstages done, ms=" + ((System.currentTimeMillis() - s)));
    }

    public static FileNode locateLib(FileNode bin) throws ReadLinkException {
        return (FileNode) bin.join("lib").resolveLink();
    }

    private static Session loadWithoutBackstageWipe(Logging logging, String user, String command, Environment environment, Console console,
                                                  FileNode invocationFile, String svnuser, String svnpassword) throws IOException {
        ExtensionsFactory factory;
        Gson gson;
        FileNode lib;
        FileNode bin;
        Session result;

        factory = ExtensionsFactory.create(console.world);
        gson = gson(console.world, factory);
        bin = environment.stoolBin(console.world);
        bin.checkDirectory();
        lib = locateLib(bin);
        result = new Session(factory, gson, logging, user, command, lib, bin, console, environment, StoolConfiguration.load(gson, lib),
                Bedroom.loadOrCreate(gson, lib), invocationFile, svnuser, svnpassword);
        result.selectedStageName = environment.getOpt(Environment.STOOL_SELECTED);
        return result;
    }

    private static final int MEM_RESERVED_OS = 500;

    //--

    public final ExtensionsFactory extensionsFactory;
    public final Gson gson;
    public final Logging logging;
    public final String user;
    private final String command;

    public final FileNode lib;
    public final FileNode bin;
    private String lazyGroup;

    public final Console console;
    public final Environment environment;
    public final StoolConfiguration configuration;
    public final Bedroom bedroom;

    public final FileNode backstages;


    /** may be null */
    private final FileNode invocationFile;
    private final Subversion subversion;

    private String selectedStageName;
    private final String stageIdPrefix;
    private int nextStageId;
    public final Users users;
    public final LockManager lockManager;

    private Pool lazyPool;

    private static final DateTimeFormatter FMT = DateTimeFormatter.ofPattern("yyMMdd");

    public Session(ExtensionsFactory extensionsFactory, Gson gson, Logging logging, String user, String command, FileNode lib, FileNode bin,
                   Console console, Environment environment, StoolConfiguration configuration,
                   Bedroom bedroom, FileNode invocationFile, String svnuser, String svnpassword) {
        this.extensionsFactory = extensionsFactory;
        this.gson = gson;
        this.logging = logging;
        this.user = user;
        this.command = command;
        this.lib = lib;
        this.bin = bin;
        this.lazyGroup = null;
        this.console = console;
        this.environment = environment;
        this.configuration = configuration;
        this.bedroom = bedroom;
        this.backstages = lib.join("backstages");
        this.selectedStageName = null;
        this.invocationFile = invocationFile;
        this.subversion = new Subversion(svnuser, svnpassword);
        this.stageIdPrefix = FMT.format(LocalDate.now()) + "." + logging.id + ".";
        this.nextStageId = 0;
        if (configuration.ldapUrl.isEmpty()) {
            this.users = Users.fromLogin();
        } else {
            this.users = Users.fromLdap(configuration.ldapUrl, configuration.ldapPrincipal, configuration.ldapCredentials);
        }
        this.lockManager = LockManager.create(lib.join("run/locks"), user + ":" + command, 10);
        this.lazyPool= null;
    }

    //--

    private static final Logger LOG = LoggerFactory.getLogger(Session.class);

    /** logs an error for administrators, i.e. the user is not expected to understand/fix this problem. */
    public void reportException(String context, Throwable e) {
        String subject;
        StringWriter body;
        PrintWriter writer;

        LOG.error("[" + command + "] " + context + ": " + e.getMessage(), e);
        if (!configuration.contactAdmin.isEmpty()) {
            subject = "[stool exception] " + e.getMessage();
            body = new StringWriter();
            body.write("stool: " + JavaSetup.versionString(console.world) + "\n");
            body.write("command: " + command + "\n");
            body.write("context: " + context + "\n");
            body.write("user: " + user + "\n");
            body.write("hostname: " + configuration.hostname + "\n");
            writer = new PrintWriter(body);
            while (true) {
                e.printStackTrace(writer);
                e = e.getCause();
                if (e == null) {
                    break;
                }
                body.append("Caused by:\n");
            }
            try {
                configuration.mailer().send(configuration.contactAdmin,
                        new String[]{configuration.contactAdmin}, subject, body.toString());
            } catch (MessagingException suppressed) {
                LOG.error("cannot send exception email: " + suppressed.getMessage(), suppressed);
            }
        }
    }

    //--

    public String group() throws ModeException {
        if (lazyGroup == null) {
            lazyGroup = lib.getGroup().toString();
        }
        return lazyGroup;
    }

    public FileNode bin(String name) {
        return bin.join(name);
    }

    //-- environment handling

    public static int memTotal() {
        long result;

        result = ((com.sun.management.OperatingSystemMXBean) ManagementFactory.getOperatingSystemMXBean()).getTotalPhysicalMemorySize();
        return (int) (result / 1024 / 1024);
    }

    //--

    public List list(EnumerationFailed problems, Predicate predicate) throws IOException {
        List result;
        Stage stage;

        result = new ArrayList<>();
        for (FileNode backstage : backstages.list()) {
            if (StageConfiguration.file(backstage).exists()) {
                try {
                    stage = Stage.load(this, backstage);
                } catch (IOException e) {
                    problems.add(backstage, e);
                    continue;
                }
                if (predicate.matches(stage)) {
                    result.add(stage);
                }
            } else {
                // stage is being created, we're usually waiting the the checkout to complete
            }
        }
        return result;
    }

    public List listWithoutSystem() throws IOException {
        List result;
        EnumerationFailed problems;

        problems = new EnumerationFailed();
        result = list(problems, new Predicate() {
            @Override
            public boolean matches(Stage stage) {
                return !stage.isSystem();
            }
        });
        for (Map.Entry entry : problems.problems.entrySet()) {
            reportException(entry.getKey() + ": Session.listWithoutDashboard", entry.getValue());
        }
        return result;
    }


    public void select(Stage selected) {
        if (selected == null) {
            throw new IllegalArgumentException();
        }
        selectedStageName = selected.getName();
        environment.setAll(environment(selected));
    }

    public void backupEnvironment() {
        String backupKey;
        String backupValue;

        for (String key : environment(null).keys()) {
            backupKey = Environment.backupKey(key);
            backupValue = environment.getOpt(backupKey);
            if (backupValue != null) {
                throw new ArgumentException("session already opened (environment variable already defined: " + backupKey + ")");
            }
            environment.set(backupKey, environment.getOpt(key));
        }
    }

    public void resetEnvironment() {
        Environment reset;
        String backupKey;
        String backupValue;

        reset = environment(null);
        for (String key : reset.keys()) {
            backupKey = Environment.backupKey(key);
            backupValue = environment.getOpt(backupKey);
            environment.set(key, backupValue);
            environment.set(backupKey, null);
        }
    }

    private FileNode cd;

    public void cd(FileNode dest) {
        cd = dest;
    }

    public void invocationFileUpdate() throws IOException {
        List lines;

        if (invocationFile != null) {
            lines = new ArrayList<>();
            for (String key : environment(null).keys()) {
                lines.add(environment.code(key));
                lines.add(environment.code(Environment.backupKey(key)));
            }
            if (cd != null) {
                lines.add("cd '" + cd.getAbsolute() + "'");
            }
            if (console.getVerbose()) {
                for (String line : lines) {
                    console.verbose.println("[env] " + line);
                }
            }
            invocationFile.writeLines(lines);
        }
    }

    public Subversion subversion() {
        return subversion;
    }
    public Stage load(String stageName) throws IOException {
        FileNode backstage;

        backstage = backstages.join(stageName);
        return Stage.load(this, backstage);
    }
    public List stageNames() throws IOException {
        List files;
        List result;

        files = backstages.list();
        result = new ArrayList<>(files.size());
        for (FileNode file : files) {
            result.add(file.getName());
        }
        return result;
    }

    //-- Memory checks - all value in MB
    public String getSelectedStageName() {
        return selectedStageName;
    }

    public Environment environment(Stage stage) {
        Environment env;
        String stoolIndicator;
        String mavenOpts;
        String prompt;

        if (stage == null) {
            mavenOpts = "";
        } else {
            mavenOpts = stage.macros().replace(stage.config().mavenOpts);
        }
        env = new Environment();
        env.set(Environment.STOOL_SELECTED, selectedStageName);
        // for pws and repositories:
        if (stage != null) {
            env.set(Environment.MACHINE, stage.getMachine());
        }
        // for pws:
        env.set(Environment.STAGE_HOST, stage != null ? stage.getName() + "." + configuration.hostname : null);
        // not that both MAVEN and ANT use JAVA_HOME to locate their JVM - it's not necessary to add java to the PATH variable
        env.set(Environment.JAVA_HOME, stage != null ? stage.config().javaHome : null);
        env.set(Environment.MAVEN_HOME, (stage != null && stage.config().mavenHome() != null) ? stage.config().mavenHome() : null);
        env.set(Environment.MAVEN_OPTS, mavenOpts);
        // to avoid ulimit permission denied warnings on Ubuntu machines:
        if (stage == null) {
            stoolIndicator = "";
        } else {
            stoolIndicator = "\\[$(stoolIndicatorColor)\\]" + stage.getName() + "\\[\\e[m\\]";
        }
        prompt = configuration.prompt;
        prompt = Strings.replace(prompt, "\\+", stoolIndicator);
        prompt = Strings.replace(prompt, "\\=", this.environment.getOpt(Environment.backupKey(Environment.PS1)));
        env.set(Environment.PS1, prompt);
        return env;
    }

    //--

    /** @return memory not yet reserved */
    public int memUnreserved() throws IOException {
        return memTotal() - MEM_RESERVED_OS - memReservedTomcats();
    }

    /** used for running tomcat */
    private int memReservedTomcats() throws IOException {
        int reserved;
        StageConfiguration stage;

        reserved = 0;
        for (FileNode backstage : getBackstages()) {
            if (Stage.shared(backstage).join("run/tomcat.pid").exists()) {
                stage = loadStageConfiguration(backstage);
                reserved += stage.tomcatHeap;
                reserved += stage.tomcatPerm;
            }
        }
        return reserved;
    }

    public void checkDiskFree() {
        int free;
        int min;

        free = diskFree();
        min = configuration.diskMin;
        if (free < min) {
            throw new ArgumentException("Disk almost full. Currently available " + free + " mb, required " + min + " mb.");
        }
    }

    /** @return Free disk space in partition used for stool lib. CAUTION: not necessarily the partition used for stages. */
    public int diskFree() {
        return (int) (lib.toPath().toFile().getUsableSpace() / 1024 / 1024);
    }

    public List getBackstages() throws IOException {
        List lst;

        lst = backstages.list();
        Collections.sort(lst, new Comparator() {
            @Override
            public int compare(Node left, Node right) {
                return left.getName().compareTo(right.getName());
            }
        });
        return lst;
    }

    public User lookupUser(String login) throws NamingException, UserNotFound {
        if (configuration.shared) {
            return users.byLogin(login);
        } else {
            return null;
        }
    }


    public void chown(Stage stage, String newOwner) throws Failure {
        chown(newOwner, stage.backstage, stage.getDirectory());
    }

    public void chown(String newOwner, FileNode ... dirs) throws Failure {
        Launcher launcher;

        launcher = new Launcher(lib, "sudo", bin("chowntree.sh").getAbsolute(), newOwner);
        for (FileNode dir : dirs) {
            launcher.arg(dir.getAbsolute());
        }
        launcher.exec(console.info);
    }

    public FileNode ports() {
        return lib.join("run/ports");
    }

    public boolean isSelected(Stage stage) {
        return stage.getName().equals(selectedStageName);
    }

    //-- stage properties


    public void saveStageProperties(StageConfiguration stageConfiguration, FileNode backstage) throws IOException {
        stageConfiguration.save(gson, StageConfiguration.file(backstage));
    }

    public StageConfiguration loadStageConfiguration(FileNode backstage) throws IOException {
        return StageConfiguration.load(gson, StageConfiguration.file(backstage));
    }

    //-- stool properties

    public List stageDirectories() throws IOException {
        List result;

        result = new ArrayList<>();
        for (FileNode backstage : getBackstages()) {
            result.add((FileNode) Stage.anchor(backstage).resolveLink());
        }
        return result;
    }

    public Pool pool() throws IOException {
        if (lazyPool == null) {
            lazyPool = Pool.loadOpt(ports(), configuration.portFirst, configuration.portLast, backstages);
        }
        return lazyPool;
    }

    public void updatePool() { // TODO: hack to see updates application urls
        lazyPool = null;
    }

    public StageConfiguration createStageConfiguration(String url) {
        String mavenHome;
        StageConfiguration stage;

        try {
            mavenHome = Maven.locateMaven(console.world).getAbsolute();
        } catch (IOException e) {
            mavenHome = "";
        }
        stage = new StageConfiguration(nextStageId(), javaHome(), mavenHome, extensionsFactory.newInstance());
        configuration.setDefaults(StageConfiguration.properties(extensionsFactory), stage, url);
        return stage;
    }

    private String nextStageId() {
        nextStageId++;
        return stageIdPrefix + nextStageId;
    }

    public static String javaHome() {
        String result;

        result = System.getProperty("java.home");
        if (result == null) {
            throw new IllegalStateException();
        }
        result = Strings.removeRightOpt(result, "/");
        // prefer jdk, otherwise Java tools like jar while report an error on Mac OS ("unable to locate executable at $JAVA_HOME")
        result = Strings.removeRightOpt(result, "/jre");
        return result;
    }

    private DefaultPlexusContainer lazyPlexus;

    public DefaultPlexusContainer plexus() {
        if (lazyPlexus == null) {
            lazyPlexus = Maven.container();
        }
        return lazyPlexus;
    }

    public static Gson gson(World world, ExtensionsFactory factory) {
        return new GsonBuilder()
                .registerTypeAdapter(FileNode.class, new FileNodeTypeAdapter(world))
                .registerTypeAdapter(Until.class, new UntilTypeAdapter())
                .registerTypeAdapterFactory(ExtensionsAdapter.factory(factory))
                .disableHtmlEscaping()
                .serializeNulls()
                .excludeFieldsWithModifiers(Modifier.STATIC, Modifier.TRANSIENT)
                .setPrettyPrinting()
                .create();
    }

    public FileNode downloadCache() {
        return configuration.downloadCache;
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy