From c1f4ab6adad753df66d7e6138941ecfd5ca341cd Mon Sep 17 00:00:00 2001 From: Markus Kreusch Date: Sun, 14 Dec 2014 21:38:16 +0100 Subject: [PATCH] Refactored script execution --- .../org/cryptomator/ui/model/Directory.java | 2 +- .../org/cryptomator/ui/util/CommandUtil.java | 44 ------- .../ui/util/command/AsyncLineWriter.java | 51 ++++++++ .../ui/util/command/AsyncStreamCopier.java | 51 ++++++++ .../ui/util/command/CommandResult.java | 113 ++++++++++++++++++ .../ui/util/command/CommandRunner.java | 77 ++++++++++++ .../cryptomator/ui/util/command/Script.java | 58 +++++++++ .../util/webdav/LinuxGvfsWebDavMounter.java | 30 +++-- .../ui/util/webdav/MacOsXWebDavMounter.java | 20 +++- .../ui/util/webdav/WindowsWebDavMounter.java | 23 +++- 10 files changed, 405 insertions(+), 64 deletions(-) delete mode 100644 main/ui/src/main/java/org/cryptomator/ui/util/CommandUtil.java create mode 100644 main/ui/src/main/java/org/cryptomator/ui/util/command/AsyncLineWriter.java create mode 100644 main/ui/src/main/java/org/cryptomator/ui/util/command/AsyncStreamCopier.java create mode 100644 main/ui/src/main/java/org/cryptomator/ui/util/command/CommandResult.java create mode 100644 main/ui/src/main/java/org/cryptomator/ui/util/command/CommandRunner.java create mode 100644 main/ui/src/main/java/org/cryptomator/ui/util/command/Script.java diff --git a/main/ui/src/main/java/org/cryptomator/ui/model/Directory.java b/main/ui/src/main/java/org/cryptomator/ui/model/Directory.java index d1245188e..e172ac994 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/model/Directory.java +++ b/main/ui/src/main/java/org/cryptomator/ui/model/Directory.java @@ -70,7 +70,7 @@ public class Directory implements Serializable { public boolean mount() { try { - URI shareUri = URI.create(String.format("dav://localhost:", server.getPort())); + URI shareUri = URI.create(String.format("dav://localhost:%d", server.getPort())); webDavMount = WebDavMounter.mount(shareUri); return true; } catch (CommandFailedException e) { diff --git a/main/ui/src/main/java/org/cryptomator/ui/util/CommandUtil.java b/main/ui/src/main/java/org/cryptomator/ui/util/CommandUtil.java deleted file mode 100644 index 536909bee..000000000 --- a/main/ui/src/main/java/org/cryptomator/ui/util/CommandUtil.java +++ /dev/null @@ -1,44 +0,0 @@ -/******************************************************************************* - * Copyright (c) 2014 Sebastian Stenzel, Markus Kreusch - * This file is licensed under the terms of the MIT license. - * See the LICENSE.txt file for more info. - * - * Contributors: - * Sebastian Stenzel - initial API and implementation - * Markus Kreusch - ******************************************************************************/ -package org.cryptomator.ui.util; - -import static java.lang.String.join; - -import java.io.IOException; -import java.util.concurrent.TimeUnit; - -import org.apache.commons.io.IOUtils; -import org.cryptomator.ui.util.webdav.CommandFailedException; - -public final class CommandUtil { - - private static final int DEFAULT_TIMEOUT_SECONDS = 3; - - public static String exec(String ... command) throws CommandFailedException { - try { - final Process proc = Runtime.getRuntime().exec(command); - if (!proc.waitFor(DEFAULT_TIMEOUT_SECONDS, TimeUnit.SECONDS)) { - proc.destroy(); - throw new CommandFailedException("Timeout executing command " + join(" ", command)); - } - if (proc.exitValue() != 0) { - throw new CommandFailedException(IOUtils.toString(proc.getErrorStream())); - } - return IOUtils.toString(proc.getInputStream()); - } catch (IOException | InterruptedException | IllegalThreadStateException e) { - throw new CommandFailedException(e); - } - } - - private CommandUtil() { - throw new IllegalStateException("Class is not instantiable"); - } - -} diff --git a/main/ui/src/main/java/org/cryptomator/ui/util/command/AsyncLineWriter.java b/main/ui/src/main/java/org/cryptomator/ui/util/command/AsyncLineWriter.java new file mode 100644 index 000000000..64a202ffb --- /dev/null +++ b/main/ui/src/main/java/org/cryptomator/ui/util/command/AsyncLineWriter.java @@ -0,0 +1,51 @@ +/******************************************************************************* + * Copyright (c) 2014 Markus Kreusch + * This file is licensed under the terms of the MIT license. + * See the LICENSE.txt file for more info. + * + * Contributors: + * Markus Kreusch + ******************************************************************************/ +package org.cryptomator.ui.util.command; + +import java.io.IOException; +import java.io.OutputStream; + +final class AsyncLineWriter extends Thread { + + private final String[] lines; + private final OutputStream output; + + private IOException exception; + + public AsyncLineWriter(String[] lines, OutputStream output) { + this.lines = lines; + this.output = output; + start(); + } + + @Override + public void run() { + try (OutputStream outputToBeClosed = output) { + for (String line : lines) { + output.write(line.getBytes()); + output.write("\n".getBytes()); + output.flush(); + } + } catch (IOException e) { + exception = e; + } + } + + public void assertOk() throws IOException { + try { + join(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + if (exception != null) { + throw exception; + } + } + +} 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 new file mode 100644 index 000000000..e490646ae --- /dev/null +++ b/main/ui/src/main/java/org/cryptomator/ui/util/command/AsyncStreamCopier.java @@ -0,0 +1,51 @@ +/******************************************************************************* + * Copyright (c) 2014 Markus Kreusch + * This file is licensed under the terms of the MIT license. + * See the LICENSE.txt file for more info. + * + * Contributors: + * Markus Kreusch + ******************************************************************************/ +package org.cryptomator.ui.util.command; + +import static org.apache.commons.io.IOUtils.copy; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +final class AsyncStreamCopier extends Thread { + + private final InputStream input; + private final OutputStream output; + + private IOException exception; + + public AsyncStreamCopier(InputStream input, OutputStream output) { + this.input = input; + this.output = output; + start(); + } + + @Override + public void run() { + try (InputStream inputToBeClosed = input; + OutputStream outputToBeClosed = output) { + copy(input, output); + } catch (IOException e) { + exception = e; + } + } + + public void assertOk() throws IOException { + try { + join(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + 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 new file mode 100644 index 000000000..d5c347c47 --- /dev/null +++ b/main/ui/src/main/java/org/cryptomator/ui/util/command/CommandResult.java @@ -0,0 +1,113 @@ +/******************************************************************************* + * Copyright (c) 2014 Markus Kreusch + * This file is licensed under the terms of the MIT license. + * See the LICENSE.txt file for more info. + * + * Contributors: + * Markus Kreusch + ******************************************************************************/ +package org.cryptomator.ui.util.command; + +import static java.lang.String.format; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.concurrent.TimeUnit; + +import org.cryptomator.ui.util.webdav.CommandFailedException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class CommandResult { + + private static final int DEFAULT_TIMEOUT_MILLISECONDS = 10000; + + private static final Logger LOG = LoggerFactory.getLogger(CommandResult.class); + + private final ByteArrayOutputStream output = new ByteArrayOutputStream(); + private final ByteArrayOutputStream error = new ByteArrayOutputStream(); + private final Process process; + + private final AsyncStreamCopier processOutputCopier; + private final AsyncStreamCopier processErrorCopier; + + private boolean finished; + + public CommandResult(Process process, String[] lines) { + this.process = process; + new AsyncLineWriter(lines, process.getOutputStream()); + processOutputCopier = new AsyncStreamCopier(process.getInputStream(), output); + processErrorCopier = new AsyncStreamCopier(process.getErrorStream(), error); + } + + public String getOutput() throws CommandFailedException { + return getOutput(DEFAULT_TIMEOUT_MILLISECONDS, TimeUnit.MICROSECONDS); + } + + public String getError() throws CommandFailedException { + return getError(DEFAULT_TIMEOUT_MILLISECONDS, TimeUnit.MICROSECONDS); + } + + public String getOutput(long timeout, TimeUnit unit) throws CommandFailedException { + waitAndAssertOk(timeout, unit); + return new String(output.toByteArray()); + } + + public String getError(long timeout, TimeUnit unit) throws CommandFailedException { + waitAndAssertOk(timeout, unit); + return new String(error.toByteArray()); + } + + public int getExitValue(long timeout, TimeUnit unit) throws CommandFailedException { + waitAndAssertOk(timeout, unit); + return process.exitValue(); + } + + private void waitAndAssertOk(long timeout, TimeUnit unit) throws CommandFailedException { + if (finished) return; + try { + if (!process.waitFor(timeout, unit)) { + throw new CommandFailedException("Waiting time elapsed before command execution finished"); + } + processOutputCopier.assertOk(); + processErrorCopier.assertOk(); + finished = true; + logDebugInfo(); + } catch (IOException | InterruptedException e) { + throw new CommandFailedException(e); + } + } + + 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())); + } + } + + public void assertOk() throws CommandFailedException { + assertOk(DEFAULT_TIMEOUT_MILLISECONDS, TimeUnit.MILLISECONDS); + } + + public void assertOk(long timeout, TimeUnit unit) throws CommandFailedException { + int exitValue = getExitValue(timeout, unit); + 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()), + new String(error.toByteArray()))); + } + } + +} 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 new file mode 100644 index 000000000..9ccadd7da --- /dev/null +++ b/main/ui/src/main/java/org/cryptomator/ui/util/command/CommandRunner.java @@ -0,0 +1,77 @@ +/******************************************************************************* + * Copyright (c) 2014 Markus Kreusch + * This file is licensed under the terms of the MIT license. + * See the LICENSE.txt file for more info. + * + * Contributors: + * Markus Kreusch + ******************************************************************************/ +package org.cryptomator.ui.util.command; + +import static java.lang.String.format; +import static org.apache.commons.lang3.SystemUtils.IS_OS_UNIX; +import static org.apache.commons.lang3.SystemUtils.IS_OS_WINDOWS; + +import java.io.IOException; + +import org.apache.commons.lang3.SystemUtils; +import org.cryptomator.ui.util.webdav.CommandFailedException; + +/** + *

+ * Runs commands using a system compatible CLI. + *

+ * 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 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 { + + public static final String CLI_EXECUTABLE_PROPERTY = "cryptomator.cli"; + + public static final String WINDOWS_DEFAULT_CLI[] = {"cmd"}; + public static final String UNIX_DEFAULT_CLI[] = {"/bin/sh", "-e"}; + + static CommandResult execute(Script script) throws CommandFailedException { + ProcessBuilder builder = new ProcessBuilder(determineCli()); + builder.environment().clear(); + builder.environment().putAll(script.environment()); + try { + return run(builder.start(), script.getLines()); + } catch (IOException e) { + throw new CommandFailedException(e); + } + } + + private static CommandResult run(Process process, String[] lines) { + return new CommandResult(process, lines); + } + + private static String[] determineCli() { + final String cliFromProperty = System.getProperty(CLI_EXECUTABLE_PROPERTY); + if (cliFromProperty != null) { + return cliFromProperty.split(""); + } else if (IS_OS_WINDOWS) { + return WINDOWS_DEFAULT_CLI; + } 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)); + } + } + +} 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 new file mode 100644 index 000000000..4417f0281 --- /dev/null +++ b/main/ui/src/main/java/org/cryptomator/ui/util/command/Script.java @@ -0,0 +1,58 @@ +/******************************************************************************* + * Copyright (c) 2014 Markus Kreusch + * This file is licensed under the terms of the MIT license. + * See the LICENSE.txt file for more info. + * + * Contributors: + * Markus Kreusch + ******************************************************************************/ +package org.cryptomator.ui.util.command; + +import java.util.HashMap; +import java.util.Map; + +import org.cryptomator.ui.util.webdav.CommandFailedException; + +public final class Script { + + public static Script fromLines(String ... commands) { + return new Script(commands); + } + + private final String[] lines; + 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); + } + + Map environment() { + return environment; + } + + public Script setEnv(Map environment) { + this.environment.clear(); + addEnv(environment); + return this; + } + + public Script addEnv(Map environment) { + this.environment.putAll(environment); + return this; + } + + public Script addEnv(String name, String value) { + environment.put(name, value); + return this; + } + +} diff --git a/main/ui/src/main/java/org/cryptomator/ui/util/webdav/LinuxGvfsWebDavMounter.java b/main/ui/src/main/java/org/cryptomator/ui/util/webdav/LinuxGvfsWebDavMounter.java index 65e95895c..766bdb1b0 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/util/webdav/LinuxGvfsWebDavMounter.java +++ b/main/ui/src/main/java/org/cryptomator/ui/util/webdav/LinuxGvfsWebDavMounter.java @@ -9,28 +9,44 @@ ******************************************************************************/ package org.cryptomator.ui.util.webdav; -import static org.cryptomator.ui.util.CommandUtil.exec; - import java.net.URI; import org.apache.commons.lang3.SystemUtils; +import org.cryptomator.ui.util.command.Script; final class LinuxGvfsWebDavMounter implements WebDavMounterStrategy { @Override public boolean shouldWork() { - // TODO check result of "which gvfs-mount" - return SystemUtils.IS_OS_LINUX; + if (SystemUtils.IS_OS_LINUX) { + final Script checkScripts = Script.fromLines("which gvfs-mount xdg-open"); + try { + checkScripts.execute().assertOk(); + return true; + } catch (CommandFailedException e) { + return false; + } + } else { + return false; + } } @Override public WebDavMount mount(final URI uri) throws CommandFailedException { - exec("gvfs-mount", uri.toString()); - exec("xdg-open", uri.toString()); + final Script mountScript = Script.fromLines( + "set -x", + "gvfs-mount \"$URI\"", + "xdg-open \"$URI\"") + .addEnv("URI", uri.toString()); + final Script unmountScript = Script.fromLines( + "set -x", + "gvfs-mount -u \"$URI\"") + .addEnv("URI", uri.toString()); + mountScript.execute().assertOk(); return new WebDavMount() { @Override public void unmount() throws CommandFailedException { - exec("gvfs-mount", "-u", uri.toString()); + unmountScript.execute().assertOk(); } }; } diff --git a/main/ui/src/main/java/org/cryptomator/ui/util/webdav/MacOsXWebDavMounter.java b/main/ui/src/main/java/org/cryptomator/ui/util/webdav/MacOsXWebDavMounter.java index a51ba0d98..c8ef2e6dc 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/util/webdav/MacOsXWebDavMounter.java +++ b/main/ui/src/main/java/org/cryptomator/ui/util/webdav/MacOsXWebDavMounter.java @@ -9,11 +9,10 @@ ******************************************************************************/ package org.cryptomator.ui.util.webdav; -import static org.cryptomator.ui.util.CommandUtil.exec; - import java.net.URI; import org.apache.commons.lang3.SystemUtils; +import org.cryptomator.ui.util.command.Script; final class MacOsXWebDavMounter implements WebDavMounterStrategy { @@ -25,13 +24,22 @@ final class MacOsXWebDavMounter implements WebDavMounterStrategy { @Override public WebDavMount mount(URI uri) throws CommandFailedException { final String path = "/Volumes/Cryptomator" + uri.getPort(); - exec("mkdir", "/Volumes/Cryptomator" + uri.getPort()); - exec("mount_webdav", "-S", "-v", "Cryptomator", uri.toString(), path); - exec("open", path); + final Script mountScript = Script.fromLines( + "set -x", + "mkdir \"$MOUNT_PATH\"", + "mount_webdav -S -v Cryptomator \"$URI\" \"$MOUNT_PATH\"", + "open \"$MOUNT_PATH\"") + .addEnv("URI", uri.toString()) + .addEnv("MOUNT_PATH", path); + final Script unmountScript = Script.fromLines( + "set -x", + "unmount $MOUNT_PATH") + .addEnv("MOUNT_PATH", path); + mountScript.execute().assertOk(); return new WebDavMount() { @Override public void unmount() throws CommandFailedException { - exec("unmount", path); + unmountScript.execute().assertOk(); } }; } diff --git a/main/ui/src/main/java/org/cryptomator/ui/util/webdav/WindowsWebDavMounter.java b/main/ui/src/main/java/org/cryptomator/ui/util/webdav/WindowsWebDavMounter.java index f1e956c55..8dfb74cab 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/util/webdav/WindowsWebDavMounter.java +++ b/main/ui/src/main/java/org/cryptomator/ui/util/webdav/WindowsWebDavMounter.java @@ -10,7 +10,7 @@ package org.cryptomator.ui.util.webdav; import static java.lang.String.format; -import static org.cryptomator.ui.util.CommandUtil.exec; +import static org.cryptomator.ui.util.command.Script.fromLines; import java.net.URI; import java.net.URISyntaxException; @@ -18,6 +18,8 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; import org.apache.commons.lang3.SystemUtils; +import org.cryptomator.ui.util.command.CommandResult; +import org.cryptomator.ui.util.command.Script; /** * A {@link WebDavMounterStrategy} utilizing the "net use" command. @@ -28,7 +30,7 @@ import org.apache.commons.lang3.SystemUtils; */ final class WindowsWebDavMounter implements WebDavMounterStrategy { - private static final Pattern WIN_MOUNT_DRIVELETTER_PATTERN = Pattern.compile("\\s*[A-Z]:\\s*"); + private static final Pattern WIN_MOUNT_DRIVELETTER_PATTERN = Pattern.compile("Laufwerk\\s*([A-Z]:)\\s*ist"); @Override public boolean shouldWork() { @@ -37,12 +39,21 @@ final class WindowsWebDavMounter implements WebDavMounterStrategy { @Override public WebDavMount mount(URI uri) throws CommandFailedException { - final String result = exec("net", "use", "*", toHttpUri(uri), "/persistent:no"); - final String driveLetter = getDriveLetter(result); + final Script mountScript = fromLines( + "net use * %URI% /persistent:no", + "if %errorLevel% neq 0 exit %errorLevel%") + .addEnv("URI", toHttpUri(uri)); + final CommandResult mountResult = mountScript.execute(); + mountResult.assertOk(); + final String driveLetter = getDriveLetter(mountResult.getOutput()); + final Script unmountScript = fromLines( + "net use "+driveLetter+" /delete", + "if %errorLevel% neq 0 exit %errorLevel%") + .addEnv("DRIVE_LETTER", driveLetter); return new WebDavMount() { @Override public void unmount() throws CommandFailedException { - exec("net", "use", driveLetter, "/delete"); + unmountScript.execute().assertOk(); } }; } @@ -50,7 +61,7 @@ final class WindowsWebDavMounter implements WebDavMounterStrategy { private String getDriveLetter(String result) throws CommandFailedException { final Matcher matcher = WIN_MOUNT_DRIVELETTER_PATTERN.matcher(result); if (matcher.find()) { - return matcher.group(); + return matcher.group(1); } else { throw new CommandFailedException("Failed to get a drive letter from net use output."); }