Refactored script execution

This commit is contained in:
Markus Kreusch
2014-12-14 21:38:16 +01:00
parent a2c3b38a75
commit c1f4ab6ada
10 changed files with 405 additions and 64 deletions

View File

@@ -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) {

View File

@@ -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");
}
}

View File

@@ -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;
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -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())));
}
}
}

View File

@@ -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;
/**
* <p>
* Runs commands using a system compatible CLI.
* <p>
* To detect the system type {@link SystemUtils} is used. The following CLIs are
* used by default:
* <ul>
* <li><i>{@link #WINDOWS_DEFAULT_CLI}</i> if {@link SystemUtils#IS_OS_WINDOWS}
* <li><i>{@link #UNIX_DEFAULT_CLI}</i> if {@link SystemUtils#IS_OS_UNIX}
* </ul>
* <p>
* 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.
* <p>
* 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));
}
}
}

View File

@@ -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<String,String> 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<String,String> environment() {
return environment;
}
public Script setEnv(Map<String,String> environment) {
this.environment.clear();
addEnv(environment);
return this;
}
public Script addEnv(Map<String,String> environment) {
this.environment.putAll(environment);
return this;
}
public Script addEnv(String name, String value) {
environment.put(name, value);
return this;
}
}

View File

@@ -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();
}
};
}

View File

@@ -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();
}
};
}

View File

@@ -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.");
}