Compare commits

...

22 Commits

Author SHA1 Message Date
oSumAtrIX
4955fb15a9
build: Change group id to app.revanced 2023-10-09 15:06:59 +02:00
oSumAtrIX
d4a59ad40f
build: Bump version to 1.2.1 2023-10-09 15:05:59 +02:00
Lucaskyy
2531a28109
feat: Add support for shell v2 commands (vidstige/jadb#121) 2022-06-11 18:14:23 +02:00
js6pak
fedca18ae1 Fix last modified time when pushing a local file 2022-02-13 19:49:29 +01:00
Samuel Carlsson
f738b9f86c adbserver: no client threads 2022-02-12 20:57:18 +01:00
Samuel Carlsson
fd1a833e6d better assert messages on unit test fail 2022-02-12 20:57:18 +01:00
Samuel Carlsson
081154ee1e updating badge 2022-02-12 20:57:18 +01:00
Samuel Carlsson
469604206b use "master" in triggers 2022-02-12 20:57:18 +01:00
Samuel Carlsson
3207c50778 switch to github action 2022-02-08 16:37:07 +01:00
Samuel Carlsson
6dd039aeb5 bump version 2021-03-05 16:42:59 +01:00
Samuel Carlsson
cfc66ff87c
Merge pull request #136 from root-intruder/master
fix FakeADBServer testcases which expect certain commands
2021-03-05 14:48:51 +01:00
Kris Heid
3ace8c814d fix FakeADBServer testcases which expect certain commands...however due to previous commits these commands are now quoted 2021-03-05 14:37:12 +01:00
Samuel Carlsson
d7565382eb
Merge pull request #135 from root-intruder/master
filter not allowed characters out of the adb protocol
2021-03-04 16:23:18 +01:00
Kris Heid
597102ab83 simplify special character handling intregration test with Stream.readAll() 2021-03-04 13:28:52 +01:00
Kris Heid
f4f41efbc3 add unit test to verify special character handling for commands 2021-03-04 13:27:04 +01:00
Kris Heid
ecf5a605fb calculate the correct length of the command to be transmitted for special characters like ä,ö... 2021-03-02 10:13:09 +01:00
Kris Heid
423ccb3a89 always quote command since not only whitespaces require quoting 2021-03-02 10:11:00 +01:00
Samuel Carlsson
0fe4484380
Merge pull request #131 from vidstige/dependabot/maven/junit-junit-4.13.1
Bump junit from 4.10 to 4.13.1
2020-11-17 11:37:18 +01:00
dependabot[bot]
9d016bef26
Bump junit from 4.10 to 4.13.1
Bumps [junit](https://github.com/junit-team/junit4) from 4.10 to 4.13.1.
- [Release notes](https://github.com/junit-team/junit4/releases)
- [Changelog](https://github.com/junit-team/junit4/blob/main/doc/ReleaseNotes4.10.md)
- [Commits](https://github.com/junit-team/junit4/compare/r4.10...r4.13.1)

Signed-off-by: dependabot[bot] <support@github.com>
2020-10-13 04:38:16 +00:00
Samuel Carlsson
3e2a88faf1
Encourage adding automated tests 2020-09-25 11:14:41 +02:00
Samuel Carlsson
60a070132e
Merge pull request #130 from root-intruder/master
push apk to /data/local/tmp/ instead of sdcard, otherwise packagemanager cannot install from sdcard
2020-09-25 09:12:58 +00:00
root-intruder
163fa7f07f push apk to /data/local/tmp/ instead of sdcard, otherwise packagemanager cannot install from sdcard 2020-09-25 09:13:13 +02:00
18 changed files with 818 additions and 91 deletions

26
.github/workflows/maven.yml vendored Normal file
View File

@ -0,0 +1,26 @@
# This workflow will build a Java project with Maven, and cache/restore any dependencies to improve the workflow execution time
# For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-maven
name: Java CI with Maven
on:
push:
branches: [ master ]
pull_request:
branches: [ master ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up JDK 8
uses: actions/setup-java@v2
with:
java-version: '8'
distribution: 'temurin'
cache: maven
- name: maven
run: mvn -B package --file pom.xml

View File

@ -1,5 +0,0 @@
language: java
jdk:
- openjdk8
after_success:
- bash <(curl -s https://codecov.io/bash)

View File

@ -7,10 +7,10 @@ The Android SDK Tools are available for the major platforms (Mac, Windows & Linu
This projects aims at providing an up to date implementation of the ADB protocol.
[![Build Status](https://travis-ci.org/vidstige/jadb.svg?branch=master)](https://travis-ci.org/vidstige/jadb)
[![](https://jitpack.io/v/vidstige/jadb.svg)](https://jitpack.io/#vidstige/jadb)
![Build Status](https://github.com/vidstige/jadb/actions/workflows/maven.yml/badge.svg)
[![jitpack badge](https://jitpack.io/v/vidstige/jadb.svg)](https://jitpack.io/#vidstige/jadb)
[![codecov](https://codecov.io/gh/vidstige/jadb/branch/master/graph/badge.svg)](https://codecov.io/gh/vidstige/jadb)
[![](http://img.shields.io/badge/first--timers--only-friendly-green.svg?style=flat&colorB=FF69B4)](http://www.firsttimersonly.com/)
[![first timers friendly](http://img.shields.io/badge/first--timers--only-friendly-green.svg?style=flat&colorB=FF69B4)](http://www.firsttimersonly.com/)
## Example ##
@ -70,7 +70,7 @@ project name and tag ignoring actual values from pom.xml. So you need to write:
<dependency>
<groupId>com.github.vidstige</groupId>
<artifactId>jadb</artifactId>
<version>v1.1.0</version>
<version>v1.2.1</version>
</dependency>
```
@ -94,6 +94,8 @@ This project would not be where it is, if it where not for the helpful [contribu
supporting jadb with pull requests, issue reports, and great ideas. If _you_ would like to
contribute, please read through [CONTRIBUTING.md](CONTRIBUTING.md).
* If you fix a bug, try to _first_ create a failing test. Reach out to me for assistance or guidance if needed.
## Authors ##
Samuel Carlsson <samuel.carlsson@gmail.com>

14
pom.xml
View File

@ -4,9 +4,9 @@
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>se.vidstige</groupId>
<groupId>app.revanced</groupId>
<artifactId>jadb</artifactId>
<version>1.1.0-SNAPSHOT</version>
<version>1.2.1</version>
<url>https://github.com/vidstige/jadb</url>
<licenses>
@ -30,14 +30,14 @@
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<maven.compiler.target>1.7</maven.compiler.target>
<maven.compiler.source>1.7</maven.compiler.source>
<mockito-core.version>2.7.17</mockito-core.version>
<mockito-core.version>2.28.2</mockito-core.version>
</properties>
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.10</version>
<version>4.13.1</version>
<scope>test</scope>
</dependency>
@ -116,7 +116,7 @@
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.7.7.201606060606</version>
<version>0.8.4</version>
<executions>
<execution>
<goals>
@ -196,7 +196,7 @@
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-javadoc-plugin</artifactId>
<version>2.10.3</version>
<version>2.10.4</version>
<executions>
<execution>
<id>attach-javadocs</id>
@ -218,4 +218,4 @@
<tag>HEAD</tag>
</scm>
</project>
</project>

View File

@ -5,6 +5,7 @@ import se.vidstige.jadb.managers.Bash;
import java.io.*;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
public class JadbDevice {
@SuppressWarnings("squid:S00115")
@ -56,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.
@ -115,6 +116,19 @@ public class JadbDevice {
}
}
/** <p>Execute a shell command.</p>
*
* <p>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}.</p>
*
* @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());
}
/** <p>Execute a command with raw binary output.</p>
*
* <p>Support for this command was added in Lollipop (Android 5.0), and is the recommended way to transmit binary
@ -199,7 +213,7 @@ public class JadbDevice {
public void push(File local, RemoteFile remote) throws IOException, JadbException {
try (FileInputStream fileStream = new FileInputStream(local)) {
push(fileStream, local.lastModified(), DEFAULT_MODE, remote);
push(fileStream, TimeUnit.MILLISECONDS.toSeconds(local.lastModified()), DEFAULT_MODE, remote);
}
}

View File

@ -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<Integer> exitCodeFuture;
private final ShellProtocolTransport transport;
private Integer exitCode = null;
ShellProcess(OutputStream outputStream, InputStream inputStream, InputStream errorStream, Future<Integer> 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();
}
}

View File

@ -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.
*
* <p>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)}.</p>
*
* <p>Use {@link #start()} to execute the command, and then use {@link Process#waitFor()} to wait for the command to
* complete.</p>
*
* <p><b>Warning:</b> If stdout and stderr are both set to {@link java.lang.ProcessBuilder.Redirect#PIPE} (the default),
* you <b>must</b> read from their InputStreams ({@link Process#getInputStream()} and {@link Process#getErrorStream()})
* <b>concurrently</b>. 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()}:
*
* <pre>{@code
* Process process = jadbDevice.shellProcessBuilder("command")
* .redirectErrorStream(errorStream)
* .start();
* String stdoutAndStderr = new Scanner(process.getInputStream()).useDelimiter("\\A").next();
* int exitCode = process.waitFor();
* }</pre>
* <p>
* 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}:
*
* <pre>{@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());
* }</pre>
*/
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.
* <p>Note: this output steam will be called from a separate thread.</p>
*
* @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.
* <p>Note: this output steam will be called from a separate thread.</p>
*
* @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<Integer> transportTask = new FutureTask<>(new Callable<Integer>() {
@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;
}
}
}

View File

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

View File

@ -51,7 +51,7 @@ class Transport implements Closeable {
}
private String getCommandLength(String command) {
return String.format("%04x", command.length());
return String.format("%04x", command.getBytes().length);
}
public void send(String command) throws IOException {
@ -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();

View File

@ -6,10 +6,6 @@ public class Bash {
}
public static String quote(String s) {
// Check that s contains no whitespace
if (s.matches("\\S+")) {
return s;
}
return "'" + s.replace("'", "'\\''") + "'";
}
}

View File

@ -49,7 +49,7 @@ public class PackageManager {
}
private void install(File apkFile, List<String> extraArguments) throws IOException, JadbException {
RemoteFile remote = new RemoteFile("/sdcard/tmp/" + apkFile.getName());
RemoteFile remote = new RemoteFile("/data/local/tmp/" + apkFile.getName());
device.push(apkFile, remote);
List<String> arguments = new ArrayList<>();
arguments.add("install");

View File

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

View File

@ -40,9 +40,7 @@ public abstract class SocketServer implements Runnable {
while (true) {
Socket c = socket.accept();
Thread clientThread = new Thread(createResponder(c), "AdbClientWorker");
clientThread.setDaemon(true);
clientThread.start();
createResponder(c).run();
}
} catch (IOException e) {
// Empty on purpose

View File

@ -186,10 +186,18 @@ public class FakeAdbServer implements AdbResponder {
}
public void verifyExpectations() {
org.junit.Assert.assertEquals(0, fileExpectations.size());
org.junit.Assert.assertEquals(0, shellExpectations.size());
org.junit.Assert.assertEquals(0, listExpectations.size());
org.junit.Assert.assertEquals(0, tcpipExpectations.size());
for (FileExpectation expectation : fileExpectations) {
org.junit.Assert.fail(expectation.toString());
}
for (ShellExpectation expectation : shellExpectations) {
org.junit.Assert.fail(expectation.toString());
}
for (ListExpectation expectation : listExpectations) {
org.junit.Assert.fail(expectation.toString());
}
for (int expectation : tcpipExpectations) {
org.junit.Assert.fail("Expected tcp/ip on" + expectation);
}
}
private static class FileExpectation implements ExpectationBuilder {
@ -233,6 +241,11 @@ public class FakeAdbServer implements AdbResponder {
public void returnFile(ByteArrayOutputStream buffer) throws IOException {
buffer.write(content);
}
@Override
public String toString() {
return "Expected file " + path;
}
}
public static class ShellExpectation {
@ -254,6 +267,11 @@ public class FakeAdbServer implements AdbResponder {
public void writeOutputTo(DataOutputStream stdout) throws IOException {
stdout.write(this.stdout);
}
@Override
public String toString() {
return "Expected shell " + command;
}
}
public static class ListExpectation {
@ -282,6 +300,11 @@ public class FakeAdbServer implements AdbResponder {
public List<RemoteFile> getFiles() {
return Collections.unmodifiableList(files);
}
@Override
public String toString() {
return "Expected file list " + remotePath;
}
private static class MockFileEntry extends RemoteFile {

View File

@ -0,0 +1,66 @@
package se.vidstige.jadb.test.integration;
import org.junit.Assert;
import org.junit.BeforeClass;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import se.vidstige.jadb.JadbConnection;
import se.vidstige.jadb.JadbDevice;
import se.vidstige.jadb.Stream;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Collection;
@RunWith(Parameterized.class)
public class ExecuteCmdTests {
private static JadbConnection jadb;
private static JadbDevice jadbDevice;
@Parameterized.Parameter
public String input;
@Parameterized.Parameters(name="Test {index} input={0}")
public static Collection input() {
return Arrays.asList(new Object[]{
"öäasd",
"asf dsa",
"sdf&g",
"sd& fg",
"da~f",
"asd'as",
"a¡f",
"asĂĽd",
"adös tz",
"⾀",
"ĂĄ",
"æ",
"{}"});
}
@BeforeClass
public static void connect() {
try {
jadb = new JadbConnection();
jadb.getHostVersion();
jadbDevice = jadb.getAnyDevice();
} catch (Exception e) {
org.junit.Assume.assumeNoException(e);
}
}
@Test
public void testExecuteWithSpecialChars() throws Exception {
InputStream response = jadbDevice.execute("echo", input);
Assert.assertEquals(input, Stream.readAll(response, StandardCharsets.UTF_8).replaceAll("\n$", ""));
}
}

View File

@ -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<String> stdout = new AtomicReference<>();
AtomicReference<String> 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<String> 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 {

View File

@ -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<RemoteFile> 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<RemoteFile> 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);
@ -99,7 +136,7 @@ public class MockedTestCases {
@Test
public void testExecuteShell() throws Exception {
server.add("serial-123");
server.expectShell("serial-123", "ls -l").returns("total 0");
server.expectShell("serial-123", "ls '-l'").returns("total 0");
JadbDevice device = connection.getDevices().get(0);
device.executeShell("ls", "-l");
}
@ -119,11 +156,48 @@ public class MockedTestCases {
server.expectShell("serial-123", "echo 'tab\tstring'").returns("tab\tstring");
server.expectShell("serial-123", "echo 'newline1\nstring'").returns("newline1\nstring");
server.expectShell("serial-123", "echo 'newline2\r\nstring'").returns("newline2\r\nstring");
server.expectShell("serial-123", "echo 'fuö äzpo'").returns("fuö äzpo");
server.expectShell("serial-123", "echo 'h¡t]&poli'").returns("h¡t]&poli");
JadbDevice device = connection.getDevices().get(0);
device.executeShell("ls", "space file");
device.executeShell("echo", "tab\tstring");
device.executeShell("echo", "newline1\nstring");
device.executeShell("echo", "newline2\r\nstring");
device.executeShell("echo", "fuö äzpo");
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
@ -146,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<RemoteFile> 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<RemoteFile> 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!");
}
}

View File

@ -44,7 +44,7 @@ public class PackageManagerTest {
String response = "package:/system/priv-app/Contacts.apk-com.android.contacts\n" +
"package:/system/priv-app/Teleservice.apk-com.android.phone";
server.expectShell(DEVICE_SERIAL, "pm list packages").returns(response);
server.expectShell(DEVICE_SERIAL, "pm 'list' 'packages'").returns(response);
//Act
List<Package> actual = new PackageManager(device).getPackages();
@ -64,7 +64,7 @@ public class PackageManagerTest {
"[malformed_line]\n" +
"package:/system/priv-app/Teleservice.apk-com.android.phone";
server.expectShell(DEVICE_SERIAL, "pm list packages").returns(response);
server.expectShell(DEVICE_SERIAL, "pm 'list' 'packages'").returns(response);
//Act
List<Package> actual = new PackageManager(device).getPackages();
@ -79,7 +79,7 @@ public class PackageManagerTest {
List<Package> expected = new ArrayList<>();
String response = "";
server.expectShell(DEVICE_SERIAL, "pm list packages").returns(response);
server.expectShell(DEVICE_SERIAL, "pm 'list' 'packages'").returns(response);
//Act
List<Package> actual = new PackageManager(device).getPackages();