How to Build a Web‑Based Android Shell with ADB, Node‑PTY, and Socket.io
This article explains how to create a web‑based Android terminal by combining ADB, a persistent socket connection, node‑pty, and xterm.js, enabling remote device management, command execution, and file operations for IoT and development scenarios.
Background
In the IoT field, Android devices are widely used, making their management and operation essential. For developers who are not familiar with Android, controlling these devices via a web‑based graphical terminal can simplify debugging, log viewing, batch command execution, and reduce management costs.
Preliminary方案
Initially we tried an MQTT‑based command dispatch where the Android side receives commands, executes them, and reports results to a log platform. This approach required a new command implementation for each added function and suffered from uncertainty when commands or logs were lost.
ADB
ADB (Android Debug Bridge) is a command‑line tool that enables a computer to communicate with Android devices over USB or TCP. Common commands include:
# Connect devices
adb devices
adb connect <IP>:<port>
# App management
adb install path/to/app.apk
adb uninstall com.example.app
# File transfer
adb push local/path /sdcard/remote/path
adb pull /sdcard/remote/path local/path
# Execute shell
adb shell
adb shell <command>More details are available in the Android developer documentation.
ADB basic architecture
ADB Client : runs on the PC and starts when adb is invoked.
ADB Server : a background process on the PC that mediates communication between the client and daemon.
ADB Daemon (adbd) : runs on the device or emulator and is started by the init system.
The client and server communicate over a local TCP connection on port 5037.
Android Shell
Introduction
Android Shell is a Linux‑based command‑line interface that bridges developers and the Android system.
Shell environment
After connecting via USB or adb connect, running adb shell opens an interactive session where commands such as ls list files.
Web ↔ Android Shell Interaction
Similar to a jump server, a web page can open an ADB shell through a persistent socket connection. Commands are sent from the browser to the server, forwarded to the device, and the output is streamed back.
Bidirectional Communication
Using node-pty to spawn an ADB shell on the server and socket.io to relay data, the client can type commands in a browser‑based terminal (xterm.js) and see real‑time results.
Establishing the connection
Devices can be reached via USB or over LAN with adb connect ip:port. For devices outside the LAN, the Android port is mapped to a public address using FRP, then accessed with the same adb connect command. await execa `adb connect ip:port`; When multiple devices are attached, specify the target with adb -s ip:port shell.
$ adb devices
List of devices attached
emulator-5554 device
192.168.1.101:5555 deviceCreating the PTY process
const ptyProcess = pty.spawn('adb', ['shell'], {
name: 'xterm-color',
cols: 80,
rows: 30
});Binding PTY to socket
const server = new Server(socket.server);
server.on('connection', socket => {
socket.on('data', data => {
ptyProcess.write(data);
});
});
ptyProcess.onData(data => {
server.emit('data', data);
});Client‑side terminal
let socket;
await fetch('/api/node-pty');
socket = io();
// receive data from server
socket.on('data', data => {
xterm.write(data);
});
// send data to server
xterm.onData(data => {
socket.emit('data', data);
});Result
The demo demonstrates a functional web‑based Android shell with additional features such as desktop control and file management, laying the groundwork for more advanced operation scripts.
Conclusion
This simple demo shows how to control Android devices from a browser, and how FRP can expose remote devices. A well‑designed operation platform should enable non‑technical users to manage devices easily.
Appendix
The example is built with Next.js; the source code can be referenced for further development.
import { Server } from 'socket.io';
import { NextApiRequest, NextApiResponse } from 'next';
// eslint-disable-next-line @typescript-eslint/no-var-requires
const pty = require('node-pty');
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
const ptyProcess = pty.spawn('adb', ['shell'], {
name: 'xterm-color',
cols: 80,
rows: 30
});
const socket = res.socket as any;
const server = new Server(socket.server);
server.on('connection', socket => {
socket.on('data', (data: string) => {
ptyProcess.write(data);
});
});
ptyProcess.onData((data: string) => {
server.emit('data', data);
});
return res.status(200).json({ message: 'success' });
};
export default handler; 'use client';
import React, { useEffect, useRef } from 'react';
import 'xterm/css/xterm.css';
import { io, Socket } from 'socket.io-client';
import { DefaultEventsMap } from '@socket.io/component-emitter';
const TerminalComponent: React.FC = () => {
const terminalRef = useRef<HTMLDivElement>(null);
useEffect(() => {
let socket: Socket<DefaultEventsMap, DefaultEventsMap>;
let xterm: any;
const initializeTerminal = async () => {
const FitAddon = (await import('xterm-addon-fit')).FitAddon;
const Terminal = (await import('xterm')).Terminal;
await fetch('/api/node-pty');
socket = io();
if (terminalRef.current) {
xterm = new Terminal({
cols: 100,
rows: 60,
cursorBlink: true,
cursorStyle: 'block',
fontSize: 14,
convertEol: true,
theme: {
background: '#000000',
foreground: '#ffffff',
cursor: '#2dea5f'
}
});
const fitAddon = new FitAddon();
xterm.loadAddon(fitAddon);
xterm.open(terminalRef.current);
fitAddon.fit();
xterm.focus();
socket.on('data', (data: ArrayBuffer) => {
xterm.write(data);
});
xterm.onData((data: string) => {
socket.emit('data', data);
});
setTimeout(() => {
socket.emit('data', 's');
socket.emit('data', 'u');
socket.emit('data', '\r');
}, 1000);
}
};
initializeTerminal();
return () => {
if (xterm) xterm.dispose();
};
}, []);
return <div ref={terminalRef} className="h-full w-full" />;
};
export default TerminalComponent;Goodme Frontend Team
Regularly sharing the team's insights and expertise in the frontend field
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
