net.oneandone.stool.util.Session 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.util;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import net.oneandone.inline.ArgumentException;
import net.oneandone.inline.Console;
import net.oneandone.maven.embedded.Maven;
import net.oneandone.setenv.Setenv;
import net.oneandone.stool.cli.EnumerationFailed;
import net.oneandone.stool.cli.Main;
import net.oneandone.stool.configuration.Bedroom;
import net.oneandone.stool.configuration.Expire;
import net.oneandone.stool.configuration.Option;
import net.oneandone.stool.configuration.Property;
import net.oneandone.stool.configuration.StageConfiguration;
import net.oneandone.stool.configuration.StoolConfiguration;
import net.oneandone.stool.configuration.adapter.ExpireTypeAdapter;
import net.oneandone.stool.configuration.adapter.ExtensionsAdapter;
import net.oneandone.stool.configuration.adapter.FileNodeTypeAdapter;
import net.oneandone.stool.extensions.ExtensionsFactory;
import net.oneandone.stool.locking.LockManager;
import net.oneandone.stool.scm.Scm;
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.fs.LinkException;
import net.oneandone.sushi.fs.ModeException;
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.Separator;
import net.oneandone.sushi.util.Strings;
import org.codehaus.plexus.DefaultPlexusContainer;
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.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/** Home with some session information. */
public class Session {
public static Session load(boolean setenv, FileNode home, Logging logging, String command, Console console, World world,
String svnuser, String svnpassword) throws IOException {
Session session;
session = loadWithoutBackstageWipe(setenv, home, logging, command, console, world, svnuser, svnpassword);
// Stale backstage wiping: how to detect backstage directories 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 rmdir+mkdir is reported as modification. Plus the code is quite complex and
// I don't know how to handle overflow events.
// So I simply wipe them whenever I load stool a session. 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;
s = System.currentTimeMillis();
for (FileNode backstage : backstages.list()) {
if (backstage.exists() && !backstage.isFile() && !backstage.isDirectory()) {
console.info.println("removing stale backstage: " + backstage);
backstage.deleteTree();
}
}
console.verbose.println("wipeStaleBackstages done, ms=" + ((System.currentTimeMillis() - s)));
}
private static Session loadWithoutBackstageWipe(boolean setenv, FileNode home, Logging logging, String command, Console console,
World world, String svnuser, String svnpassword) throws IOException {
ExtensionsFactory factory;
Gson gson;
Session result;
Environment environment;
factory = ExtensionsFactory.create(world);
gson = gson(world, factory);
environment = Environment.loadSystem();
result = new Session(setenv, factory, gson, logging, command, home, console, world, environment,
StoolConfiguration.load(gson, home), Bedroom.loadOrCreate(gson, home), svnuser, svnpassword);
return result;
}
private static final int MEM_RESERVED_OS = 500;
//--
private final boolean setenv;
public final ExtensionsFactory extensionsFactory;
public final Gson gson;
public final Logging logging;
public final String user;
private final String command;
public final FileNode home;
private String lazyGroup;
public final Console console;
public final World world;
public final Environment environment;
public final StoolConfiguration configuration;
public final Bedroom bedroom;
private final FileNode backstages;
private final Credentials svnCredentials;
private final String stageIdPrefix;
private int nextStageId;
public final Users users;
public final LockManager lockManager;
private Pool lazyPool;
public Session(boolean setenv, ExtensionsFactory extensionsFactory, Gson gson, Logging logging, String command,
FileNode home, Console console, World world, Environment environment, StoolConfiguration configuration,
Bedroom bedroom, String svnuser, String svnpassword) {
this.setenv = setenv;
this.extensionsFactory = extensionsFactory;
this.gson = gson;
this.logging = logging;
this.user = logging.getUser();
this.command = command;
this.home = home;
this.lazyGroup = null;
this.console = console;
this.world = world;
this.environment = environment;
this.configuration = configuration;
this.bedroom = bedroom;
this.backstages = home.join("backstages");
this.svnCredentials = new Credentials(svnuser, svnpassword);
this.stageIdPrefix = 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(home.join("run/locks"), user + ":" + command.replace("\n", "\\n"), 10);
this.lazyPool= null;
}
public Map properties() {
Map result;
Option option;
result = new LinkedHashMap<>();
for (java.lang.reflect.Field field : StageConfiguration.class.getFields()) {
option = field.getAnnotation(Option.class);
if (option != null) {
result.put(option.key(), new Property(option.key(), field, null));
}
}
extensionsFactory.fields(result);
return result;
}
public void add(FileNode backstage, String id) throws LinkException {
backstage.link(backstages.join(id));
}
public FileNode backstageLink(String id) throws ReadLinkException {
return backstages.join(id);
}
public FileNode findStageDirectory(FileNode dir) {
do {
if (Stage.backstageDirectory(dir).exists()) {
return dir;
}
dir = dir.getParent();
} while (dir != null);
return null;
}
//--
/** 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;
logging.error("[" + command + "] " + context + ": " + e.getMessage(), e);
if (!configuration.admin.isEmpty()) {
subject = "[stool exception] " + e.getMessage();
body = new StringWriter();
body.write("stool: " + Main.versionString(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.admin, new String[]{configuration.admin}, subject, body.toString());
} catch (MessagingException suppressed) {
logging.error("cannot send exception email: " + suppressed.getMessage(), suppressed);
}
}
}
//--
public String group() throws ModeException {
if (lazyGroup == null) {
lazyGroup = backstages.getGroup().toString();
}
return lazyGroup;
}
public FileNode bin(String name) {
return home.join("bin", 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;
FileNode backstage;
result = new ArrayList<>();
for (FileNode link : backstages.list()) {
backstage = link.resolveLink();
if (StageConfiguration.file(backstage).exists()) {
try {
stage = Stage.load(this, link);
} catch (IOException e) {
problems.add(link.getName(), 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 Scm scm(String url) throws IOException {
return Scm.forUrl(url, svnCredentials);
}
public Scm scmOpt(String url) {
return Scm.forUrlOpt(url, svnCredentials);
}
public Credentials svnCredentials() {
return svnCredentials;
}
public Stage load(String id) throws IOException {
return Stage.load(this, backstages.join(id));
}
public Stage loadByName(String stageName) throws IOException {
List links;
FileNode bs;
StageConfiguration config;
links = backstages.list();
for (FileNode link : links) {
bs = link.resolveLink();
config = StageConfiguration.load(gson, StageConfiguration.file(bs));
if (stageName.equals(config.name)) {
return load(link.getName());
}
}
throw new IllegalArgumentException("stage not found: " + stageName);
}
public List stageNames() throws IOException {
List links;
FileNode bs;
StageConfiguration config;
List result;
links = backstages.list();
result = new ArrayList<>(links.size());
for (FileNode link : links) {
bs = link.resolveLink();
config = StageConfiguration.load(gson, StageConfiguration.file(bs));
result.add(config.name);
}
return result;
}
/** return directory or null */
public FileNode lookup(String stageName) throws IOException {
List links;
FileNode bs;
StageConfiguration config;
links = backstages.list();
for (FileNode link : links) {
bs = link.resolveLink();
config = StageConfiguration.load(gson, StageConfiguration.file(bs));
if (stageName.equals(config.name)) {
return bs.getParent();
}
}
return null;
}
private static final String UNKNOWN = "../unknown/..";
private String lazySelectedId = UNKNOWN;
public String getSelectedStageId() throws IOException {
FileNode directory;
FileNode bs;
if (lazySelectedId == UNKNOWN) {
directory = findStageDirectory(world.getWorking());
if (directory == null) {
lazySelectedId = null;
} else {
bs = Stage.backstageDirectory(directory);
for (FileNode link : backstages.list()) {
if (link.resolveLink().equals(bs)) {
lazySelectedId = link.getName();
break;
}
}
}
}
return lazySelectedId;
}
public Environment environment(Stage stage) {
Environment env;
String mavenOpts;
if (stage == null) {
mavenOpts = "";
} else {
mavenOpts = stage.macros().replace(stage.config().mavenOpts);
}
env = new Environment();
// note 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);
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;
FileNode backstage;
reserved = 0;
for (FileNode link : backstages.list()) {
backstage = link.resolveLink();
if (backstage.join("run/tomcat.pid").exists()) {
stage = loadStageConfiguration(backstage);
reserved += stage.tomcatHeap;
}
}
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. TODO: not necessarily the partition used for stages. */
public int diskFree() {
return (int) (home.toPath().toFile().getUsableSpace() / 1024 / 1024);
}
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.getDirectory());
}
public void chown(String newOwner, FileNode ... dirs) throws Failure {
Launcher launcher;
launcher = new Launcher(home, "sudo", bin("chowntree.sh").getAbsolute(), newOwner);
for (FileNode dir : dirs) {
launcher.arg(dir.getAbsolute());
}
launcher.exec(console.info);
}
public FileNode ports() {
return home.join("run/ports");
}
public boolean isSelected(Stage stage) throws IOException {
return stage.getId().equals(getSelectedStageId());
}
//-- 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 link : backstages.list()) {
result.add(link.resolveLink().getParent());
}
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 result;
Scm scm;
String refresh;
try {
mavenHome = Maven.locateMaven(world).getAbsolute();
} catch (IOException e) {
mavenHome = "";
}
scm = scmOpt(url);
refresh = scm == null ? "" : scm.refresh();
result = new StageConfiguration(javaHome(), mavenHome, refresh, extensionsFactory.newInstance());
result.notify.add(user);
configuration.setDefaults(properties(), result, url);
return result;
}
public 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(Expire.class, new ExpireTypeAdapter())
.registerTypeAdapterFactory(ExtensionsAdapter.factory(factory))
.disableHtmlEscaping()
.serializeNulls()
.excludeFieldsWithModifiers(Modifier.STATIC, Modifier.TRANSIENT)
.setPrettyPrinting()
.create();
}
public FileNode downloadCache() {
return configuration.downloadCache;
}
public List search(String search) throws IOException {
FileNode working;
List cmd;
List result;
int idx;
working = world.getWorking();
result = new ArrayList<>();
if (configuration.search.isEmpty()) {
throw new IOException("no search tool configured");
}
cmd = Separator.SPACE.split(configuration.search);
idx = cmd.indexOf("()");
if (idx == -1) {
throw new IOException("search tool configured without () placeholder");
}
cmd.set(idx, search);
for (String line : Separator.RAW_LINE.split(working.exec(Strings.toArray(cmd)))) {
line = line.trim();
result.add(line);
}
return result;
}
public void cd(FileNode dir) {
if (setenv) {
Setenv.get().cd(dir.getAbsolute());
}
}
public void checkVersion() throws IOException {
String homeVersion;
String binVersion;
homeVersion = home.join("version").readString().trim();
binVersion = Main.versionString(world);
if (!homeVersion.equals(binVersion)) {
throw new IOException("Cannot use home directory version " + homeVersion + " with Stool " + binVersion
+ "\nTry 'stool setup'");
}
}
public static final String majorMinor(String version) {
int major;
int minor;
major = version.indexOf('.');
minor = version.indexOf('.', major + 1);
if (minor == -1) {
throw new IllegalArgumentException(version);
}
return version.substring(0, minor);
}
public Property property(String name) {
return properties().get(name);
}
public Info[] fieldsAndName() {
Field[] fields;
Info[] result;
fields = Field.values();
result = new Info[fields.length + 1];
System.arraycopy(fields, 0, result, 1, fields.length);
result[0] = property("name");
return result;
}
}