diff --git a/main/ui/src/main/java/org/cryptomator/ui/util/command/AsyncStreamCopier.java b/main/ui/src/main/java/org/cryptomator/ui/util/command/AsyncStreamCopier.java index e490646ae..da98bd9a7 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/util/command/AsyncStreamCopier.java +++ b/main/ui/src/main/java/org/cryptomator/ui/util/command/AsyncStreamCopier.java @@ -13,39 +13,57 @@ import static org.apache.commons.io.IOUtils.copy; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; +import java.util.concurrent.locks.Condition; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; -final class AsyncStreamCopier extends Thread { +final class AsyncStreamCopier implements Runnable { + private final Lock lock = new ReentrantLock(); + private final Condition doneCondition = lock.newCondition(); private final InputStream input; private final OutputStream output; - + private IOException exception; - + private boolean done; + public AsyncStreamCopier(InputStream input, OutputStream output) { this.input = input; this.output = output; - start(); } - + @Override public void run() { - try (InputStream inputToBeClosed = input; - OutputStream outputToBeClosed = output) { + lock.lock(); + try (InputStream inputToBeClosed = input; OutputStream outputToBeClosed = output) { copy(input, output); + done = true; + doneCondition.signal(); } catch (IOException e) { exception = e; + } finally { + lock.unlock(); } } - - public void assertOk() throws IOException { + + private void waitUntilDone() { + lock.lock(); try { - join(); + while (!done) { + doneCondition.await(); + } } catch (InterruptedException e) { Thread.currentThread().interrupt(); + } finally { + lock.unlock(); } + } + + public void assertOk() throws IOException { + this.waitUntilDone(); if (exception != null) { throw exception; } } - + } diff --git a/main/ui/src/main/java/org/cryptomator/ui/util/command/CommandResult.java b/main/ui/src/main/java/org/cryptomator/ui/util/command/CommandResult.java index 3798f4c7f..d11ccd67e 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/util/command/CommandResult.java +++ b/main/ui/src/main/java/org/cryptomator/ui/util/command/CommandResult.java @@ -12,92 +12,114 @@ import static java.lang.String.format; import java.io.ByteArrayOutputStream; import java.io.IOException; +import java.util.concurrent.Executor; import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.Condition; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; import org.cryptomator.ui.util.mount.CommandFailedException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class CommandResult { - + private static final Logger LOG = LoggerFactory.getLogger(CommandResult.class); private final ByteArrayOutputStream output = new ByteArrayOutputStream(); private final ByteArrayOutputStream error = new ByteArrayOutputStream(); + private final Lock lock = new ReentrantLock(); + private final Condition finishedCondition = lock.newCondition(); private final Process process; - + private final AsyncStreamCopier processOutputCopier; private final AsyncStreamCopier processErrorCopier; - + private boolean finished; - - public CommandResult(Process process, String[] lines) { + private CommandFailedException exception; + + public CommandResult(Process process, String[] lines, Executor cmdExecutor, long timeout, TimeUnit unit) { this.process = process; - new AsyncLineWriter(lines, process.getOutputStream()); processOutputCopier = new AsyncStreamCopier(process.getInputStream(), output); processErrorCopier = new AsyncStreamCopier(process.getErrorStream(), error); + cmdExecutor.execute(processOutputCopier); + cmdExecutor.execute(processErrorCopier); + cmdExecutor.execute(new CommandTask(timeout, unit)); } - + public String getOutput() throws CommandFailedException { - if (!finished) { - throw new IllegalStateException("Command not yet finished."); - } + this.waitUntilFinished(); return new String(output.toByteArray()); } - + public String getError() throws CommandFailedException { - if (!finished) { - throw new IllegalStateException("Command not yet finished."); - } + this.waitUntilFinished(); return new String(error.toByteArray()); } - - public int getExitValue(long timeout, TimeUnit unit) throws CommandFailedException { - waitAndAssertOk(timeout, unit); + + public int getExitValue() throws CommandFailedException { + this.waitUntilFinished(); return process.exitValue(); } - private void waitAndAssertOk(long timeout, TimeUnit unit) throws CommandFailedException { - if (finished) return; + private void waitUntilFinished() throws CommandFailedException { + lock.lock(); try { - if (!process.waitFor(timeout, unit)) { - throw new CommandFailedException("Waiting time elapsed before command execution finished"); + while (!finished) { + finishedCondition.await(); } - processOutputCopier.assertOk(); - processErrorCopier.assertOk(); - finished = true; - logDebugInfo(); - } catch (IOException | InterruptedException e) { - throw new CommandFailedException(e); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } finally { + lock.unlock(); + } + if (exception != null) { + throw exception; } } private void logDebugInfo() { if (LOG.isDebugEnabled()) { - LOG.debug("Command execution finished. Exit code: {}\n" - + "Output:\n" - + "{}\n" - + "Error:\n" - + "{}\n", - process.exitValue(), - new String(output.toByteArray()), - new String(error.toByteArray())); + LOG.debug("Command execution finished. Exit code: {}\n" + "Output:\n" + "{}\n" + "Error:\n" + "{}\n", process.exitValue(), new String(output.toByteArray()), new String(error.toByteArray())); } } - - public void assertOk(long timeout, TimeUnit unit) throws CommandFailedException { - int exitValue = getExitValue(timeout, unit); + + void assertOk() throws CommandFailedException { + int exitValue = getExitValue(); if (exitValue != 0) { - throw new CommandFailedException(format( - "Command execution failed. Exit code: %d\n" - + "# Output:\n" - + "%s\n" - + "# Error:\n" - + "%s", - exitValue, - new String(output.toByteArray()), + throw new CommandFailedException(format("Command execution failed. Exit code: %d\n" + "# Output:\n" + "%s\n" + "# Error:\n" + "%s", exitValue, new String(output.toByteArray()), new String(error.toByteArray()))); } } + private class CommandTask implements Runnable { + + private final long timeout; + private final TimeUnit unit; + + private CommandTask(long timeout, TimeUnit unit) { + this.timeout = timeout; + this.unit = unit; + } + + @Override + public void run() { + try { + if (!process.waitFor(timeout, unit)) { + exception = new CommandFailedException("Waiting time elapsed before command execution finished"); + } + processOutputCopier.assertOk(); + processErrorCopier.assertOk(); + logDebugInfo(); + } catch (IOException | InterruptedException e) { + exception = new CommandFailedException(e); + } finally { + lock.lock(); + finished = true; + finishedCondition.signal(); + lock.unlock(); + } + } + } + } diff --git a/main/ui/src/main/java/org/cryptomator/ui/util/command/CommandRunner.java b/main/ui/src/main/java/org/cryptomator/ui/util/command/CommandRunner.java index b52f56f02..c2e9c2ce7 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/util/command/CommandRunner.java +++ b/main/ui/src/main/java/org/cryptomator/ui/util/command/CommandRunner.java @@ -14,6 +14,9 @@ import static org.apache.commons.lang3.SystemUtils.IS_OS_WINDOWS; import java.io.IOException; import java.util.List; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; import org.apache.commons.lang3.ArrayUtils; @@ -24,44 +27,43 @@ import org.cryptomator.ui.util.mount.CommandFailedException; *

* Runs commands using a system compatible CLI. *

- * To detect the system type {@link SystemUtils} is used. The following CLIs are - * used by default: + * To detect the system type {@link SystemUtils} is used. The following CLIs are used by default: *

*

- * If the path to the executables differs from the default or the system can not - * be detected the Java system property {@value #CLI_EXECUTABLE_PROPERTY} can be - * set to define it. + * If the path to the executables differs from the default or the system can not be detected the Java system property + * {@value #CLI_EXECUTABLE_PROPERTY} can be set to define it. *

- * If a CLI executable can not be determined using these methods operation of - * {@link CommandRunner} will fail with {@link IllegalStateException}s. + * If a CLI executable can not be determined using these methods operation of {@link CommandRunner} will fail with + * {@link IllegalStateException}s. * * @author Markus Kreusch */ -class CommandRunner { +final class CommandRunner { public static final String CLI_EXECUTABLE_PROPERTY = "cryptomator.cli"; - public static final String WINDOWS_DEFAULT_CLI[] = {"cmd", "/C"}; public static final String UNIX_DEFAULT_CLI[] = {"/bin/sh", "-c"}; - + private static final Executor CMD_EXECUTOR = Executors.newCachedThreadPool(); + /** * Executes all lines in the given script in the specified order. Stops as soon as the first command fails. + * * @param script Script containing command lines and environment variables. * @return Result of the last command, if it exited successfully. * @throws CommandFailedException If one of the command lines in the given script fails. */ - static CommandResult execute(Script script) throws CommandFailedException { + static CommandResult execute(Script script, long timeout, TimeUnit unit) throws CommandFailedException { try { final List env = script.environment().entrySet().stream().map(e -> e.getKey() + "=" + e.getValue()).collect(Collectors.toList()); CommandResult result = null; for (final String line : script.getLines()) { final String[] cmds = ArrayUtils.add(determineCli(), line); final Process proc = Runtime.getRuntime().exec(cmds, env.toArray(new String[0])); - result = run(proc, script.getLines()); - result.assertOk(script.getTimeout(), script.getTimeoutUnit()); + result = run(proc, script.getLines(), timeout, unit); + result.assertOk(); } return result; } catch (IOException e) { @@ -69,8 +71,8 @@ class CommandRunner { } } - private static CommandResult run(Process process, String[] lines) { - return new CommandResult(process, lines); + private static CommandResult run(Process process, String[] lines, long timeout, TimeUnit unit) { + return new CommandResult(process, lines, CMD_EXECUTOR, timeout, unit); } private static String[] determineCli() { @@ -82,10 +84,8 @@ class CommandRunner { } else if (IS_OS_UNIX) { return UNIX_DEFAULT_CLI; } else { - throw new IllegalStateException(format( - "Failed to determine cli to use. Set Java system property %s to the executable.", - CLI_EXECUTABLE_PROPERTY)); + throw new IllegalStateException(format("Failed to determine cli to use. Set Java system property %s to the executable.", CLI_EXECUTABLE_PROPERTY)); } } - + } diff --git a/main/ui/src/main/java/org/cryptomator/ui/util/command/Script.java b/main/ui/src/main/java/org/cryptomator/ui/util/command/Script.java index 1cb717a6c..0fb1c78c1 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/util/command/Script.java +++ b/main/ui/src/main/java/org/cryptomator/ui/util/command/Script.java @@ -15,65 +15,51 @@ import java.util.concurrent.TimeUnit; import org.cryptomator.ui.util.mount.CommandFailedException; public final class Script { - + private static final int DEFAULT_TIMEOUT_MILLISECONDS = 3000; - - public static Script fromLines(String ... commands) { + + public static Script fromLines(String... commands) { return new Script(commands); } - + private final String[] lines; - private final Map environment = new HashMap<>(); - private long timeout = DEFAULT_TIMEOUT_MILLISECONDS; - private TimeUnit timeoutUnit = TimeUnit.MILLISECONDS; - + private final Map environment = new HashMap<>(); + private Script(String[] lines) { this.lines = lines; setEnv(System.getenv()); } - + public String[] getLines() { return lines; } - + public CommandResult execute() throws CommandFailedException { - return CommandRunner.execute(this); + return CommandRunner.execute(this, DEFAULT_TIMEOUT_MILLISECONDS, TimeUnit.MILLISECONDS); } - - Map environment() { + + public CommandResult execute(long timeout, TimeUnit unit) throws CommandFailedException { + return CommandRunner.execute(this, timeout, unit); + } + + Map environment() { return environment; } - - public Script setEnv(Map environment) { + + public Script setEnv(Map environment) { this.environment.clear(); addEnv(environment); return this; } - - public Script addEnv(Map environment) { + + public Script addEnv(Map environment) { this.environment.putAll(environment); return this; } - + public Script addEnv(String name, String value) { environment.put(name, value); return this; } - public long getTimeout() { - return timeout; - } - - public void setTimeout(long timeout) { - this.timeout = timeout; - } - - public TimeUnit getTimeoutUnit() { - return timeoutUnit; - } - - public void setTimeoutUnit(TimeUnit timeoutUnit) { - this.timeoutUnit = timeoutUnit; - } - } diff --git a/main/ui/src/main/java/org/cryptomator/ui/util/mount/WindowsWebDavMounter.java b/main/ui/src/main/java/org/cryptomator/ui/util/mount/WindowsWebDavMounter.java index 8d94b8d99..2da099274 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/util/mount/WindowsWebDavMounter.java +++ b/main/ui/src/main/java/org/cryptomator/ui/util/mount/WindowsWebDavMounter.java @@ -29,7 +29,7 @@ import org.cryptomator.ui.util.command.Script; final class WindowsWebDavMounter implements WebDavMounterStrategy { private static final Pattern WIN_MOUNT_DRIVELETTER_PATTERN = Pattern.compile("\\s*([A-Z]:)\\s*"); - + @Override public boolean shouldWork() { return SystemUtils.IS_OS_WINDOWS; @@ -37,14 +37,10 @@ final class WindowsWebDavMounter implements WebDavMounterStrategy { @Override public WebDavMount mount(int localPort) throws CommandFailedException { - final Script mountScript = fromLines("net use * http://0--1.ipv6-literal.net:%PORT% /persistent:no") - .addEnv("PORT", String.valueOf(localPort)); - mountScript.setTimeout(30); - mountScript.setTimeoutUnit(TimeUnit.SECONDS); - final CommandResult mountResult = mountScript.execute(); + final Script mountScript = fromLines("net use * http://0--1.ipv6-literal.net:%PORT% /persistent:no").addEnv("PORT", String.valueOf(localPort)); + final CommandResult mountResult = mountScript.execute(30, TimeUnit.SECONDS); final String driveLetter = getDriveLetter(mountResult.getOutput()); - final Script unmountScript = fromLines("net use "+driveLetter+" /delete") - .addEnv("DRIVE_LETTER", driveLetter); + final Script unmountScript = fromLines("net use " + driveLetter + " /delete").addEnv("DRIVE_LETTER", driveLetter); return new WebDavMount() { @Override public void unmount() throws CommandFailedException {