Mobile Development 13 min read

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.

Goodme Frontend Team
Goodme Frontend Team
Goodme Frontend Team
How to Build a Web‑Based Android Shell with ADB, Node‑PTY, and Socket.io

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.

alt
alt

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

alt
alt

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.

alt
alt

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.

alt
alt

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 device

Creating 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.

alt
alt

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;
AndroidNode.jsADBdevice managementSocket.IOweb shell
Goodme Frontend Team
Written by

Goodme Frontend Team

Regularly sharing the team's insights and expertise in the frontend field

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.