diff --git a/README.md b/README.md
index d9b867323..2cda8d425 100644
--- a/README.md
+++ b/README.md
@@ -53,6 +53,8 @@ User documentation:
* [Remoting 3 Compatibility Notes](docs/remoting-3-compatibility.md)
* [Remoting Protocols](docs/protocols.md) - Overview of protocols integrated with Jenkins
* [Remoting Configuration](docs/configuration.md) - Configuring remoting agents
+* [Logging](docs/logging.md) - Logging
+* [Work Directory](docs/workDir.md) - Remoting work directory (new in Remoting `TODO`)
* [Jenkins Specifics](docs/jenkins-specifics.md) - Notes on using remoting in Jenkins
* [Troubleshooting](docs/troubleshooting.md) - Investigating and solving common remoting issues
diff --git a/docs/logging.md b/docs/logging.md
new file mode 100644
index 000000000..8155ed489
--- /dev/null
+++ b/docs/logging.md
@@ -0,0 +1,46 @@
+Logging
+===
+
+In Remoting logging is powered by the standard `java.util.logging` engine.
+The default behavior depends on the [Work Directory](workDir.md) mode.
+
+### Configuration
+
+In order to configure logging it is possible to use an external property file, path to which can be defined using the `-loggingConfig` CLI option or the `java.util.logging.config.file` system property.
+
+If logging is configured via `-loggingConfig`, some messages printed before the logging system initialization may be missing in startup logs configured by this option.
+
+See details about the property file format
+in [Oracle documentation](https://docs.oracle.com/cd/E19717-01/819-7753/6n9m71435/index.html)
+and [this guide](http://tutorials.jenkov.com/java-logging/configuration.html).
+Note that `ConsoleHandler` won't be enabled by default if this option is specified.
+
+### Default behavior with work directory
+
+With work directory Remoting automatically writes logs to the disk.
+This is a main difference from the legacy mode without workDir.
+
+Logging destinations:
+
+* STDOUT and STDERR
+ * Logs include `java.util.logging` and messages printed to _STDOUT/STDERR_ directly.
+* Files - `${workDir}/${internalDir}/logs` directory
+ * File base name - `remoting.log`
+ * Logs are being automatically rotated.
+ By default, Remoting keeps 5 10MB files
+ * Default logging level - `INFO`
+ * If the legacy `-agentLog` or `-slaveLog` option is enabled, this file logging will be disabled.
+
+If `-agentLog` or `-slaveLog` are not specified, `${workDir}/${internalDir}/logs` directory will be created during the work directory initialization (if required).
+
+
+
+### Default behavior without work directory (legacy mode)
+
+* By default, all logs within the system are being sent to _STDOUT/STDERR_ using `java.util.logging`.
+* If `-agentLog` or `-slaveLog` option is specified, the log will be also forwarded to the specified file
+ * The existing file will be overridden on startup
+ * Remoting does not perform automatic log rotation of this log file
+
+Particular Jenkins components use external features to provide better logging in the legacy mode.
+E.g. Windows agent services generate logs using features provided by [Windows Service Wrapper (WinSW)](https://github.com/kohsuke/winsw/).
\ No newline at end of file
diff --git a/docs/workDir.md b/docs/workDir.md
new file mode 100644
index 000000000..fa086e54b
--- /dev/null
+++ b/docs/workDir.md
@@ -0,0 +1,45 @@
+Remoting Work directory
+===
+
+In Remoting work directory is a storage
+
+Remoting work directory is available starting from Remoting `TODO`.
+Before this version there was no working directory concept in the library itself;
+all operations were managed by library users (e.g. Jenkins agent workspaces).
+
+### Before Remoting TODO
+
+* There is no work directory management in Remoting itself
+* Logs are not being persisted to the disk unless `-slaveLog` option is specified
+* JAR Cache is being stored in `${user.home}/.jenkins` unless `-jarCache` option is specified
+
+### After Remoting TODO
+
+Due to compatibility reasons, Remoting retains the legacy behavior by default.
+Work directory can be enabled using the `-workDir` option in CLI or via the `TODO` [system property](configuration.md).
+
+Once the option is enabled, Remoting starts using the following structure:
+
+```
+${WORKDIR}
+ |_ ${INTERNAL_DIR} - defined by '-internalDir', 'remoting' by default
+ |_ jarCache - JAR Cache
+ |_ logs - Remoting logs
+ |_ ... - Other directories contributed by library users
+```
+
+Structure of the `logs` directory depends on the logging settings.
+See [this page](logging.md) for more information.
+
+### Migrating to work directories in Jenkins
+
+:exclamation: Remoting does not perform migration from the previous structure,
+because it cannot identify potential external users of the data.
+
+Once the `-workDir` flag is enabled in Remoting, admins are expected to do the following:
+
+1. Remove the `${user.home}/.jenkins` directory if there is no other Remoting instances running under the same user.
+2. Consider upgrading configurations of agents in order to enable Work Directories
+ * SSH agents can be configured in agent settings.
+ * JNLP agents should be started with the `-workDir` parameter.
+ * See [JENKINS-TODO](TODO) for more information about changes in Jenkins plugins, which enable work directories by default.
\ No newline at end of file
diff --git a/src/main/java/hudson/remoting/Engine.java b/src/main/java/hudson/remoting/Engine.java
index 15484e12d..65734d7e0 100644
--- a/src/main/java/hudson/remoting/Engine.java
+++ b/src/main/java/hudson/remoting/Engine.java
@@ -29,9 +29,12 @@
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
import java.io.IOException;
+import java.io.PrintStream;
import java.net.Socket;
import java.net.URL;
+import java.nio.file.Path;
import java.security.AccessController;
import java.security.KeyManagementException;
import java.security.KeyStore;
@@ -68,6 +71,7 @@
import org.jenkinsci.remoting.engine.JnlpConnectionStateListener;
import org.jenkinsci.remoting.engine.JnlpProtocolHandler;
import org.jenkinsci.remoting.engine.JnlpProtocolHandlerFactory;
+import org.jenkinsci.remoting.engine.WorkDirManager;
import org.jenkinsci.remoting.protocol.IOHub;
import org.jenkinsci.remoting.protocol.cert.BlindTrustX509ExtendedTrustManager;
import org.jenkinsci.remoting.protocol.cert.DelegatingX509ExtendedTrustManager;
@@ -150,7 +154,62 @@ public void run() {
*/
private boolean keepAlive = true;
- private JarCache jarCache = new FileSystemJarCache(new File(System.getProperty("user.home"),".jenkins/cache/jars"),true);
+
+ /**
+ * Default JAR cache location for disabled workspace Manager.
+ */
+ private static final File DEFAULT_NOWS_JAR_CACHE_LOCATION =
+ new File(System.getProperty("user.home"),".jenkins/cache/jars");
+
+ @CheckForNull
+ private JarCache jarCache = null;
+
+ /**
+ * Specifies a destination for the agent log.
+ * If specified, this option overrides the default destination within {@link #workDir}.
+ * If both this options and {@link #workDir} is not set, the log will not be generated.
+ * @since TODO
+ */
+ @CheckForNull
+ private Path agentLog;
+
+ /**
+ * Specified location of the property file with JUL settings.
+ * @since TODO
+ */
+ @CheckForNull
+ private Path loggingConfigFilePath = null;
+
+ /**
+ * Specifies a default working directory of the remoting instance.
+ * If specified, this directory will be used to store logs, JAR cache, etc.
+ *
+ * In order to retain compatibility, the option is disabled by default.
+ *
+ * Jenkins specifics: This working directory is expected to be equal to the agent root specified in Jenkins configuration.
+ * @since TODO
+ */
+ @CheckForNull
+ public Path workDir = null;
+
+ /**
+ * Specifies a directory within {@link #workDir}, which stores all the remoting-internal files.
+ *
+ * This option is not expected to be used frequently, but it allows remoting users to specify a custom
+ * storage directory if the default {@code remoting} directory is consumed by other stuff.
+ * @since TODO
+ */
+ @Nonnull
+ public String internalDir = WorkDirManager.DirType.INTERNAL_DIR.getDefaultLocation();
+
+ /**
+ * Fail the initialization if the workDir or internalDir are missing.
+ * This option presumes that the workspace structure gets initialized previously in order to ensure that we do not start up with a borked instance
+ * (e.g. if a filesystem mount gets disconnected).
+ * @since TODO
+ */
+ @Nonnull
+ public boolean failIfWorkDirIsMissing = WorkDirManager.DEFAULT_FAIL_IF_WORKDIR_IS_MISSING;
private DelegatingX509ExtendedTrustManager agentTrustManager = new DelegatingX509ExtendedTrustManager(new BlindTrustX509ExtendedTrustManager());
@@ -165,12 +224,68 @@ public Engine(EngineListener listener, List hudsonUrls, String secretKey, S
}
/**
- * Configures JAR caching for better performance.
+ * Starts the engine.
+ * The procedure initializes the working directory and all the required environment
+ * @throws IOException Initialization error
+ * @since TODO
+ */
+ public synchronized void startEngine() throws IOException {
+
+ @CheckForNull File jarCacheDirectory = null;
+
+ // Prepare the working directory if required
+ if (workDir != null) {
+ final WorkDirManager workDirManager = WorkDirManager.getInstance();
+ if (jarCache != null) {
+ // Somebody has already specificed Jar Cache, hence we do not need it in the workspace.
+ workDirManager.disable(WorkDirManager.DirType.JAR_CACHE_DIR);
+ }
+
+ if (loggingConfigFilePath != null) {
+ workDirManager.setLoggingConfig(loggingConfigFilePath.toFile());
+ }
+
+ final Path path = workDirManager.initializeWorkDir(workDir.toFile(), internalDir, failIfWorkDirIsMissing);
+ jarCacheDirectory = workDirManager.getLocation(WorkDirManager.DirType.JAR_CACHE_DIR);
+ workDirManager.setupLogging(path, agentLog);
+ } else if (jarCache != null) {
+ LOGGER.log(Level.WARNING, "No Working Directory. Using the legacy JAR Cache location: {0}", DEFAULT_NOWS_JAR_CACHE_LOCATION);
+ jarCacheDirectory = DEFAULT_NOWS_JAR_CACHE_LOCATION;
+ }
+
+ if (jarCache == null){
+ if (jarCacheDirectory == null) {
+ // Should never happen in the current code
+ throw new IOException("Cannot find the JAR Cache location");
+ }
+ LOGGER.log(Level.FINE, "Using standard File System JAR Cache. Root Directory is {0}", jarCacheDirectory);
+ jarCache = new FileSystemJarCache(jarCacheDirectory, true);
+ } else {
+ LOGGER.log(Level.INFO, "Using custom JAR Cache: {0}", jarCache);
+ }
+
+ // Start the engine thread
+ this.start();
+ }
+
+ /**
+ * Configures custom JAR Cache location.
+ * Starting from TODO, this option disables JAR Caching in the working directory.
+ * @param jarCache JAR Cache to be used
* @since 2.24
*/
- public void setJarCache(JarCache jarCache) {
+ public void setJarCache(@Nonnull JarCache jarCache) {
this.jarCache = jarCache;
}
+
+ /**
+ * Sets path to the property file with JUL settings.
+ * @param filePath JAR Cache to be used
+ * @since TODO
+ */
+ public void setLoggingConfigFile(@Nonnull Path filePath) {
+ this.loggingConfigFilePath = filePath;
+ }
/**
* Provides Jenkins URL if available.
@@ -198,6 +313,44 @@ public void setNoReconnect(boolean noReconnect) {
this.noReconnect = noReconnect;
}
+ /**
+ * Sets the destination for agent logs.
+ * @param agentLog Path to the agent log.
+ * If {@code null}, the engine will pick the default behavior depending on the {@link #workDir} value
+ * @since TODO
+ */
+ public void setAgentLog(@CheckForNull Path agentLog) {
+ this.agentLog = agentLog;
+ }
+
+ /**
+ * Specified a path to the work directory.
+ * @param workDir Path to the working directory of the remoting instance.
+ * {@code null} Disables the working directory.
+ * @since TODO
+ */
+ public void setWorkDir(@CheckForNull Path workDir) {
+ this.workDir = workDir;
+ }
+
+ /**
+ * Specifies name of the internal data directory within {@link #workDir}.
+ * @param internalDir Directory name
+ * @since TODO
+ */
+ public void setInternalDir(@Nonnull String internalDir) {
+ this.internalDir = internalDir;
+ }
+
+ /**
+ * Sets up behavior if the workDir or internalDir are missing during the startup.
+ * This option presumes that the workspace structure gets initialized previously in order to ensure that we do not start up with a borked instance
+ * (e.g. if a filesystem mount gets disconnected).
+ * @param failIfWorkDirIsMissing Flag
+ * @since TODO
+ */
+ public void setFailIfWorkDirIsMissing(boolean failIfWorkDirIsMissing) { this.failIfWorkDirIsMissing = failIfWorkDirIsMissing; }
+
/**
* Returns {@code true} if and only if the socket to the master will have {@link Socket#setKeepAlive(boolean)} set.
*
@@ -239,6 +392,7 @@ public void removeListener(EngineListener el) {
@Override
public void run() {
+ // Create the engine
try {
IOHub hub = IOHub.create(executor);
try {
diff --git a/src/main/java/hudson/remoting/FileSystemJarCache.java b/src/main/java/hudson/remoting/FileSystemJarCache.java
index 09471d5c6..173697062 100644
--- a/src/main/java/hudson/remoting/FileSystemJarCache.java
+++ b/src/main/java/hudson/remoting/FileSystemJarCache.java
@@ -37,11 +37,15 @@ public class FileSystemJarCache extends JarCacheSupport {
private final Map checksumsByPath = new HashMap<>();
/**
+ * @param rootDir
+ * Root directory.
* @param touch
* True to touch the cached jar file that's used. This enables external LRU based cache
* eviction at the expense of increased I/O.
+ * @throws IllegalArgumentException
+ * Root directory is {@code null} or not writable.
*/
- public FileSystemJarCache(File rootDir, boolean touch) {
+ public FileSystemJarCache(@Nonnull File rootDir, boolean touch) {
this.rootDir = rootDir;
this.touch = touch;
if (rootDir==null)
@@ -54,6 +58,11 @@ public FileSystemJarCache(File rootDir, boolean touch) {
}
}
+ @Override
+ public String toString() {
+ return String.format("FileSystem JAR Cache: path=%s, touch=%s", rootDir, Boolean.toString(touch));
+ }
+
@Override
protected URL lookInCache(Channel channel, long sum1, long sum2) throws IOException, InterruptedException {
File jar = map(sum1, sum2);
diff --git a/src/main/java/hudson/remoting/Launcher.java b/src/main/java/hudson/remoting/Launcher.java
index a63bb8b45..753204b2c 100644
--- a/src/main/java/hudson/remoting/Launcher.java
+++ b/src/main/java/hudson/remoting/Launcher.java
@@ -27,14 +27,19 @@
import hudson.remoting.Channel.Mode;
import java.io.FileInputStream;
import java.io.UnsupportedEncodingException;
+import java.nio.file.Files;
+import java.nio.file.Path;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchProviderException;
import java.security.PrivilegedActionException;
import java.security.cert.CertificateFactory;
import javax.annotation.CheckForNull;
+import javax.annotation.Nonnull;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManagerFactory;
+
+import org.jenkinsci.remoting.engine.WorkDirManager;
import org.jenkinsci.remoting.util.IOUtils;
import org.w3c.dom.Document;
import org.w3c.dom.NodeList;
@@ -95,7 +100,7 @@
import javax.crypto.spec.SecretKeySpec;
/**
- * Entry point for running a {@link Channel}. This is the main method of the slave JVM.
+ * Entry point for running a {@link Channel}. This is the main method of the agent JVM.
*
*
* This class also defines several methods for
@@ -111,7 +116,13 @@ public class Launcher {
@Option(name="-ping")
public boolean ping = true;
- @Option(name="-slaveLog", usage="create local slave error log")
+ /**
+ * Specifies a destination for error logs.
+ * If specified, this option overrides the default destination within {@link #workDir}.
+ * If both this options and {@link #workDir} is not set, the log will not be generated.
+ */
+ @Option(name="-agentLog", aliases = {"-slaveLog"}, usage="Local agent error log destination (overrides workDir)")
+ @CheckForNull
public File slaveLog = null;
@Option(name="-text",usage="encode communication with the master with base64. " +
@@ -165,6 +176,14 @@ public void addClasspath(String pathList) throws Exception {
@Option(name="-jar-cache",metaVar="DIR",usage="Cache directory that stores jar files sent from the master")
public File jarCache = new File(System.getProperty("user.home"),".jenkins/cache/jars");
+ /**
+ * Specified location of the property file with JUL settings.
+ * @since TODO
+ */
+ @CheckForNull
+ @Option(name="-loggingConfig",usage="Path to the property file with java.util.logging settings")
+ public File loggingConfigFilePath = null;
+
@Option(name = "-cert",
usage = "Specify additional X.509 encoded PEM certificates to trust when connecting to Jenkins " +
"root URLs. If starting with @ then the remainder is assumed to be the name of the " +
@@ -210,6 +229,45 @@ public boolean verify(String s, SSLSession sslSession) {
usage = "Disable TCP socket keep alive on connection to the master.")
public boolean noKeepAlive = false;
+ /**
+ * Specifies a default working directory of the remoting instance.
+ * If specified, this directory will be used to store logs, JAR cache, etc.
+ *
+ * In order to retain compatibility, the option is disabled by default.
+ *
+ * Jenkins specifics: This working directory is expected to be equal to the agent root specified in Jenkins configuration.
+ * @since TODO
+ */
+ @Option(name = "-workDir",
+ usage = "Declares the working directory of the remoting instance (stores cache and logs by default)")
+ @CheckForNull
+ public File workDir = null;
+
+ /**
+ * Specifies a directory within {@link #workDir}, which stores all the remoting-internal files.
+ *
+ * This option is not expected to be used frequently, but it allows remoting users to specify a custom
+ * storage directory if the default {@code remoting} directory is consumed by other stuff.
+ * @since TODO
+ */
+ @Option(name = "-internalDir",
+ usage = "Specifies a name of the internal files within a working directory ('remoting' by default)",
+ depends = "-workDir")
+ @Nonnull
+ public String internalDir = WorkDirManager.DirType.INTERNAL_DIR.getDefaultLocation();
+
+ /**
+ * Fail the initialization if the workDir or internalDir are missing.
+ * This option presumes that the workspace structure gets initialized previously in order to ensure that we do not start up with a borked instance
+ * (e.g. if a filesystem mount gets disconnected).
+ * @since TODO
+ */
+ @Option(name = "-failIfWorkDirIsMissing",
+ usage = "Fails the initialization if the requested workDir or internalDir are missing ('false' by default)",
+ depends = "-workDir")
+ @Nonnull
+ public boolean failIfWorkDirIsMissing = WorkDirManager.DEFAULT_FAIL_IF_WORKDIR_IS_MISSING;
+
public static void main(String... args) throws Exception {
Launcher launcher = new Launcher();
CmdLineParser parser = new CmdLineParser(launcher);
@@ -226,9 +284,18 @@ public static void main(String... args) throws Exception {
@edu.umd.cs.findbugs.annotations.SuppressWarnings("DM_DEFAULT_ENCODING") // log file, just like console output, should be in platform default encoding
public void run() throws Exception {
- if (slaveLog!=null) {
- System.setErr(new PrintStream(new TeeOutputStream(System.err,new FileOutputStream(slaveLog))));
+
+ // Create and verify working directory and logging
+ // TODO: The pass-through for the JNLP mode has been added in JENKINS-39817. But we still need to keep this parameter in
+ // consideration for other modes (TcpServer, TcpClient, etc.) to retain the legacy behavior.
+ // On the other hand, in such case there is no need to invoke WorkDirManager and handle the double initialization logic
+ final WorkDirManager workDirManager = WorkDirManager.getInstance();
+ final Path internalDirPath = workDirManager.initializeWorkDir(workDir, internalDir, failIfWorkDirIsMissing);
+ if (slaveLog != null) {
+ workDirManager.disable(WorkDirManager.DirType.LOGS_DIR);
}
+ workDirManager.setupLogging(internalDirPath, slaveLog != null ? slaveLog.toPath() : null);
+
if(auth!=null) {
final int idx = auth.indexOf(':');
if(idx<0) throw new CmdLineException(null, "No ':' in the -auth option");
@@ -256,6 +323,23 @@ public void run() throws Exception {
if (this.noKeepAlive) {
jnlpArgs.add("-noKeepAlive");
}
+ if (slaveLog != null) {
+ jnlpArgs.add("-agentLog");
+ jnlpArgs.add(slaveLog.getPath());
+ }
+ if (loggingConfigFilePath != null) {
+ jnlpArgs.add("-loggingConfig");
+ jnlpArgs.add(loggingConfigFilePath.getAbsolutePath());
+ }
+ if (this.workDir != null) {
+ jnlpArgs.add("-workDir");
+ jnlpArgs.add(workDir.getAbsolutePath());
+ jnlpArgs.add("-internalDir");
+ jnlpArgs.add(internalDir);
+ if (failIfWorkDirIsMissing) {
+ jnlpArgs.add("-failIfWorkDirIsMissing");
+ }
+ }
if (candidateCertificates != null && !candidateCertificates.isEmpty()) {
for (String c: candidateCertificates) {
jnlpArgs.add("-cert");
diff --git a/src/main/java/hudson/remoting/TeeOutputStream.java b/src/main/java/hudson/remoting/TeeOutputStream.java
index 56051fce0..6967324e4 100644
--- a/src/main/java/hudson/remoting/TeeOutputStream.java
+++ b/src/main/java/hudson/remoting/TeeOutputStream.java
@@ -16,6 +16,9 @@
*/
package hudson.remoting;
+import org.kohsuke.accmod.Restricted;
+import org.kohsuke.accmod.restrictions.NoExternalUse;
+
import java.io.FilterOutputStream;
import java.io.IOException;
import java.io.OutputStream;
@@ -27,7 +30,8 @@
*
* @version $Id: TeeOutputStream.java 610010 2008-01-08 14:50:59Z niallp $
*/
-class TeeOutputStream extends FilterOutputStream {
+@Restricted(NoExternalUse.class)
+public class TeeOutputStream extends FilterOutputStream {
/** the second OutputStream to write to */
protected OutputStream branch;
diff --git a/src/main/java/hudson/remoting/jnlp/Main.java b/src/main/java/hudson/remoting/jnlp/Main.java
index 60e2ce480..d262369bf 100644
--- a/src/main/java/hudson/remoting/jnlp/Main.java
+++ b/src/main/java/hudson/remoting/jnlp/Main.java
@@ -32,6 +32,8 @@
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
+
+import org.jenkinsci.remoting.engine.WorkDirManager;
import org.jenkinsci.remoting.util.IOUtils;
import org.kohsuke.args4j.Option;
import org.kohsuke.args4j.CmdLineParser;
@@ -49,6 +51,10 @@
import hudson.remoting.Engine;
import hudson.remoting.EngineListener;
+import java.nio.file.Path;
+
+import javax.annotation.CheckForNull;
+import javax.annotation.Nonnull;
/**
* Entry point to JNLP slave agent.
@@ -96,6 +102,63 @@ public class Main {
"certificate file to read.")
public List candidateCertificates;
+ /**
+ * Specifies a destination for error logs.
+ * If specified, this option overrides the default destination within {@link #workDir}.
+ * If both this options and {@link #workDir} is not set, the log will not be generated.
+ * @since TODO
+ */
+ @Option(name="-agentLog", usage="Local agent error log destination (overrides workDir)")
+ @CheckForNull
+ public File agentLog = null;
+
+ /**
+ * Specified location of the property file with JUL settings.
+ * @since TODO
+ */
+ @CheckForNull
+ @Option(name="-loggingConfig",usage="Path to the property file with java.util.logging settings")
+ public File loggingConfigFile = null;
+
+ /**
+ * Specifies a default working directory of the remoting instance.
+ * If specified, this directory will be used to store logs, JAR cache, etc.
+ *
+ * In order to retain compatibility, the option is disabled by default.
+ *
+ * Jenkins specifics: This working directory is expected to be equal to the agent root specified in Jenkins configuration.
+ * @since TODO
+ */
+ @Option(name = "-workDir",
+ usage = "Declares the working directory of the remoting instance (stores cache and logs by default)")
+ @CheckForNull
+ public File workDir = null;
+
+ /**
+ * Specifies a directory within {@link #workDir}, which stores all the remoting-internal files.
+ *
+ * This option is not expected to be used frequently, but it allows remoting users to specify a custom
+ * storage directory if the default {@code remoting} directory is consumed by other stuff.
+ * @since TODO
+ */
+ @Option(name = "-internalDir",
+ usage = "Specifies a name of the internal files within a working directory ('remoting' by default)",
+ depends = "-workDir")
+ @Nonnull
+ public String internalDir = WorkDirManager.DirType.INTERNAL_DIR.getDefaultLocation();
+
+ /**
+ * Fail the initialization if the workDir or internalDir are missing.
+ * This option presumes that the workspace structure gets initialized previously in order to ensure that we do not start up with a borked instance
+ * (e.g. if a filesystem mount gets disconnected).
+ * @since TODO
+ */
+ @Option(name = "-failIfWorkDirIsMissing",
+ usage = "Fails the initialization if the requested workDir or internalDir are missing ('false' by default)",
+ depends = "-workDir")
+ @Nonnull
+ public boolean failIfWorkDirIsMissing = WorkDirManager.DEFAULT_FAIL_IF_WORKDIR_IS_MISSING;
+
/**
* @since 2.24
*/
@@ -151,7 +214,7 @@ public static void _main(String[] args) throws IOException, InterruptedException
public void main() throws IOException, InterruptedException {
Engine engine = createEngine();
- engine.start();
+ engine.startEngine();
try {
engine.join();
LOGGER.fine("Engine has died");
@@ -179,6 +242,15 @@ public Engine createEngine() {
engine.setJarCache(new FileSystemJarCache(jarCache,true));
engine.setNoReconnect(noReconnect);
engine.setKeepAlive(!noKeepAlive);
+
+ // TODO: ideally logging should be initialized before the "Setting up slave" entry
+ if (agentLog != null) {
+ engine.setAgentLog(agentLog.toPath());
+ }
+ if (loggingConfigFile != null) {
+ engine.setLoggingConfigFile(loggingConfigFile.toPath());
+ }
+
if (candidateCertificates != null && !candidateCertificates.isEmpty()) {
CertificateFactory factory;
try {
@@ -246,6 +318,14 @@ public Engine createEngine() {
}
engine.setCandidateCertificates(certificates);
}
+
+ // Working directory settings
+ if (workDir != null) {
+ engine.setWorkDir(workDir.toPath());
+ }
+ engine.setInternalDir(internalDir);
+ engine.setFailIfWorkDirIsMissing(failIfWorkDirIsMissing);
+
return engine;
}
diff --git a/src/main/java/org/jenkinsci/remoting/engine/WorkDirManager.java b/src/main/java/org/jenkinsci/remoting/engine/WorkDirManager.java
new file mode 100644
index 000000000..2f3266afa
--- /dev/null
+++ b/src/main/java/org/jenkinsci/remoting/engine/WorkDirManager.java
@@ -0,0 +1,384 @@
+/*
+ * The MIT License
+ *
+ * Copyright (c) 2016 CloudBees, Inc.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ *
+ */
+
+package org.jenkinsci.remoting.engine;
+
+import hudson.remoting.TeeOutputStream;
+import org.kohsuke.accmod.Restricted;
+import org.kohsuke.accmod.restrictions.NoExternalUse;
+
+import javax.annotation.CheckForNull;
+import javax.annotation.Nonnull;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.PrintStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.logging.ConsoleHandler;
+import java.util.logging.FileHandler;
+import java.util.logging.Handler;
+import java.util.logging.Level;
+import java.util.logging.LogManager;
+import java.util.logging.Logger;
+import java.util.logging.SimpleFormatter;
+
+/**
+ * Performs working directory management in remoting.
+ * Using this manager remoting can initialize its working directory and put the data there.
+ * The structure of the directory is described in {@link DirType}.
+ * @author Oleg Nenashev
+ * @since TODO
+ */
+@Restricted(NoExternalUse.class)
+public class WorkDirManager {
+
+ private static final Logger LOGGER = Logger.getLogger(WorkDirManager.class.getName());
+
+ private static WorkDirManager INSTANCE = new WorkDirManager();
+
+ /**
+ * Default value for the behavior when the requested working directory is missing.
+ * The default value is {@code false}, because otherwise agents would fail on the first startup.
+ */
+ public static final boolean DEFAULT_FAIL_IF_WORKDIR_IS_MISSING = false;
+
+ /**
+ * Regular expression, which declares restrictions of the remoting internal directory symbols
+ */
+ public static final String SUPPORTED_INTERNAL_DIR_NAME_MASK = "[a-zA-Z0-9._-]*";
+
+ /**
+ * Name of the standard System Property, which points to the {@code java.util.logging} config file.
+ */
+ public static final String JUL_CONFIG_FILE_SYSTEM_PROPERTY_NAME = "java.util.logging.config.file";
+
+ // Status data
+ boolean loggingInitialized;
+
+ @CheckForNull
+ private File loggingConfigFile = null;
+
+ /**
+ * Provides a list of directories, which should not be initialized in the Work Directory.
+ */
+ private final Set disabledDirectories = new HashSet<>();
+
+ /**
+ * Cache of initialized directory locations.
+ */
+ private final Map directories = new HashMap<>();
+
+
+ private WorkDirManager() {
+ // Cannot be instantiated outside
+ }
+
+ /**
+ * Retrieves the instance of the {@link WorkDirManager}.
+ * Currently the implementation is hardcoded, but it may change in the future.
+ * @return Workspace manager
+ */
+ @Nonnull
+ public static WorkDirManager getInstance() {
+ return INSTANCE;
+ }
+
+ /*package*/ static void reset() {
+ INSTANCE = new WorkDirManager();
+ LogManager.getLogManager().reset();
+ }
+
+ public void disable(@Nonnull DirType dir) {
+ disabledDirectories.add(dir);
+ }
+
+ @CheckForNull
+ public File getLocation(@Nonnull DirType type) {
+ return directories.get(type);
+ }
+
+ /**
+ * Sets path to the Logging JUL property file with logging settings.
+ * @param configFile config file
+ */
+ public void setLoggingConfig(@Nonnull File configFile) {
+ this.loggingConfigFile = configFile;
+ }
+
+ /**
+ * Gets path to the property file with JUL settings.
+ * This method checks the value passed from {@link #setLoggingConfig(java.io.File)} first.
+ * If the value is not specified, it also checks the standard {@code java.util.logging.config.file} System property.
+ * @return Path to the logging config file.
+ * {@code null} if it cannot be found.
+ */
+ @CheckForNull
+ public File getLoggingConfigFile() {
+ if (loggingConfigFile != null) {
+ return loggingConfigFile;
+ }
+
+ String property = System.getProperty(JUL_CONFIG_FILE_SYSTEM_PROPERTY_NAME);
+ if (property != null && !property.trim().isEmpty()) {
+ return new File(property);
+ }
+
+ return null;
+ }
+
+ //TODO: New interfaces should ideally use Path instead of File
+ /**
+ * Initializes the working directory for the agent.
+ * Within the working directory the method also initializes a working directory for internal needs (like logging)
+ * @param workDir Working directory
+ * @param internalDir Name of the remoting internal data directory within the working directory.
+ * The range of the supported symbols is restricted to {@link #SUPPORTED_INTERNAL_DIR_NAME_MASK}.
+ * @param failIfMissing Fail the initialization if the workDir or internalDir are missing.
+ * This option presumes that the workspace structure gets initialized previously in order to ensure that we do not start up with a borked instance
+ * (e.g. if a mount gets disconnected).
+ * @return Initialized directory for internal files within workDir or {@code null} if it is disabled
+ * @throws IOException Workspace allocation issue (e.g. the specified directory is not writable).
+ * In such case Remoting should not start up at all.
+ */
+ @CheckForNull
+ public Path initializeWorkDir(final @CheckForNull File workDir, final @Nonnull String internalDir, final boolean failIfMissing) throws IOException {
+
+ if (!internalDir.matches(SUPPORTED_INTERNAL_DIR_NAME_MASK)) {
+ throw new IOException(String.format("Name of %s ('%s') is not compliant with the required format: %s",
+ DirType.INTERNAL_DIR, internalDir, SUPPORTED_INTERNAL_DIR_NAME_MASK));
+ }
+
+ if (workDir == null) {
+ // TODO: this message likely suppresses the connection setup on CI
+ // LOGGER.log(Level.WARNING, "Agent working directory is not specified. Some functionality introduced in Remoting 3 may be disabled");
+ return null;
+ } else {
+ // Verify working directory
+ verifyDirectory(workDir, DirType.WORK_DIR, failIfMissing);
+ directories.put(DirType.WORK_DIR, workDir);
+
+ // Create a subdirectory for remoting operations
+ final File internalDirFile = new File(workDir, internalDir);
+ verifyDirectory(internalDirFile, DirType.INTERNAL_DIR, failIfMissing);
+ directories.put(DirType.INTERNAL_DIR, internalDirFile);
+
+ // Create the directory on-demand
+ final Path internalDirPath = internalDirFile.toPath();
+ Files.createDirectories(internalDirPath);
+ LOGGER.log(Level.INFO, "Using {0} as a remoting work directory", internalDirPath);
+
+ // Create components of the internal directory
+ createInternalDirIfRequired(internalDirFile, DirType.JAR_CACHE_DIR);
+ createInternalDirIfRequired(internalDirFile, DirType.LOGS_DIR);
+
+ return internalDirPath;
+ }
+ }
+
+ private void createInternalDirIfRequired(File internalDir, DirType type)
+ throws IOException {
+ if (!disabledDirectories.contains(type)) {
+ final File directory = new File(internalDir, type.getDefaultLocation());
+ verifyDirectory(directory, type, false);
+ Files.createDirectories(directory.toPath());
+ directories.put(type, directory);
+ } else {
+ LOGGER.log(Level.FINE, "Skipping the disabled directory: {0}", type.getName());
+ }
+ }
+
+ /**
+ * Verifies that the directory is compliant with the specified requirements.
+ * The directory is expected to have {@code RWX} permissions if exists.
+ * @param dir Directory
+ * @param type Type of the working directory component to be verified
+ * @param failIfMissing Fail if the directory is missing
+ * @throws IOException Verification failure
+ */
+ private static void verifyDirectory(@Nonnull File dir, @Nonnull DirType type, boolean failIfMissing) throws IOException {
+ if (dir.exists()) {
+ if (!dir.isDirectory()) {
+ throw new IOException("The specified " + type + " path points to a non-directory file: " + dir.getPath());
+ }
+ if (!dir.canWrite() || !dir.canRead() || !dir.canExecute()) {
+ throw new IOException("The specified " + type + " should be fully accessible to the remoting executable (RWX): " + dir.getPath());
+ }
+ } else if (failIfMissing) {
+ throw new IOException("The " + type + " is missing, but it is expected to exist: " + dir.getPath());
+ }
+ }
+
+ /**
+ * Sets up logging subsystem in the working directory.
+ * If the logging system is already initialized, do nothing
+ * @param internalDirPath Path to the internal remoting directory within the WorkDir.
+ * If this value and {@code overrideLogPath} are null, the logging subsystem woill not
+ * be initialized at all
+ * @param overrideLogPath Overrides the common location of the remoting log.
+ * If specified, logging system will be initialized in the legacy way.
+ * If {@code null}, the behavior is defined by {@code internalDirPath}.
+ * @throws IOException Initialization error
+ */
+ public void setupLogging(@CheckForNull Path internalDirPath, @CheckForNull Path overrideLogPath) throws IOException {
+ if (loggingInitialized) {
+ // Do nothing, in Remoting initialization there may be two calls due to the
+ // legacy -slaveLog behavior implementation.
+ LOGGER.log(Level.CONFIG, "Logging susystem has been already initialized");
+ return;
+ }
+
+ final File configFile = getLoggingConfigFile();
+ if (configFile != null) {
+ // TODO: There is a risk of second configuration call in the case of java.util.logging.config.file, but it's safe
+ LOGGER.log(Level.FINE, "Reading Logging configuration from file: {0}", configFile);
+ try(FileInputStream fis = new FileInputStream(configFile)) {
+ LogManager.getLogManager().readConfiguration(fis);
+ }
+ }
+
+ if (overrideLogPath != null) { // Legacy behavior
+ System.out.println("Using " + overrideLogPath + " as an agent Error log destination. 'Out' log won't be generated");
+ System.out.flush(); // Just in case the channel
+ System.err.flush();
+ System.setErr(new PrintStream(new TeeOutputStream(System.err, new FileOutputStream(overrideLogPath.toFile()))));
+ this.loggingInitialized = true;
+ } else if (internalDirPath != null) { // New behavior
+ System.out.println("Both error and output logs will be printed to " + internalDirPath);
+ System.out.flush();
+ System.err.flush();
+
+ // Also redirect JUL to files if custom logging is not specified
+ final File internalDirFile = internalDirPath.toFile();
+ createInternalDirIfRequired(internalDirFile, DirType.LOGS_DIR);
+ final File logsDir = getLocation(DirType.LOGS_DIR);
+
+ // TODO: Forward these logs? Likely no, we do not expect something to get there
+ //System.setErr(new PrintStream(new TeeOutputStream(System.err,
+ // new FileOutputStream(new File(logsDir, "remoting.err.log")))));
+ //System.setOut(new PrintStream(new TeeOutputStream(System.out,
+ // new FileOutputStream(new File(logsDir, "remoting.out.log")))));
+
+ if (configFile == null) {
+ final Logger rootLogger = Logger.getLogger("");
+ final File julLog = new File(logsDir, "remoting.log");
+ final FileHandler logHandler = new FileHandler(julLog.getAbsolutePath(),
+ 10*1024*1024, 5, false);
+ logHandler.setFormatter(new SimpleFormatter());
+ logHandler.setLevel(Level.INFO);
+ rootLogger.addHandler(logHandler);
+
+ // TODO: Uncomment if there is TeeOutputStream added
+ // Remove console handler since the logs are going to the file now
+ // ConsoleHandler consoleHandler = findConsoleHandler(rootLogger);
+ // if (consoleHandler != null) {
+ // rootLogger.removeHandler(consoleHandler);
+ // }
+ }
+
+ this.loggingInitialized = true;
+ } else {
+ // TODO: This message is suspected to break the CI
+ // System.err.println("WARNING: Log location is not specified (neither -workDir nor -slaveLog/-agentLog set)");
+ }
+ }
+
+ @CheckForNull
+ private static ConsoleHandler findConsoleHandler(Logger logger) {
+ for (Handler h : logger.getHandlers()) {
+ if (h instanceof ConsoleHandler) {
+ return (ConsoleHandler)h;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Defines components of the Working directory.
+ * @since TODO
+ */
+ @Restricted(NoExternalUse.class)
+ public enum DirType {
+ /**
+ * Top-level entry of the working directory.
+ */
+ WORK_DIR("working directory", "", null),
+ /**
+ * Directory, which stores internal data of the Remoting layer itself.
+ * This directory is located within {@link #WORK_DIR}.
+ */
+ INTERNAL_DIR("remoting internal directory", "remoting", WORK_DIR),
+
+ /**
+ * Directory, which stores the JAR Cache.
+ */
+ JAR_CACHE_DIR("JAR Cache directory", "jarCache", INTERNAL_DIR),
+
+ /**
+ * Directory, which stores logs.
+ */
+ LOGS_DIR("Log directory", "logs", INTERNAL_DIR);
+
+ @Nonnull
+ private final String name;
+
+ @Nonnull
+ private final String defaultLocation;
+
+ @CheckForNull
+ private final DirType parentDir;
+
+ DirType(String name, String defaultLocation, @CheckForNull DirType parentDir) {
+ this.name = name;
+ this.defaultLocation = defaultLocation;
+ this.parentDir = parentDir;
+ }
+
+ @Override
+ public String toString() {
+ return name;
+ }
+
+ @Nonnull
+ public String getDefaultLocation() {
+ return defaultLocation;
+ }
+
+ @Nonnull
+ public String getName() {
+ return name;
+ }
+
+ @CheckForNull
+ public DirType getParentDir() {
+ return parentDir;
+ }
+ }
+}
diff --git a/src/test/java/org/jenkinsci/remoting/engine/WorkDirManagerTest.java b/src/test/java/org/jenkinsci/remoting/engine/WorkDirManagerTest.java
new file mode 100644
index 000000000..30d979a3f
--- /dev/null
+++ b/src/test/java/org/jenkinsci/remoting/engine/WorkDirManagerTest.java
@@ -0,0 +1,401 @@
+/*
+ *
+ * * The MIT License
+ * *
+ * * Copyright (c) 2016-2017 CloudBees, Inc.
+ * *
+ * * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * * of this software and associated documentation files (the "Software"), to deal
+ * * in the Software without restriction, including without limitation the rights
+ * * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * * copies of the Software, and to permit persons to whom the Software is
+ * * furnished to do so, subject to the following conditions:
+ * *
+ * * The above copyright notice and this permission notice shall be included in
+ * * all copies or substantial portions of the Software.
+ * *
+ * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * * THE SOFTWARE.
+ *
+ */
+
+package org.jenkinsci.remoting.engine;
+
+import org.apache.commons.io.FileUtils;
+import org.junit.Assert;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import javax.annotation.Nonnull;
+import org.apache.commons.io.IOUtils;
+
+import static org.hamcrest.CoreMatchers.*;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import org.jenkinsci.remoting.engine.WorkDirManager.DirType;
+import org.junit.After;
+import org.junit.Before;
+import org.jvnet.hudson.test.Bug;
+
+/**
+ * Tests of {@link WorkDirManager}
+ * @author Oleg Nenashev.
+ * @since TODO
+ */
+public class WorkDirManagerTest {
+
+ @Rule
+ public TemporaryFolder tmpDir = new TemporaryFolder();
+
+ @Before
+ public void resetWorkDirManagerBeforeTest() {
+ WorkDirManager.reset();
+ }
+
+ @After
+ public void resetWorkDirManagerAfterTest() {
+ // Resets logging settings
+ WorkDirManager.reset();
+ }
+
+ @Test
+ public void shouldInitializeCorrectlyForExistingDirectory() throws Exception {
+ final File dir = tmpDir.newFolder("foo");
+
+ // Probe files to confirm the directory does not get wiped
+ final File probeFileInWorkDir = new File(dir, "probe.txt");
+ FileUtils.write(probeFileInWorkDir, "Hello!");
+ final File remotingDir = new File(dir, DirType.INTERNAL_DIR.getDefaultLocation());
+ Files.createDirectory(remotingDir.toPath());
+ final File probeFileInInternalDir = new File(remotingDir, "/probe.txt");
+ FileUtils.write(probeFileInInternalDir, "Hello!");
+
+ // Initialize and check the results
+ final Path createdDir = WorkDirManager.getInstance().initializeWorkDir(dir, DirType.INTERNAL_DIR.getDefaultLocation(), false);
+ assertThat("The initialized " + DirType.INTERNAL_DIR + " differs from the expected one", createdDir.toFile(), equalTo(remotingDir));
+
+ // Ensure that the files have not been wiped
+ Assert.assertTrue("Probe file in the " + DirType.WORK_DIR + " has been wiped", probeFileInWorkDir.exists());
+ Assert.assertTrue("Probe file in the " + DirType.INTERNAL_DIR + " has been wiped", probeFileInInternalDir.exists());
+
+ // Ensure that sub directories are in place
+ assertExists(DirType.JAR_CACHE_DIR);
+ assertExists(DirType.LOGS_DIR);
+ }
+
+ @Test
+ public void shouldPerformMkdirsIfRequired() throws Exception {
+ final File tmpDirFile = tmpDir.newFolder("foo");
+ final File workDir = new File(tmpDirFile, "just/a/long/non/existent/path");
+ Assert.assertFalse("The " + DirType.INTERNAL_DIR + " should not exist in the test", workDir.exists());
+
+ // Probe files to confirm the directory does not get wiped;
+ final File remotingDir = new File(workDir, DirType.INTERNAL_DIR.getDefaultLocation());
+
+ // Initialize and check the results
+ final Path createdDir = WorkDirManager.getInstance().initializeWorkDir(workDir, DirType.INTERNAL_DIR.getDefaultLocation(), false);
+ assertThat("The initialized " + DirType.INTERNAL_DIR + " differs from the expected one", createdDir.toFile(), equalTo(remotingDir));
+ Assert.assertTrue("Remoting " + DirType.INTERNAL_DIR + " should have been initialized", remotingDir.exists());
+ }
+
+ @Test
+ public void shouldProperlyCreateDirectoriesForCustomInternalDirs() throws Exception {
+ final String internalDirectoryName = "myRemotingLogs";
+ final File tmpDirFile = tmpDir.newFolder("foo");
+ final File workDir = new File(tmpDirFile, "just/another/path");
+ Assert.assertFalse("The " + DirType.WORK_DIR + " should not exist in the test", workDir.exists());
+
+ // Probe files to confirm the directory does not get wiped;
+ final File remotingDir = new File(workDir, internalDirectoryName);
+
+ // Initialize and check the results
+ final Path createdDir = WorkDirManager.getInstance().initializeWorkDir(workDir, internalDirectoryName, false);
+ assertThat("The initialized " + DirType.INTERNAL_DIR + " differs from the expected one", createdDir.toFile(), equalTo(remotingDir));
+ Assert.assertTrue("Remoting " + DirType.INTERNAL_DIR + " should have been initialized", remotingDir.exists());
+ }
+
+ @Test
+ public void shouldFailIfWorkDirIsAFile() throws IOException {
+ File foo = tmpDir.newFile("foo");
+ try {
+ WorkDirManager.getInstance().initializeWorkDir(foo, DirType.INTERNAL_DIR.getDefaultLocation(), false);
+ } catch (IOException ex) {
+ assertThat("Wrong exception message",
+ ex.getMessage(), containsString("The specified " + DirType.WORK_DIR + " path points to a non-directory file"));
+ return;
+ }
+ Assert.fail("The " + DirType.WORK_DIR + " has been initialized, but it should fail due to the conflicting file");
+ }
+
+ @Test
+ public void shouldFailIfWorkDirIsNotExecutable() throws IOException {
+ verifyDirectoryFlag(DirType.WORK_DIR, DirectoryFlag.NOT_EXECUTABLE);
+ }
+
+ @Test
+ public void shouldFailIfWorkDirIsNotWritable() throws IOException {
+ verifyDirectoryFlag(DirType.WORK_DIR, DirectoryFlag.NOT_WRITABLE);
+ }
+
+
+ @Test
+ public void shouldFailIfWorkDirIsNotReadable() throws IOException {
+ verifyDirectoryFlag(DirType.WORK_DIR, DirectoryFlag.NOT_READABLE);
+ }
+
+ @Test
+ public void shouldFailIfInternalDirIsNotExecutable() throws IOException {
+ verifyDirectoryFlag(DirType.INTERNAL_DIR, DirectoryFlag.NOT_EXECUTABLE);
+ }
+
+ @Test
+ public void shouldFailIfInternalDirIsNotWritable() throws IOException {
+ verifyDirectoryFlag(DirType.INTERNAL_DIR, DirectoryFlag.NOT_WRITABLE);
+ }
+
+
+ @Test
+ public void shouldFailIfInternalDirIsNotReadable() throws IOException {
+ verifyDirectoryFlag(DirType.INTERNAL_DIR, DirectoryFlag.NOT_READABLE);
+ }
+
+ @Test
+ public void shouldNotSupportPathDelimitersAndSpacesInTheInternalDirName() throws IOException {
+ File foo = tmpDir.newFolder("foo");
+
+ assertAllocationFails(foo, " remoting ");
+ assertAllocationFails(foo, " remoting");
+ assertAllocationFails(foo, "directory with spaces");
+ assertAllocationFails(foo, "nested/directory");
+ assertAllocationFails(foo, "nested\\directory\\in\\Windows");
+ assertAllocationFails(foo, "just&a&symbol&I&do¬&like");
+ }
+
+ @Test
+ @Bug(39130)
+ public void shouldFailToStartupIf_WorkDir_IsMissing_andRequired() throws Exception {
+ final File tmpDirFile = tmpDir.newFolder("foo");
+ final File workDir = new File(tmpDirFile, "just/a/long/non/existent/path");
+ Assert.assertFalse("The " + DirType.INTERNAL_DIR + " should not exist in the test", workDir.exists());
+
+ assertAllocationFailsForMissingDir(workDir, DirType.WORK_DIR);
+ }
+
+ @Test
+ @Bug(39130)
+ public void shouldFailToStartupIf_InternalDir_IsMissing_andRequired() throws Exception {
+ // Create only the working directory, not the nested one
+ final File tmpDirFile = tmpDir.newFolder("foo");
+ final File workDir = new File(tmpDirFile, "just/a/long/non/existent/path");
+ Files.createDirectories(workDir.toPath());
+
+ assertAllocationFailsForMissingDir(workDir, DirType.INTERNAL_DIR);
+ }
+
+ @Test
+ public void shouldNotCreateLogsDirIfDisabled() throws Exception {
+ final File tmpDirFile = tmpDir.newFolder("foo");
+ assertDoesNotCreateDisabledDir(tmpDirFile, DirType.LOGS_DIR);
+ }
+
+ @Test
+ public void shouldNotCreateJarCacheIfDisabled() throws Exception {
+ final File tmpDirFile = tmpDir.newFolder("foo");
+ assertDoesNotCreateDisabledDir(tmpDirFile, DirType.JAR_CACHE_DIR);
+ }
+
+ @Test
+ public void shouldCreateLogFilesOnTheDisk() throws Exception {
+ final File workDir = tmpDir.newFolder("workDir");
+ final WorkDirManager mngr = WorkDirManager.getInstance();
+ mngr.initializeWorkDir(workDir, "remoting", false);
+ mngr.setupLogging(mngr.getLocation(DirType.INTERNAL_DIR).toPath(), null);
+
+ // Write something to logs
+ String message = String.format("Just 4 test. My Work Dir is %s", workDir);
+ Logger.getLogger(WorkDirManager.class.getName()).log(Level.INFO, message);
+
+ // Ensure log files have been created
+ File logsDir = mngr.getLocation(DirType.LOGS_DIR);
+ assertFileLogsExist(logsDir, "remoting.log", 0);
+
+ // Ensure the entry has been written
+ File log0 = new File(logsDir, "remoting.log.0");
+ try (FileInputStream istr = new FileInputStream(log0)) {
+ String contents = IOUtils.toString(istr);
+ assertThat("Log file " + log0 + " should contain the probe message", contents, containsString(message));
+ }
+ }
+
+ @Test
+ public void shouldUseLoggingSettingsFromFileDefinedByAPI() throws Exception {
+ final File loggingConfigFile = new File(tmpDir.getRoot(), "julSettings.prop");
+ doTestLoggingConfig(loggingConfigFile, true);
+ }
+
+ @Test
+ public void shouldUseLoggingSettingsFromFileDefinedBySystemProperty() throws Exception {
+ final File loggingConfigFile = new File(tmpDir.getRoot(), "julSettings.prop");
+ final String oldValue = System.setProperty(WorkDirManager.JUL_CONFIG_FILE_SYSTEM_PROPERTY_NAME, loggingConfigFile.getAbsolutePath());
+ try {
+ doTestLoggingConfig(loggingConfigFile, false);
+ } finally {
+ // TODO: Null check and setting empty string is a weird hack
+ System.setProperty(WorkDirManager.JUL_CONFIG_FILE_SYSTEM_PROPERTY_NAME, oldValue != null ? oldValue : "");
+ }
+ }
+
+ private void doTestLoggingConfig(File loggingConfigFile, boolean passToManager) throws IOException, AssertionError{
+ final File workDir = tmpDir.newFolder("workDir");
+ final File customLogDir = tmpDir.newFolder("mylogs");
+
+ // Create config file
+ try (FileWriter wr = new FileWriter(loggingConfigFile)) {
+ // Init FileHandler with default XML formatter
+ wr.write("handlers= java.util.logging.FileHandler\n");
+ // TODO: It won't work well on Windows
+ wr.write("java.util.logging.FileHandler.pattern=" + customLogDir.getAbsolutePath() + "/mylog.log.%g\n");
+ wr.write("java.util.logging.FileHandler.limit=81920\n");
+ wr.write("java.util.logging.FileHandler.count=5\n");
+ }
+
+ // Init WorkDirManager
+ final WorkDirManager mngr = WorkDirManager.getInstance();
+ if (passToManager) {
+ mngr.setLoggingConfig(loggingConfigFile);
+ }
+ mngr.initializeWorkDir(workDir, "remoting", false);
+ mngr.setupLogging(mngr.getLocation(DirType.INTERNAL_DIR).toPath(), null);
+
+ // Write something to logs
+ String message = String.format("Just 4 test. My Work Dir is %s", workDir);
+ Logger.getLogger(WorkDirManager.class.getName()).log(Level.INFO, message);
+
+ // Assert that logs directory still exists, but has no default logs
+ assertExists(DirType.LOGS_DIR);
+ File defaultLog0 = new File(mngr.getLocation(DirType.LOGS_DIR), "remoting.log.0");
+ Assert.assertFalse("Log settings have been passed from the config file, the default log should not exist: " + defaultLog0,
+ defaultLog0.exists());
+
+ // Assert that logs have been written to the specified custom destination
+ assertFileLogsExist(customLogDir, "mylog.log", 0);
+ File log0 = new File(customLogDir, "mylog.log.0");
+ try (FileInputStream istr = new FileInputStream(log0)) {
+ String contents = IOUtils.toString(istr);
+ assertThat("Log file " + log0 + " should contain the probe message", contents, containsString(message));
+ }
+ }
+
+ private void assertFileLogsExist(File logsDir, String prefix, int logFilesNumber) {
+ for (int i=0; i