11.19 脚本 最小web控制linux/termux

功能,安装图形库太麻烦,直接启动服务器开新线程,浏览器跑命令

不用安装VNC各种

当然,一点bug没时间修复,靠各位了

说明 console.html 是用户端,可以打包成APP等

index是随便弄得首页,防止报错

<!DOCTYPE html>

<html lang="zh-CN">

<head>

<meta charset="UTF-8">

<meta name="viewport" content="width=device-width, initial-scale=1.0">

<title>Termux Web Console</title>

<style>

body {

margin: 0;

padding: 0;

background: #000;

font-family: 'Courier New', monospace;

height: 100vh;

display: flex;

flex-direction: column;

color: #0f0;

}

.header {

background: #111;

padding: 10px;

border-bottom: 1px solid #333;

display: flex;

justify-content: space-between;

align-items: center;

}

.header h2 {

margin: 0;

color: #0f0;

font-size: 16px;

}

.status {

display: flex;

align-items: center;

gap: 5px;

font-size: 12px;

}

.status-dot {

width: 8px;

height: 8px;

border-radius: 50%;

background: #666;

}

.status.connected .status-dot {

background: #0f0;

}

.status.disconnected .status-dot {

background: #f00;

}

#terminal {

flex: 1;

background: #000;

color: #fff;

padding: 10px;

overflow-y: auto;

font-family: 'Courier New', monospace;

font-size: 14px;

line-height: 1.4;

white-space: pre-wrap;

}

.terminal-line {

margin: 2px 0;

word-wrap: break-word;

}

.command-line {

color: #0f0;

}

.output-line {

color: #fff;

}

.error-line {

color: #f00;

}

.status-line {

color: #ff0;

font-style: italic;

}

.input-prompt {

color: #0ff;

}

.user-input {

color: #0f0;

}

.input-container {

background: #111;

padding: 10px;

border-top: 1px solid #333;

display: flex;

align-items: center;

}

.prompt {

color: #0f0;

margin-right: 8px;

}

#command-input {

flex: 1;

background: #000;

border: 1px solid #333;

color: #0f0;

padding: 5px;

font-family: 'Courier New', monospace;

font-size: 14px;

outline: none;

}

#command-input:focus {

border-color: #0f0;

}

</style>

</head>

<body>

<div class="header">

<h2>Termux Web Console</h2>

<div id="status" class="status disconnected">

<span class="status-dot"></span>

<span>未连接</span>

</div>

</div>

<div id="terminal"></div>

<div class="input-container">

<span class="prompt">$</span>

<input type="text" id="command-input" placeholder="输入命令..." autocomplete="off">

</div>

<script>

const terminal = document.getElementById('terminal');

const input = document.getElementById('command-input');

const statusEl = document.getElementById('status');

let eventSource = null;

let commandHistory = [];

let historyIndex = -1;

let currentCommand = '';

let awaitingInput = false;

let lastOutputWasPrompt = false;

// 简单的HTML转义

