Skip to main content

subprocess

Spawn processes on device.

Raw mode vs PTY mode

Normally, when a process is started from terminal, it can interact with the terminal, it can:

  • Know the window size
  • Move the cursor
  • Clear the screen
  • Change the text color
  • etc.

For example, ls can print colored output when it's started from terminal, less can print exactly one screen of output and wait for user input, etc.

But if the process is started programmatically by another process, it's not attached to a terminal, and thus it cannot do any of the above. ls will print plain text, less will print all of its output and exit immediately.

The parent process (spawner) can do either of the following:

  • Spawn the child process in raw mode. The parent process can write to the child process's stdin, and read from its stdout and stderr. But the child process still don't have a terminal to interact with.
  • Use a pseudo-terminal (PTY) to spawn the child process. Pseudo-terminal API makes the child process believe that it's attached to a terminal, but actually all terminal interactions are handled by the parent process.

PTY mode support was added in Android 7.

How to choose?

  • To start a process and parse its output, use raw mode.
  • To build a terminal emulator, use PTY mode.

None protocol vs Shell protocol

ADB can transfer subprocess input/output in two protocols:

  • None protocol: Didn't have a name until Shell protocol was added in Android 7. Both readable and writable sides are continuous streams of raw bytes.
  • Shell protocol: Added in Android 7. Both readable and writable sides are using a simple packet format that identifies the type of the data.
ModeOperationNone protocolShell protocol
rawWrite to socketForwards to subprocess's stdin.One packet type: stdin
Read from socketSubprocess's stdout and stderr mixed togetherThree packet types: stdout, stderr, exit code
PTYWrite to socketForwards to PTYThree packet types: write to PTY, resize PTY, close stdin
Read from socketPTY outputTwo packet types: PTY output, exit code

How to choose?

  • To start a process and parse its output, use Shell protocol if separated stdout and stderr, or exit code is needed.
  • To build a terminal emulator, use Shell protocol for more features.
  • Otherwise, None protocol is more efficient.

Common types

export interface AdbSubprocessProtocol {
readonly stdin: WritableStream<MaybeConsumable<Uint8Array>>;
readonly stdout: ReadableStream<Uint8Array>;
readonly stderr: ReadableStream<Uint8Array>;
readonly exit: Promise<number>;
resize(rows: number, cols: number): ValueOrPromise<void>;
kill(): ValueOrPromise<void>;
}

interface AdbSubprocessProtocolConstructor {
isSupported(adb: Adb): ValueOrPromise<boolean>;
pty(adb: Adb, command: string): ValueOrPromise<AdbSubprocessProtocol>;
raw(adb: Adb, command: string): ValueOrPromise<AdbSubprocessProtocol>;
new (socket: AdbSocket): AdbSubprocessProtocol;
}

export interface AdbSubprocessOptions {
protocols: AdbSubprocessProtocolConstructor[];
}

The behavior of AdbSubprocessProtocol is described in the following table:

ModeFieldNone protocolShell protocol
rawstdinwrites to stdinwrites to stdin
stdoutread from stdout and stderrread from stdout
stderrnot usedread from stderr
exitresolve with 0 when process exitsresolve with exit code when process exits
resizedoes nothingresize PTY
killkill processkill process
PTYstdinwrites to PTYwrites to PTY
stdoutread from PTYread from PTY
stderrnot usednot used
exitresolve with 0 when process exitsresolve with exit code when process exits
resizedoes nothingresize PTY
killkill processkill process

stdin uses the Consumable pattern.

stdout and stderr will close when the process exits.

If stderr is marked as not used, reading from it won't produce any data, but it will still close when the process exits.

READ ALL STREAMS!

ADB is a multiplexing protocol (multiple logic streams are transferred over one connection), so blocking one stream will block all other streams.

You must continuously read from all incoming streams (either by piping them to WritableStreams or calling reader.read() in a loop) to prevent this from happening.

If the remaining data is not needed, stream.cancel() (or reader.cancel() if using a reader) can be called to discard them.

Start process in raw mode

declare class AdbSubprocess {
spawn(
command: string | string[],
options?: Partial<AdbSubprocessOptions>,
): Promise<AdbSubprocessProtocol>;
}

Example:

import { DecodeUtf8Stream } from "@yume-chan/stream-extra";

const process = await adb.subprocess.spawn("ls -l");
await process.stdout.pipeThrough(new DecodeUtf8Stream()).pipeTo(
new WritableStream<string>({
write(chunk) {
console.log(chunk);
},
}),
);
Equivalent ADB command
adb exec-out ls -l

This method will use Shell protocol if it's available, otherwise it will use None protocol.

To specify the protocol, use options.protocols:

import { AdbSubprocessShellProtocol } from "@yume-chan/adb";

const process = await adb.subprocess.spawn("ls -l", {
protocols: [AdbSubprocessShellProtocol],
});

This will throw an error if Shell protocol is not supported by the device.

Similarly, to use only None protocol:

import { AdbSubprocessNoneProtocol } from "@yume-chan/adb";

const process = await adb.subprocess.spawn("ls -l", {
protocols: [AdbSubprocessNoneProtocol],
});

Start process in PTY mode

declare class AdbSubprocess {
shell(
command?: string | string[],
options?: Partial<AdbSubprocessOptions>,
): Promise<AdbSubprocessProtocol>;
}

When command is undefined, the default shell is spawned.

The options parameter works the same as in spawn.

The stdout returned from this method will contain terminal escape sequences, which can be parsed using libraries like xterm.js.

Example:

import { Terminal } from "xterm";
import { encodeUtf8 } from "@yume-chan/adb";

const terminal: Terminal = new Terminal();

const process: AdbSubprocessProtocol = await adb.subprocess.shell();
process.stdout.pipeTo(
new WritableStream<Uint8Array>({
write(chunk) {
terminal.write(chunk);
},
}),
);

const writer = process.stdin.getWriter();
terminal.onData((data) => {
const buffer = encodeUtf8(data);
writer.write(buffer);
});

terminal.open(document.getElementById("terminal"));
Equivalent ADB command
adb shell

Stop process

Use the kill method to stop the process. This sends a SIGHUP signal to the process.

const process = await adb.subprocess.spawn("logcat");
await process.kill();

If there are still unread data in the stdout or stderr streams, they can still and must be read. Not reading them will cause the whole connection to be blocked.

When using Shell protocol, kill will immediately close the underlying ADB socket, even before exit code can be received. This will cause the exit promise to be rejected with an error. ADB doesn't expose any API to manually sending signals to the process, nor to get its process ID so that adb shell kill can be used.