attach-javadocs
diff --git a/src/se/vidstige/jadb/JadbDevice.java b/src/se/vidstige/jadb/JadbDevice.java
index a16c648..7e56e25 100644
--- a/src/se/vidstige/jadb/JadbDevice.java
+++ b/src/se/vidstige/jadb/JadbDevice.java
@@ -57,7 +57,7 @@ public class JadbDevice {
}
}
- private Transport getTransport() throws IOException, JadbException {
+ Transport getTransport() throws IOException, JadbException {
Transport transport = transportFactory.createTransport();
// Do not use try-with-resources here. We want to return unclosed Transport and it is up to caller
// to close it. Here we close it only in case of exception.
@@ -116,6 +116,19 @@ public class JadbDevice {
}
}
+ /** Execute a shell command.
+ *
+ * This method supports separate stdin, stdout, and stderr streams, as well as a return code. The shell command
+ * is not executed until calling {@link ShellProcessBuilder#start()}, which returns a {@link Process}.
+ *
+ * @param command main command to run, e.g. "screencap"
+ * @param args arguments to the command, e.g. "-p".
+ * @return a {@link ShellProcessBuilder}
+ */
+ public ShellProcessBuilder shellProcessBuilder(String command, String... args) {
+ return new ShellProcessBuilder(this, buildCmdLine(command, args).toString());
+ }
+
/** Execute a command with raw binary output.
*
* Support for this command was added in Lollipop (Android 5.0), and is the recommended way to transmit binary
diff --git a/src/se/vidstige/jadb/ShellProcess.java b/src/se/vidstige/jadb/ShellProcess.java
new file mode 100644
index 0000000..94f58f3
--- /dev/null
+++ b/src/se/vidstige/jadb/ShellProcess.java
@@ -0,0 +1,111 @@
+package se.vidstige.jadb;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.concurrent.*;
+
+public class ShellProcess extends Process {
+
+ private static final int KILLED_STATUS_CODE = 9;
+ private final OutputStream outputStream;
+ private final InputStream inputStream;
+ private final InputStream errorStream;
+ private final Future exitCodeFuture;
+ private final ShellProtocolTransport transport;
+ private Integer exitCode = null;
+
+ ShellProcess(OutputStream outputStream, InputStream inputStream, InputStream errorStream, Future exitCodeFuture, ShellProtocolTransport transport) {
+ this.outputStream = outputStream;
+ this.inputStream = inputStream;
+ this.errorStream = errorStream;
+ this.exitCodeFuture = exitCodeFuture;
+ this.transport = transport;
+ }
+
+ @Override
+ public OutputStream getOutputStream() {
+ return outputStream;
+ }
+
+ @Override
+ public InputStream getInputStream() {
+ return inputStream;
+ }
+
+ @Override
+ public InputStream getErrorStream() {
+ return errorStream;
+ }
+
+ @Override
+ public int waitFor() throws InterruptedException {
+ if (exitCode == null) {
+ try {
+ exitCode = exitCodeFuture.get();
+ } catch (CancellationException e) {
+ exitCode = KILLED_STATUS_CODE;
+ } catch (ExecutionException e) {
+ throw new RuntimeException(e);
+ }
+ }
+ return exitCode;
+ }
+
+ /* For 1.8 */
+ public boolean waitFor(long timeout, TimeUnit unit) throws InterruptedException {
+ if (exitCode == null) {
+ try {
+ exitCode = exitCodeFuture.get(timeout, unit);
+ } catch (CancellationException e) {
+ exitCode = KILLED_STATUS_CODE;
+ } catch (ExecutionException e) {
+ throw new RuntimeException(e);
+ } catch (TimeoutException e) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ @Override
+ public int exitValue() {
+ if (exitCode != null) {
+ return exitCode;
+ }
+ if (exitCodeFuture.isDone()) {
+ try {
+ exitCode = exitCodeFuture.get(0, TimeUnit.SECONDS);
+ return exitCode;
+ } catch (CancellationException e) {
+ exitCode = KILLED_STATUS_CODE;
+ } catch (ExecutionException e) {
+ throw new RuntimeException(e);
+ } catch (TimeoutException e) {
+ // fallthrough, but should never happen
+ } catch (InterruptedException e) {
+ // fallthrough, but should never happen
+ Thread.currentThread().interrupt();
+ }
+ }
+ throw new IllegalThreadStateException();
+ }
+
+ @Override
+ public void destroy() {
+ if (isAlive()) {
+ try {
+ exitCodeFuture.cancel(true);
+ // interrupt (usually) doesn't work for blocking read -- this will cause SocketException
+ transport.close();
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+ }
+
+ /* For 1.8 */
+ public boolean isAlive() {
+ return !exitCodeFuture.isDone();
+ }
+}
\ No newline at end of file
diff --git a/src/se/vidstige/jadb/ShellProcessBuilder.java b/src/se/vidstige/jadb/ShellProcessBuilder.java
new file mode 100644
index 0000000..bc1fd26
--- /dev/null
+++ b/src/se/vidstige/jadb/ShellProcessBuilder.java
@@ -0,0 +1,242 @@
+package se.vidstige.jadb;
+
+import java.io.*;
+import java.nio.file.Files;
+import java.nio.file.StandardOpenOption;
+import java.util.concurrent.*;
+
+/**
+ * A builder of a {@link Process} corresponding to an ADB shell command.
+ *
+ * This builder allows for configuration of the {@link Process}'s output and error streams as well as the
+ * {@link Executor} to use when starting the shell process. The output and error streams may be either be redirected
+ * (using {@link java.lang.ProcessBuilder.Redirect}) or given an explicit {@link OutputStream}. You may also combine
+ * the output and error streams via {@link #redirectErrorStream(boolean) redirectErrorStream(true)}.
+ *
+ * Use {@link #start()} to execute the command, and then use {@link Process#waitFor()} to wait for the command to
+ * complete.
+ *
+ * Warning: If stdout and stderr are both set to {@link java.lang.ProcessBuilder.Redirect#PIPE} (the default),
+ * you must read from their InputStreams ({@link Process#getInputStream()} and {@link Process#getErrorStream()})
+ * concurrently. This requires having two separate threads to read the input streams separately. Otherwise,
+ * the process may deadlock. To avoid using threads, you can use {@link #redirectErrorStream(boolean)}, in which case
+ * you must read all output from {@link Process#getInputStream()} before calling {@link Process#waitFor()}:
+ *
+ *
{@code
+ * Process process = jadbDevice.shellProcessBuilder("command")
+ * .redirectErrorStream(errorStream)
+ * .start();
+ * String stdoutAndStderr = new Scanner(process.getInputStream()).useDelimiter("\\A").next();
+ * int exitCode = process.waitFor();
+ * }
+ *
+ * You can also use one of the {@code redirectOutput} methods to have the output automatically redirected. For example,
+ * to buffer all of stdout and stderr separately, you can use {@link java.io.ByteArrayOutputStream}:
+ *
+ *
{@code
+ * ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+ * ByteArrayOutputStream errorStream = new ByteArrayOutputStream();
+ * Process process = jadbDevice.shellProcessBuilder("command")
+ * .redirectOutput(outputStream)
+ * .redirectError(errorStream)
+ * .start();
+ * int exitCode = process.waitFor();
+ * String stdout = outputStream.toString(StandardCharsets.UTF_8.name());
+ * String stderr = errorStream.toString(StandardCharsets.UTF_8.name());
+ * }
+ */
+public class ShellProcessBuilder {
+
+ private JadbDevice device;
+ private String command;
+ private ProcessBuilder.Redirect outRedirect = ProcessBuilder.Redirect.PIPE;
+ private OutputStream outOs = null;
+ private ProcessBuilder.Redirect errRedirect = ProcessBuilder.Redirect.PIPE;
+ private OutputStream errOs = null;
+ private boolean redirectErrorStream;
+ private Executor executor = null;
+
+ ShellProcessBuilder(JadbDevice device, String command) {
+ this.device = device;
+ this.command = command;
+ }
+
+ private void checkValidForWrite(ProcessBuilder.Redirect destination) {
+ if (destination.type() == ProcessBuilder.Redirect.Type.READ) {
+ throw new IllegalArgumentException("Redirect invalid for writing: " + destination);
+ }
+ }
+
+ /**
+ * Redirect stdout to the given destination. If set to anything other than
+ * {@link java.lang.ProcessBuilder.Redirect#PIPE} (the default), {@link Process#getInputStream()} does nothing.
+ *
+ * @param destination where to redirect
+ * @return this
+ */
+ public ShellProcessBuilder redirectOutput(ProcessBuilder.Redirect destination) {
+ checkValidForWrite(destination);
+ outRedirect = destination;
+ outOs = null;
+ return this;
+ }
+
+ /**
+ * Redirect stdout directly to the given output stream.
+ * Note: this output steam will be called from a separate thread.
+ *
+ * @param destination OutputStream to write
+ * @return this
+ */
+ public ShellProcessBuilder redirectOutput(OutputStream destination) {
+ outRedirect = null;
+ outOs = destination;
+ return this;
+ }
+
+ /**
+ * Redirect stderr to the given destination. If set to anything other than
+ * {@link java.lang.ProcessBuilder.Redirect#PIPE} (the default), {@link Process#getErrorStream()} does nothing.
+ *
+ * @param destination where to redirect
+ * @return this
+ */
+ public ShellProcessBuilder redirectError(ProcessBuilder.Redirect destination) {
+ checkValidForWrite(destination);
+ errRedirect = destination;
+ errOs = null;
+ return this;
+ }
+
+ /**
+ * Redirect stderr directly to the given output stream.
+ * Note: this output steam will be called from a separate thread.
+ *
+ * @param destination OutputStream to write
+ * @return this
+ */
+ public ShellProcessBuilder redirectError(OutputStream destination) {
+ errRedirect = null;
+ errOs = destination;
+ return this;
+ }
+
+ /**
+ * Set redirecting of the error stream directly to the output stream. If set, any {@code redirectError} calls are
+ * ignored, and the returned Process
+ *
+ * @param redirectErrorStream true to enable redirecting of the error stream
+ * @return this
+ */
+ public ShellProcessBuilder redirectErrorStream(boolean redirectErrorStream) {
+ this.redirectErrorStream = redirectErrorStream;
+ return this;
+ }
+
+ /**
+ * Set the {@link Executor} to use to run the process handling thread. If not set, uses
+ * {@link Executors#newSingleThreadExecutor()}.
+ *
+ * @param executor An executor
+ * @return this
+ */
+ public ShellProcessBuilder useExecutor(Executor executor) {
+ this.executor = executor;
+ return this;
+ }
+
+ /**
+ * Starts the shell command.
+ *
+ * @return a {@link Process}
+ * @throws IOException
+ * @throws JadbException
+ */
+ public ShellProcess start() throws IOException, JadbException {
+ Transport transport = null;
+ try {
+ final OutputStream outOs = getOutputStream(this.outOs, this.outRedirect, System.out);
+ InputStream outIs = getConnectedPipe(outOs);
+
+ final OutputStream errOs;
+ InputStream errIs;
+ if (redirectErrorStream) {
+ errOs = outOs;
+ errIs = NullInputStream.INSTANCE;
+ } else {
+ errOs = getOutputStream(this.errOs, this.errRedirect, System.err);
+ errIs = getConnectedPipe(errOs);
+ }
+
+ transport = device.getTransport();
+ final ShellProtocolTransport shellProtocolTransport = transport.startShellProtocol(this.command);
+ OutputStream inOs = shellProtocolTransport.getOutputStream();
+
+ FutureTask transportTask = new FutureTask<>(new Callable() {
+ @Override
+ public Integer call() throws Exception {
+ try (ShellProtocolTransport unused1 = shellProtocolTransport; OutputStream unused2 = outOs; OutputStream unused3 = errOs) {
+ return shellProtocolTransport.demuxOutput(outOs, errOs);
+ }
+ }
+ });
+
+ if (executor == null) {
+ ExecutorService service = Executors.newSingleThreadExecutor();
+ service.execute(transportTask);
+ service.shutdown();
+ } else {
+ executor.execute(transportTask);
+ }
+
+ return new ShellProcess(inOs, outIs, errIs, transportTask, shellProtocolTransport);
+ } catch (IOException | JadbException | RuntimeException e) {
+ if (transport != null) {
+ transport.close();
+ }
+ throw e;
+ }
+ }
+
+ private OutputStream getOutputStream(OutputStream os, ProcessBuilder.Redirect destination, OutputStream inherit) throws IOException {
+ if (os != null) {
+ return os;
+ }
+ switch (destination.type()) {
+ case PIPE:
+ return new PipedOutputStream();
+ case INHERIT:
+ return inherit;
+ case READ:
+ throw new IllegalArgumentException("Redirect invalid for writing: " + destination);
+ case WRITE:
+ return Files.newOutputStream(destination.file().toPath());
+ case APPEND:
+ return Files.newOutputStream(destination.file().toPath(), StandardOpenOption.CREATE, StandardOpenOption.APPEND, StandardOpenOption.WRITE);
+ default:
+ throw new IllegalArgumentException("Unknown redirect type: " + destination);
+ }
+ }
+
+ private InputStream getConnectedPipe(OutputStream os) throws IOException {
+ if (os instanceof PipedOutputStream) {
+ return new PipedInputStream((PipedOutputStream) os);
+ }
+ return NullInputStream.INSTANCE;
+ }
+
+ static class NullInputStream extends InputStream {
+ static final NullInputStream INSTANCE = new NullInputStream();
+
+ private NullInputStream() {
+ }
+
+ public int read() {
+ return -1;
+ }
+
+ public int available() {
+ return 0;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/se/vidstige/jadb/ShellProtocolTransport.java b/src/se/vidstige/jadb/ShellProtocolTransport.java
new file mode 100644
index 0000000..4ccd281
--- /dev/null
+++ b/src/se/vidstige/jadb/ShellProtocolTransport.java
@@ -0,0 +1,154 @@
+package se.vidstige.jadb;
+
+import java.io.*;
+
+class ShellProtocolTransport implements Closeable {
+ private final DataOutputStream output;
+ private final DataInputStream input;
+
+ ShellProtocolTransport(DataOutputStream outputStream, DataInputStream inputStream) {
+ output = outputStream;
+ input = inputStream;
+ }
+
+ // replace with Integer.toUnsignedLong in Java 8
+ private static long integerToUnsignedLong(int i) {
+ return ((long) (int) i) & 0xffffffffL;
+ }
+
+ // replace with Byte.toUnsignedInt in Java 8
+ private static int byteToUnsignedInt(byte b) {
+ return ((int) b) & 0xff;
+ }
+
+ private ShellMessageType readMessageType() throws IOException {
+ return ShellMessageType.fromId(input.readByte());
+ }
+
+ private long readDataLength() throws IOException {
+ return integerToUnsignedLong(Integer.reverseBytes(input.readInt()));
+ }
+
+ private void readDataTo(OutputStream out, long dataLength, byte[] buffer) throws IOException {
+ long remaining = dataLength;
+ while (remaining > 0) {
+ int len = (int) Math.min(remaining, buffer.length);
+ input.readFully(buffer, 0, len);
+ out.write(buffer, 0, len);
+ remaining -= len;
+ }
+ out.flush();
+ }
+
+ int demuxOutput(OutputStream outputStream, OutputStream errorStream) throws JadbException, IOException {
+ int exitCode = 0;
+ byte[] buf = new byte[256 * 1024];
+
+ try {
+ while (true) {
+ ShellMessageType messageType = readMessageType();
+ long length = readDataLength();
+ switch (messageType) {
+ case STDOUT:
+ readDataTo(outputStream, length, buf);
+ break;
+ case STDERR:
+ readDataTo(errorStream, length, buf);
+ break;
+ case EXIT:
+ if (length != 1) {
+ throw new JadbException("Expected only one byte for exitCode");
+ }
+ exitCode = byteToUnsignedInt(input.readByte());
+ break;
+ default:
+ // ignore;
+ break;
+ }
+ }
+ } catch (EOFException e) {
+ // ignore
+ }
+
+ return exitCode;
+ }
+
+ private void writeData(ShellMessageType type, byte[] buf, int off, int len) throws IOException {
+ output.writeByte(byteToUnsignedInt(type.getId()));
+ output.writeInt(Integer.reverseBytes(len));
+ output.write(buf, off, len);
+ }
+
+ OutputStream getOutputStream() {
+ return new ShellProtocolOutputStream(this);
+ }
+
+ @Override
+ public void close() throws IOException {
+ output.close();
+ input.close();
+ }
+
+ enum ShellMessageType {
+ STDIN((byte) 0), STDOUT((byte) 1), STDERR((byte) 2), EXIT((byte) 3), CLOSE_STDIN((byte) 4), WINDOW_SIZE_CHANGE((byte) 5), UNKNOWN(Byte.MIN_VALUE);
+
+ private final byte id;
+
+ ShellMessageType(byte id) {
+ this.id = id;
+ }
+
+ public static ShellMessageType fromId(byte b) {
+ switch (b) {
+ case 0:
+ return STDIN;
+ case 1:
+ return STDOUT;
+ case 2:
+ return STDERR;
+ case 3:
+ return EXIT;
+ case 4:
+ return CLOSE_STDIN;
+ case 5:
+ // unused
+ return WINDOW_SIZE_CHANGE;
+ default:
+ return UNKNOWN;
+ }
+ }
+
+ public byte getId() {
+ return id;
+ }
+ }
+
+ private static class ShellProtocolOutputStream extends OutputStream {
+
+ private final ShellProtocolTransport transport;
+
+ ShellProtocolOutputStream(ShellProtocolTransport transport) {
+ this.transport = transport;
+ }
+
+ @Override
+ public void write(int b) throws IOException {
+ write(new byte[]{(byte) b});
+ }
+
+ @Override
+ public void write(byte[] b, int off, int len) throws IOException {
+ transport.writeData(ShellMessageType.STDIN, b, off, len);
+ }
+
+ @Override
+ public void flush() throws IOException {
+ transport.output.flush();
+ }
+
+ @Override
+ public void close() throws IOException {
+ transport.writeData(ShellMessageType.CLOSE_STDIN, new byte[0], 0, 0);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/se/vidstige/jadb/Transport.java b/src/se/vidstige/jadb/Transport.java
index 72a828c..2d17b12 100644
--- a/src/se/vidstige/jadb/Transport.java
+++ b/src/se/vidstige/jadb/Transport.java
@@ -67,6 +67,12 @@ class Transport implements Closeable {
return new SyncTransport(dataOutput, dataInput);
}
+ public ShellProtocolTransport startShellProtocol(String command) throws IOException, JadbException {
+ send("shell,v2,raw:" + command);
+ verifyResponse();
+ return new ShellProtocolTransport(dataOutput, dataInput);
+ }
+
@Override
public void close() throws IOException {
dataInput.close();
diff --git a/src/se/vidstige/jadb/server/AdbProtocolHandler.java b/src/se/vidstige/jadb/server/AdbProtocolHandler.java
index dc27407..dc3b658 100644
--- a/src/se/vidstige/jadb/server/AdbProtocolHandler.java
+++ b/src/se/vidstige/jadb/server/AdbProtocolHandler.java
@@ -63,7 +63,7 @@ class AdbProtocolHandler implements Runnable {
hostTransport(output, command);
} else if ("sync:".equals(command)) {
sync(output, input);
- } else if (command.startsWith("shell:")) {
+ } else if (command.startsWith("shell")) {
shell(input, output, command);
return false;
} else if ("host:get-state".equals(command)) {
@@ -118,7 +118,7 @@ class AdbProtocolHandler implements Runnable {
}
private void shell(DataInput input, DataOutputStream output, String command) throws IOException {
- String shellCommand = command.substring("shell:".length());
+ String shellCommand = command.split(":", 2)[1];
output.writeBytes("OKAY");
shell(shellCommand, output, input);
}
diff --git a/test/se/vidstige/jadb/test/integration/RealDeviceTestCases.java b/test/se/vidstige/jadb/test/integration/RealDeviceTestCases.java
index 9913e9d..8e05c87 100644
--- a/test/se/vidstige/jadb/test/integration/RealDeviceTestCases.java
+++ b/test/se/vidstige/jadb/test/integration/RealDeviceTestCases.java
@@ -1,31 +1,26 @@
package se.vidstige.jadb.test.integration;
-import org.junit.Assert;
-import org.junit.Before;
-import org.junit.BeforeClass;
-import org.junit.Rule;
-import org.junit.Test;
+import org.junit.*;
import org.junit.rules.TemporaryFolder;
import se.vidstige.jadb.*;
-import java.io.ByteArrayOutputStream;
-import java.io.File;
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.io.InputStream;
+import java.io.*;
import java.net.InetSocketAddress;
+import java.nio.charset.StandardCharsets;
import java.util.List;
+import java.util.Scanner;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.*;
public class RealDeviceTestCases {
- private JadbConnection jadb;
-
@Rule
public TemporaryFolder temporaryFolder = new TemporaryFolder(); //Must be public
+ private JadbConnection jadb;
@BeforeClass
public static void tryToStartAdbServer() {
@@ -99,13 +94,74 @@ public class RealDeviceTestCases {
}
@SuppressWarnings("deprecation")
- @Test
+ @Test
public void testShellExecuteTwice() throws Exception {
JadbDevice any = jadb.getAnyDevice();
ByteArrayOutputStream bout = new ByteArrayOutputStream();
any.executeShell(bout, "ls /");
any.executeShell(bout, "ls", "-la", "/");
- System.out.write(bout.toByteArray());
+ byte[] buf = bout.toByteArray();
+ System.out.write(buf, 0, buf.length);
+ }
+
+ @Test
+ public void testShellProcessBuilderStart() throws Exception {
+ JadbDevice any = jadb.getAnyDevice();
+ Process process = any.shellProcessBuilder("ls /").start();
+ AtomicReference stdout = new AtomicReference<>();
+ AtomicReference stderr = new AtomicReference<>();
+ Thread thread1 = gobbler(process.getInputStream(), stdout);
+ Thread thread2 = gobbler(process.getErrorStream(), stderr);
+ thread1.start();
+ thread2.start();
+ process.waitFor();
+ thread1.join();
+ thread2.join();
+ System.out.println(stdout.get());
+ System.out.println(stderr.get());
+ }
+
+ private Thread gobbler(final InputStream stream, final AtomicReference out) {
+ return new Thread(new Runnable() {
+ @Override
+ public void run() {
+ out.set(new Scanner(stream).useDelimiter("\\A").next());
+ }
+ });
+ }
+
+ @Test
+ public void testShellExecuteProcessRedirectToOutputStream() throws Exception {
+ JadbDevice any = jadb.getAnyDevice();
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ ByteArrayOutputStream err = new ByteArrayOutputStream();
+ Process process = any.shellProcessBuilder("ls /")
+ .redirectOutput(out)
+ .redirectError(err)
+ .start();
+ process.waitFor();
+ System.out.println(out.toString(StandardCharsets.UTF_8.name()));
+ System.out.println(err.toString(StandardCharsets.UTF_8.name()));
+ }
+
+ @Test
+ public void testShellExecuteProcessRedirectErrorStream() throws Exception {
+ JadbDevice any = jadb.getAnyDevice();
+ Process process = any.shellProcessBuilder("ls /").redirectErrorStream(true).start();
+ String stdout = new Scanner(process.getInputStream()).useDelimiter("\\A").next();
+ process.waitFor();
+ System.out.println(stdout);
+ }
+
+ @Test
+ public void testShellExecuteProcessDestroy() throws Exception {
+ JadbDevice anyDevice = jadb.getAnyDevice();
+ ExecutorService executor = Executors.newSingleThreadExecutor();
+ ShellProcess process = anyDevice.shellProcessBuilder("sleep 30").redirectErrorStream(true).useExecutor(executor).start();
+ process.destroy();
+ assertEquals(process.waitFor(), 9);
+ executor.shutdown();
+ assertTrue(executor.awaitTermination(5, TimeUnit.SECONDS));
}
@Test
@@ -135,11 +191,10 @@ public class RealDeviceTestCases {
}
/**
- * @see #testConnectionToTcpDevice()
- *
* @throws IOException
* @throws JadbException
* @throws ConnectionToRemoteDeviceException
+ * @see #testConnectionToTcpDevice()
*/
@Test
public void testDisconnectionToTcpDevice() throws IOException, JadbException, ConnectionToRemoteDeviceException {
diff --git a/test/se/vidstige/jadb/test/unit/MockedTestCases.java b/test/se/vidstige/jadb/test/unit/MockedTestCases.java
index 1a886a6..d316ee5 100644
--- a/test/se/vidstige/jadb/test/unit/MockedTestCases.java
+++ b/test/se/vidstige/jadb/test/unit/MockedTestCases.java
@@ -12,18 +12,55 @@ import se.vidstige.jadb.test.fakes.FakeAdbServer;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
+import java.io.DataOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.List;
+import java.util.Scanner;
public class MockedTestCases {
private FakeAdbServer server;
private JadbConnection connection;
+ private static long parseDate(String date) throws ParseException {
+ DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm");
+ return dateFormat.parse(date).getTime();
+ }
+
+ private static void assertHasFile(String expPath, int expSize, long expModifyTime, List actualFiles) {
+ for (RemoteFile file : actualFiles) {
+ if (expPath.equals(file.getPath())) {
+ if (file.isDirectory()) {
+ Assert.fail("File " + expPath + " was listed as a dir!");
+ } else if (expSize != file.getSize() || expModifyTime != file.getLastModified()) {
+ Assert.fail("File " + expPath + " exists but has incorrect properties!");
+ } else {
+ return;
+ }
+ }
+ }
+ Assert.fail("File " + expPath + " could not be found!");
+ }
+
+ private static void assertHasDir(String expPath, long expModifyTime, List actualFiles) {
+ for (RemoteFile file : actualFiles) {
+ if (expPath.equals(file.getPath())) {
+ if (!file.isDirectory()) {
+ Assert.fail("Dir " + expPath + " was listed as a file!");
+ } else if (expModifyTime != file.getLastModified()) {
+ Assert.fail("Dir " + expPath + " exists but has incorrect properties!");
+ } else {
+ return;
+ }
+ }
+ }
+ Assert.fail("Dir " + expPath + " could not be found!");
+ }
+
@Before
public void setUp() throws Exception {
server = new FakeAdbServer(15037);
@@ -130,6 +167,39 @@ public class MockedTestCases {
device.executeShell("echo", "h¡t]&poli");
}
+ @Test
+ public void testExecuteShellProtocol() throws Exception {
+ server.add("serial-123");
+ server.expectShell("serial-123", "ls -l").returns(buildStream(null, null, 0));
+ server.expectShell("serial-123", "ls foobar").returns(buildStream("123", "456", 0));
+ JadbDevice device = connection.getDevices().get(0);
+
+ Assert.assertEquals(device.shellProcessBuilder("ls", "-l").start().waitFor(), 0);
+
+ Process process = device.shellProcessBuilder("ls", "foobar").redirectErrorStream(true).start();
+ Assert.assertEquals(new Scanner(process.getInputStream()).useDelimiter("\\A").next(), "123456");
+ Assert.assertEquals(process.waitFor(), 0);
+ }
+
+ private String buildStream(String out, String err, int exitCode) throws Exception {
+ ByteArrayOutputStream os = new ByteArrayOutputStream();
+ DataOutputStream dos = new DataOutputStream(os);
+ if (out != null) {
+ os.write(1);
+ dos.writeInt(Integer.reverseBytes(out.length()));
+ os.write(out.getBytes(StandardCharsets.US_ASCII));
+ }
+ if (err != null) {
+ os.write(2);
+ dos.writeInt(Integer.reverseBytes(err.length()));
+ os.write(err.getBytes(StandardCharsets.US_ASCII));
+ }
+ os.write(3); // exitcode stream
+ dos.writeInt(Integer.reverseBytes(1));
+ os.write(exitCode);
+ return os.toString(StandardCharsets.US_ASCII.name());
+ }
+
@Test
public void testFileList() throws Exception {
server.add("serial-123");
@@ -150,39 +220,4 @@ public class MockedTestCases {
assertHasFile("effective java vol. 7.epub", 0xCAFE, 0xBABE, files);
assertHasFile("\uB9AC\uADF8 \uC624\uBE0C \uB808\uC804\uB4DC", 240, 9001, files);
}
-
- private static long parseDate(String date) throws ParseException {
- DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm");
- return dateFormat.parse(date).getTime();
- }
-
- private static void assertHasFile(String expPath, int expSize, long expModifyTime, List actualFiles) {
- for (RemoteFile file : actualFiles) {
- if (expPath.equals(file.getPath())) {
- if (file.isDirectory()) {
- Assert.fail("File " + expPath + " was listed as a dir!");
- } else if (expSize != file.getSize() || expModifyTime != file.getLastModified()) {
- Assert.fail("File " + expPath + " exists but has incorrect properties!");
- } else {
- return;
- }
- }
- }
- Assert.fail("File " + expPath + " could not be found!");
- }
-
- private static void assertHasDir(String expPath, long expModifyTime, List actualFiles) {
- for (RemoteFile file : actualFiles) {
- if (expPath.equals(file.getPath())) {
- if (!file.isDirectory()) {
- Assert.fail("Dir " + expPath + " was listed as a file!");
- } else if (expModifyTime != file.getLastModified()) {
- Assert.fail("Dir " + expPath + " exists but has incorrect properties!");
- } else {
- return;
- }
- }
- }
- Assert.fail("Dir " + expPath + " could not be found!");
- }
}