function escapeHtml(text) {

return text

.replace(/&/g, '&amp;')

.replace(/</g, '&lt;')

.replace(/>/g, '&gt;')

.replace(/"/g, '&quot;')

.replace(/'/g, '&#39;');

}

// 检查是否是输入提示

function isInputPrompt(text) {

// 检查常见的输入提示模式

const promptPatterns = [

/请输入.*:?\s*$/,

/Enter.*:?\s*$/,

/.*:?\s*$/,

/Password:?\s*$/i,

/username:?\s*$/i,

/路径:?\s*$/,

/目录:?\s*$/,

/文件:?\s*$/,

/choice:?\s*$/i,

/选择.*:?\s*$/

];

// 检查是否匹配任何提示模式

for (const pattern of promptPatterns) {

if (pattern.test(text.trim())) {

return true;

}

}

// 检查是否以冒号结尾但没有命令提示符

if (text.trim().endsWith(':') && !text.includes('$') && !text.includes('#')) {

return true;

}

return false;

}

function addLine(text, className = '') {

const line = document.createElement('div');

line.className = `terminal-line ${className}`;

// 清理文本

let cleanText = escapeHtml(text);

// 如果看起来像HTML标签,移除它们

if (cleanText.includes('<span') || cleanText.includes('</span>')) {

cleanText = cleanText.replace(/<[^>]*>/g, '');

}

// 移除ANSI转义序列

cleanText = cleanText.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, '');

line.innerHTML = cleanText;

terminal.appendChild(line);

terminal.scrollTop = terminal.scrollHeight;

}

function addCommand(command) {

currentCommand = command;

addLine(` {command}`, 'command-line');

}

function addUserInput(inputText) {

addLine(inputText, 'user-input');

}

function updateStatus(connected) {

if (connected) {

statusEl.className = 'status connected';

statusEl.querySelector('span:last-child').textContent = '已连接';

} else {

statusEl.className = 'status disconnected';

statusEl.querySelector('span:last-child').textContent = '未连接';

}

}

function connect() {

if (eventSource) {

eventSource.close();

}

eventSource = new EventSource('/stream');

eventSource.onopen = function() {

updateStatus(true);

addLine('*** 已连接到服务器 ***', 'status-line');

};

eventSource.onmessage = function(event) {

try {

const data = JSON.parse(event.data);

switch(data.type) {

case 'stdout':

case 'stderr':

let outputText = data.text;

// 跳过重复的命令显示

if (currentCommand && outputText.includes(currentCommand)) {

const cmdIndex = outputText.indexOf(currentCommand);

if (cmdIndex !== -1) {

outputText = outputText.substring(cmdIndex + currentCommand.length).trim();

}

if (outputText.trim()) {

// 检查是否是输入提示

if (isInputPrompt(outputText)) {

addLine(outputText, 'input-prompt');

awaitingInput = true;

lastOutputWasPrompt = true;

} else {

addLine(outputText, data.type === 'stderr' ? 'error-line' : 'output-line');

lastOutputWasPrompt = false;

}

}

}

// 检查是否是输入提示

else if (isInputPrompt(outputText)) {

addLine(outputText, 'input-prompt');

awaitingInput = true;

lastOutputWasPrompt = true;

}

// 普通输出

else if (outputText.trim()) {

addLine(outputText, data.type === 'stderr' ? 'error-line' : 'output-line');

lastOutputWasPrompt = false;

}

break;

case 'status':

addLine(data.text, 'status-line');

break;

case 'error':

addLine(data.text, 'error-line');

break;

case 'heartbeat':

// 忽略心跳

break;

}

} catch (e) {

console.error('解析消息错误:', e);

}

};

eventSource.onerror = function() {

updateStatus(false);

addLine('*** 连接断开 ***', 'error-line');

setTimeout(connect, 3000);

};

}

async function executeCommand(command) {

if (!command.trim()) return;

addCommand(command);

commandHistory.push(command);

historyIndex = commandHistory.length;

try {

const response = await fetch('/execute', {

method: 'POST',

headers: {

'Content-Type': 'application/json',

},

body: JSON.stringify({ command: command })

});

if (!response.ok) {

const error = await response.json();

addLine(`错误: ${error.message}`, 'error-line');

}

} catch (error) {

addLine(`网络错误: ${error.message}`, 'error-line');

}

}

async function sendInput(inputText) {

try {

const response = await fetch('/execute', {

method: 'POST',

headers: {

'Content-Type': 'application/json',

},

body: JSON.stringify({ command: inputText })

});

if (!response.ok) {

const error = await response.json();

addLine(`错误: ${error.message}`, 'error-line');

}

} catch (error) {

addLine(`网络错误: ${error.message}`, 'error-line');

}

}

// 事件监听器

input.addEventListener('keydown', function(e) {

if (e.key === 'Enter') {

const command = input.value;

input.value = '';

if (awaitingInput) {

// 如果正在等待输入,显示用户输入并发送

addUserInput(command);

sendInput(command);

awaitingInput = false;

} else {

// 否则作为命令执行

executeCommand(command);

}

} else if (e.key === 'ArrowUp') {

e.preventDefault();

if (historyIndex > 0) {

historyIndex--;

input.value = commandHistory[historyIndex];

}

} else if (e.key === 'ArrowDown') {

e.preventDefault();

if (historyIndex < commandHistory.length - 1) {

historyIndex++;

input.value = commandHistory[historyIndex];

} else {

historyIndex = commandHistory.length;

input.value = '';

}

}

});

// 点击终端聚焦输入框

terminal.addEventListener('click', function() {

input.focus();

});

// 初始化

window.addEventListener('load', function() {

connect();

input.focus();

addLine('Welcome to Termux Web Console', 'status-line');

addLine('Type commands and press Enter to execute', 'status-line');

addLine('');

});

</script>

</body>

</html>

import subprocess

import threading

import queue

import json

from flask import Flask, request, jsonify, Response, send_from_directory

import os

import time

import sys

app = Flask(name, static_folder='.', static_url_path='')

添加CORS支持

@app.after_request

def after_request(response):

response.headers.add('Access-Control-Allow-Origin', '*')

response.headers.add('Access-Control-Allow-Headers', 'Content-Type,Authorization')

response.headers.add('Access-Control-Allow-Methods', 'GET,PUT,POST,DELETE,OPTIONS')

return response

--- 全局变量 ---

shell_process = None

output_queue = queue.Queue()

shell_ready = threading.Event()

def enqueue_output(stream, q, stream_name):

"""从子进程流中读取数据并放入队列"""

try:

while True:

line = stream.readline()

if not line:

break

if line:

text = line.decode('utf-8', errors='replace').rstrip()

q.put({'type': stream_name, 'text': text})

except Exception as e:

q.put({'type': 'error', 'text': f"流读取错误({stream_name}): {str(e)}"})

finally:

stream.close()

q.put({'type': 'status', 'text': f"{stream_name}流已关闭"})

def start_shell_session():

"""启动一个持久的shell会话和读取线程"""

global shell_process

print("正在启动Termux Shell会话...")

try:

shell_process = subprocess.Popen(

'/data/data/com.termux/files/usr/bin/bash', '-i'\], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, bufsize=0, env=os.environ.copy() ) t_stdout = threading.Thread(target=enqueue_output, args=(shell_process.stdout, output_queue, 'stdout')) t_stderr = threading.Thread(target=enqueue_output, args=(shell_process.stderr, output_queue, 'stderr')) t_stdout.daemon = True t_stderr.daemon = True t_stdout.start() t_stderr.start() time.sleep(1) if shell_process.poll() is None: shell_ready.set() print("Shell会话已启动成功。") output_queue.put({'type': 'status', 'text': '\*\*\* Shell已就绪 \*\*\*'}) else: print(f"Shell启动失败,退出码: {shell_process.poll()}") output_queue.put({'type': 'error', 'text': f'\*\*\* Shell启动失败,退出码: {shell_process.poll()} \*\*\*'}) except Exception as e: print(f"启动Shell时出错: {str(e)}") output_queue.put({'type': 'error', 'text': f'\*\*\* 启动Shell失败: {str(e)} \*\*\*'}) @app.route('/') def index(): """返回简单的index.html""" return app.send_static_file('index.html') @app.route('/console') def console(): """返回功能完整的控制台页面""" return app.send_static_file('console.html') @app.route('/execute', methods=\['POST', 'OPTIONS'\]) def execute(): """接收命令并发送到shell的stdin""" if request.method == 'OPTIONS': return '', 200 try: data = request.get_json() if not data or 'command' not in data: return jsonify({'status': 'error', 'message': '未提供命令'}), 400 command = data\['command'

if not shell_ready.is_set():

return jsonify({'status': 'error', 'message': 'Shell未就绪'}), 503

if shell_process and shell_process.poll() is None:

shell_process.stdin.write((command + '\n').encode('utf-8'))

shell_process.stdin.flush()

return jsonify({'status': 'success'})

else:

return jsonify({'status': 'error', 'message': 'Shell进程已退出'}), 500

except Exception as e:

return jsonify({'status': 'error', 'message': str(e)}), 500

@app.route('/stream')

def stream():

"""SSE流,将shell输出发送给前端"""

def generate():

发送初始连接消息

yield f"data: {json.dumps({'type': 'status', 'text': '*** 已连接到服务器 ***'})}\n\n"

if not shell_ready.is_set():

yield f"data: {json.dumps({'type': 'status', 'text': '*** 等待Shell启动... ***'})}\n\n"

shell_ready.wait(timeout=5)

while True:

try:

try:

output = output_queue.get(timeout=1)

yield f"data: {json.dumps(output)}\n\n"

except queue.Empty:

发送心跳保持连接

yield f"data: {json.dumps({'type': 'heartbeat'})}\n\n"

continue

except GeneratorExit:

print("SSE客户端断开连接")

break

except Exception as e:

yield f"data: {json.dumps({'type': 'error', 'text': f'流错误: {str(e)}'})}\n\n"

移除 'Connection' 和 'X-Accel-Buffering' 头

return Response(generate(), mimetype='text/event-stream',

headers={'Cache-Control': 'no-cache'})

if name == 'main':

start_shell_session()

获取本机IP地址

try:

import socket

s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)

s.connect(("8.8.8.8", 80))

local_ip = s.getsockname()[0]

s.close()

except:

local_ip = "localhost"

print("\n" + "="*50)

print("服务器已启动!")

print(f"本地访问: http://localhost:8080")

print(f"局域网访问: http://{local_ip}:8080")

print(f"控制台页面: http://{local_ip}:8080/console")

print("="*50 + "\n")

强制使用Flask开发服务器

print("使用Flask开发服务器...")

app.run(host='0.0.0.0', port=8080, threaded=True)

<!DOCTYPE html>

<html lang="zh-CN">

<head>

<meta charset="UTF-8">

<meta name="viewport" content="width=device-width, initial-scale=1.0">

<title>Termux Web Console</title>

<style>

body {

font-family: Arial, sans-serif;

background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);

color: white;

text-align: center;

padding: 50px;

margin: 0;

min-height: 100vh;

display: flex;

flex-direction: column;

justify-content: center;

align-items: center;

}

.container {

background: rgba(0, 0, 0, 0.3);

padding: 40px;

border-radius: 15px;

box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);

max-width: 500px;

}

h1 {

margin-bottom: 30px;

font-size: 2.5em;

}

.button {

display: inline-block;

background: #4CAF50;

color: white;

padding: 15px 30px;

text-decoration: none;

border-radius: 5px;

font-size: 1.2em;

margin: 10px;

transition: background 0.3s;

}

.button:hover {

background: #45a049;

}

.info {

margin-top: 30px;

font-size: 0.9em;

opacity: 0.8;

}

</style>

</head>

<body>

<div class="container">

<h1>🚀 Termux Web Console</h1>

<p>服务器正在运行中...</p>

<a href="/console" class="button">打开控制台</a>

<div class="info">

<p>提示:将此URL分享给同一网络下的其他设备</p>

<p>其他设备访问: http://[你的手机IP]:8080/console</p>

</div>

</div>

</body>

</html>

相关推荐
a123560mh2 小时前
国产信创操作系统银河麒麟常见软件适配(MongoDB、 Redis、Nginx、Tomcat)
linux·redis·nginx·mongodb·tomcat·kylin
赖small强2 小时前
【Linux驱动开发】Linux MMC子系统技术分析报告 - 第二部分:协议实现与性能优化
linux·驱动开发·mmc
九河云2 小时前
不同级别华为云代理商的增值服务内容与质量差异分析
大数据·服务器·人工智能·科技·华为云
许泽宇的技术分享2 小时前
当AI学会“说人话“:Azure语音合成技术的魔法世界
后端·python·flask
SongYuLong的博客2 小时前
Ubuntu24.04搭建GitLab服务器
运维·服务器·gitlab
guygg882 小时前
Linux服务器上安装配置GitLab
linux·运维·gitlab
百***35513 小时前
Linux(CentOS)安装 Nginx
linux·nginx·centos
tzhou644523 小时前
Linux文本处理工具:cut、sort、uniq、tr
linux·运维·服务器
这个一个非常哈3 小时前
VUE篇之推送+瀑布流
css·vue.js·css3