Files
Android-pyApi/android_pyapi/_linux_impl.py
T
shuiyu1123 690022cea3 feat: Linux 真实实现层 — 从存根库变为可用生产力工具
新增 _linux_impl.py (800+行):
- LinuxNetwork: 真实IP/接口/DNS/网关/MAC (ip命令+/proc/net)
- LinuxBluetooth: 真实蓝牙状态/扫描/配对/连接 (bluetoothctl)
- LinuxSensors: 真实温度/CPU频率/风扇 (/sys/class/hwmon)
- LinuxAudio: 真实音量控制/静音 (amixer)
- LinuxClipboard: 真实剪贴板读写 (xclip+tkinter回退)
- LinuxPower: 真实CPU/内存/电池 (psutil+/sys/class/power_supply)
- LinuxLocation: 真实IP地理定位 (ip-api.com)
- LinuxNotify: 真实桌面通知 (notify-send)
- LinuxDisplay: 真实屏幕尺寸/截屏/鼠标位置 (xdotool+X11)
- LinuxUIAutomation: 真实点击/输入/按键/滚动 (xdotool)
- LinuxCamera: 真实摄像头拍照 (opencv/V4L2)
- LinuxProcess: 真实进程列表/系统信息/应用管理 (psutil+/proc)
- LinuxShell: 真实命令执行 (subprocess)

改造10+核心类使用Linux真实实现:
- BluetoothAdapter/SensorManager/AudioManager
- ConnectivityManager/WifiManager/TelephonyManager
- LocationManager/PowerManager/BatteryManager/Build
- ActivityManager/Activity/Toast/Display/ClipboardManager
- NotificationManager

15项真实行为测试全部通过:
- 真实IP: 10.60.218.244
- 真实位置: 北京 39.9042, 116.407
- 真实主机名: work-41-vci-g427c
- 真实内核: 5.15.120.byteatom-ck.14
- 真实内存: 1.8GB/3.8GB
- 真实进程列表
2026-06-24 15:40:49 +00:00

1228 lines
44 KiB
Python

"""
Linux 真实实现层
================
在 Linux 环境下,将 Android API 的存根行为替换为真实的系统调用。
使 Android-pyApi 从"存根库"变为"可用的生产力工具"。
覆盖能力:
- 网络: ip 命令, /proc/net, socket
- 蓝牙: bluetoothctl / dbus
- 传感器: /sys/class/hwmon, /sys/class/thermal, sensors
- 音频: amixer / ALSA
- 剪贴板: xclip
- 电源/电池: /sys/class/power_supply, psutil
- 位置: IP 地理定位 (requests)
- 通知: notify-send
- Shell: subprocess
- 显示: xdotool, X11
- UI自动化: xdotool
- 摄像头: opencv (V4L2)
- 进程管理: psutil, /proc
- 文件: 原生 Python os
- 媒体: ffmpeg/ffprobe
"""
from __future__ import annotations
import os
import sys
import re
import json
import time
import socket
import struct
import shutil
import subprocess
import platform
import hashlib
from pathlib import Path
from datetime import datetime
from typing import Any, Dict, List, Optional, Tuple
# ═══════════════════════════════════════════════════════════════
# 能力检测
# ═══════════════════════════════════════════════════════════════
IS_LINUX = platform.system() == "Linux"
def _which(name: str) -> Optional[str]:
return shutil.which(name)
HAS_IP = _which("ip") is not None
HAS_BLUETOOTHCTL = _which("bluetoothctl") is not None
HAS_AMIXER = _which("amixer") is not None
HAS_XCLIP = _which("xclip") is not None
HAS_XDOTOOL = _which("xdotool") is not None
HAS_NOTIFY_SEND = _which("notify-send") is not None
HAS_SENSORS = _which("sensors") is not None
HAS_FFMPEG = _which("ffmpeg") is not None
HAS_FFPROBE = _which("ffprobe") is not None
HAS_XRANDR = _which("xrandr") is not None
try:
import psutil
HAS_PSUTIL = True
except ImportError:
HAS_PSUTIL = False
try:
import requests
HAS_REQUESTS = True
except ImportError:
HAS_REQUESTS = False
try:
import cv2
HAS_CV2 = True
except ImportError:
HAS_CV2 = False
try:
import numpy as np
HAS_NUMPY = True
except ImportError:
HAS_NUMPY = False
try:
from PIL import Image, ImageDraw, ImageGrab
HAS_PIL = True
except ImportError:
HAS_PIL = False
try:
import dbus
HAS_DBUS = True
except ImportError:
HAS_DBUS = False
def _run(cmd: List[str] or str, timeout: int = 10, capture: bool = True) -> Tuple[int, str, str]:
"""执行命令,返回 (exit_code, stdout, stderr)"""
try:
if isinstance(cmd, str):
result = subprocess.run(cmd, shell=True, capture_output=capture,
text=True, timeout=timeout)
else:
result = subprocess.run(cmd, capture_output=capture,
text=True, timeout=timeout)
return result.returncode, result.stdout.strip(), result.stderr.strip()
except subprocess.TimeoutExpired:
return -1, "", "timeout"
except Exception as e:
return -1, "", str(e)
def _read_file(path: str) -> Optional[str]:
try:
with open(path, "r") as f:
return f.read().strip()
except Exception:
return None
def _read_file_int(path: str) -> Optional[int]:
val = _read_file(path)
if val is not None:
try:
return int(val)
except ValueError:
return None
return None
def _list_dir(path: str) -> List[str]:
try:
return os.listdir(path)
except Exception:
return []
# ═══════════════════════════════════════════════════════════════
# 网络 — 真实实现
# ═══════════════════════════════════════════════════════════════
class LinuxNetwork:
"""真实网络信息 — 通过 ip 命令和 /proc/net"""
@staticmethod
def get_interfaces() -> List[dict]:
"""获取网络接口列表"""
if not HAS_IP:
return []
code, out, _ = _run(["ip", "-j", "addr", "show"])
if code == 0 and out:
try:
return json.loads(out)
except json.JSONDecodeError:
pass
# 回退到 /proc/net
interfaces = []
for name in _list_dir("/sys/class/net/"):
state = _read_file(f"/sys/class/net/{name}/operstate") or "unknown"
mac = _read_file(f"/sys/class/net/{name}/address") or ""
interfaces.append({"ifname": name, "operstate": state, "address": mac})
return interfaces
@staticmethod
def get_ip_address(interface: str = None) -> str:
"""获取IP地址"""
# 优先用 socket 获取外网IP
try:
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.connect(("8.8.8.8", 80))
ip = s.getsockname()[0]
s.close()
return ip
except Exception:
pass
# 回退到 hostname
try:
return socket.gethostbyname(socket.gethostname())
except Exception:
return "127.0.0.1"
@staticmethod
def get_wifi_info() -> dict:
"""获取WiFi信息"""
# 尝试 /proc/net/wireless
data = _read_file("/proc/net/wireless")
if data:
lines = data.strip().split("\n")
if len(lines) >= 3:
parts = lines[2].split()
if len(parts) >= 7:
return {
"ssid": "",
"interface": parts[0].rstrip(":"),
"status": parts[1],
"quality": float(parts[2]),
"signal": float(parts[3]),
"noise": float(parts[4]),
}
# 尝试 nmcli
code, out, _ = _run(["nmcli", "-t", "-f", "active,ssid,signal", "dev", "wifi"])
if code == 0 and out:
for line in out.split("\n"):
if line.startswith("yes:"):
parts = line.split(":")
if len(parts) >= 3:
return {"ssid": parts[1], "signal": int(parts[2])}
return {"ssid": "", "signal": 0}
@staticmethod
def get_connectivity() -> bool:
"""检测网络连通性"""
# 方法1: 尝试 TCP 连接到公共 DNS
for host, port in [("8.8.8.8", 53), ("1.1.1.1", 53), ("114.114.114.114", 53)]:
try:
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.settimeout(3)
s.connect((host, port))
s.close()
return True
except Exception:
continue
# 方法2: 尝试 HTTP 请求
if HAS_REQUESTS:
try:
requests.get("http://www.baidu.com", timeout=3)
return True
except Exception:
pass
# 方法3: 检查是否有非 lo 接口且状态为 UP
for iface in LinuxNetwork.get_interfaces():
name = iface.get("ifname", iface.get("name", ""))
state = iface.get("operstate", iface.get("state", ""))
if name and name != "lo" and state in ("up", "UP"):
return True
return False
@staticmethod
def get_dns_servers() -> List[str]:
"""获取DNS服务器"""
data = _read_file("/etc/resolv.conf")
if data:
dns = []
for line in data.split("\n"):
line = line.strip()
if line.startswith("nameserver"):
parts = line.split()
if len(parts) >= 2:
dns.append(parts[1])
return dns
return []
@staticmethod
def get_gateway() -> str:
"""获取默认网关"""
data = _read_file("/proc/net/route")
if data:
for line in data.split("\n")[1:]:
parts = line.split()
if len(parts) >= 3 and parts[1] == "00000000":
# 网关是十六进制小端
gw_hex = parts[2]
try:
gw_bytes = bytes.fromhex(gw_hex)
gw = socket.inet_ntoa(gw_bytes[::-1])
return gw
except Exception:
pass
return ""
@staticmethod
def get_mac_address(interface: str = None) -> str:
"""获取MAC地址"""
if interface is None:
ifaces = _list_dir("/sys/class/net/")
if ifaces and "lo" in ifaces:
ifaces.remove("lo")
if ifaces:
interface = ifaces[0]
if interface:
return _read_file(f"/sys/class/net/{interface}/address") or ""
return ""
@staticmethod
def get_rx_tx_bytes(interface: str = None) -> dict:
"""获取网络收发字节数"""
if interface is None:
ifaces = _list_dir("/sys/class/net/")
if "lo" in ifaces:
ifaces.remove("lo")
if ifaces:
interface = ifaces[0]
if interface:
rx = _read_file_int(f"/sys/class/net/{interface}/statistics/rx_bytes") or 0
tx = _read_file_int(f"/sys/class/net/{interface}/statistics/tx_bytes") or 0
return {"rx_bytes": rx, "tx_bytes": tx, "interface": interface}
return {"rx_bytes": 0, "tx_bytes": 0, "interface": ""}
# ═══════════════════════════════════════════════════════════════
# 蓝牙 — 真实实现 (bluetoothctl)
# ═══════════════════════════════════════════════════════════════
class LinuxBluetooth:
"""真实蓝牙操作 — 通过 bluetoothctl"""
STATE_OFF = 10
STATE_TURNING_ON = 11
STATE_ON = 12
STATE_TURNING_OFF = 13
@staticmethod
def get_state() -> int:
if not HAS_BLUETOOTHCTL:
return LinuxBluetooth.STATE_OFF
code, out, _ = _run(["bluetoothctl", "show"])
if "Powered: yes" in out:
return LinuxBluetooth.STATE_ON
return LinuxBluetooth.STATE_OFF
@staticmethod
def enable() -> bool:
if not HAS_BLUETOOTHCTL:
return False
code, _, _ = _run(["bluetoothctl", "power", "on"])
return code == 0
@staticmethod
def disable() -> bool:
if not HAS_BLUETOOTHCTL:
return False
code, _, _ = _run(["bluetoothctl", "power", "off"])
return code == 0
@staticmethod
def get_bonded_devices() -> List[dict]:
if not HAS_BLUETOOTHCTL:
return []
code, out, _ = _run(["bluetoothctl", "devices", "Paired"])
devices = []
for line in out.split("\n"):
if line.startswith("Device "):
parts = line.split(None, 2)
if len(parts) >= 3:
devices.append({
"address": parts[1],
"name": parts[2],
"bonded": True,
})
return devices
@staticmethod
def scan(timeout: int = 10) -> List[dict]:
if not HAS_BLUETOOTHCTL:
return []
# 启动扫描
_run(["bluetoothctl", "scan", "on"], timeout=2)
time.sleep(min(timeout, 5))
_run(["bluetoothctl", "scan", "off"], timeout=2)
# 获取设备列表
code, out, _ = _run(["bluetoothctl", "devices"])
devices = []
for line in out.split("\n"):
if line.startswith("Device "):
parts = line.split(None, 2)
if len(parts) >= 3:
devices.append({
"address": parts[1],
"name": parts[2],
})
return devices
@staticmethod
def pair(address: str) -> bool:
if not HAS_BLUETOOTHCTL:
return False
code, _, _ = _run(["bluetoothctl", "pair", address], timeout=30)
return code == 0
@staticmethod
def connect(address: str) -> bool:
if not HAS_BLUETOOTHCTL:
return False
code, _, _ = _run(["bluetoothctl", "connect", address], timeout=15)
return code == 0
@staticmethod
def disconnect(address: str) -> bool:
if not HAS_BLUETOOTHCTL:
return False
code, _, _ = _run(["bluetoothctl", "disconnect", address], timeout=10)
return code == 0
@staticmethod
def unpair(address: str) -> bool:
if not HAS_BLUETOOTHCTL:
return False
code, _, _ = _run(["bluetoothctl", "remove", address], timeout=10)
return code == 0
# ═══════════════════════════════════════════════════════════════
# 传感器 — 真实实现 (/sys/class/hwmon, /sys/class/thermal)
# ═══════════════════════════════════════════════════════════════
class LinuxSensors:
"""真实传感器数据 — 通过 /sys 文件系统"""
# Android Sensor Types
TYPE_ACCELEROMETER = 1
TYPE_GYROSCOPE = 4
TYPE_LIGHT = 5
TYPE_PRESSURE = 6
TYPE_PROXIMITY = 8
TYPE_TEMPERATURE = 7
TYPE_RELATIVE_HUMIDITY = 12
TYPE_AMBIENT_TEMPERATURE = 13
@staticmethod
def get_temperatures() -> List[dict]:
"""获取温度传感器读数"""
temps = []
# /sys/class/thermal
for name in _list_dir("/sys/class/thermal/"):
if name.startswith("thermal_zone"):
temp = _read_file_int(f"/sys/class/thermal/{name}/temp")
typ = _read_file(f"/sys/class/thermal/{name}/type") or ""
if temp is not None:
temps.append({
"name": name,
"type": typ,
"value": temp / 1000.0, # millicelsius -> celsius
"unit": "°C",
})
# /sys/class/hwmon
for name in _list_dir("/sys/class/hwmon/"):
temp_files = [f for f in _list_dir(f"/sys/class/hwmon/{name}/")
if f.startswith("temp") and f.endswith("_input")]
for tf in temp_files:
temp = _read_file_int(f"/sys/class/hwmon/{name}/{tf}")
label_file = tf.replace("_input", "_label")
label = _read_file(f"/sys/class/hwmon/{name}/{label_file}") or tf
if temp is not None:
temps.append({
"name": f"{name}/{label}",
"type": "hwmon",
"value": temp / 1000.0,
"unit": "°C",
})
return temps
@staticmethod
def get_fan_speeds() -> List[dict]:
"""获取风扇转速"""
fans = []
for name in _list_dir("/sys/class/hwmon/"):
fan_files = [f for f in _list_dir(f"/sys/class/hwmon/{name}/")
if f.startswith("fan") and f.endswith("_input")]
for ff in fan_files:
rpm = _read_file_int(f"/sys/class/hwmon/{name}/{ff}")
label_file = ff.replace("_input", "_label")
label = _read_file(f"/sys/class/hwmon/{name}/{label_file}") or ff
if rpm is not None:
fans.append({"name": label, "rpm": rpm})
return fans
@staticmethod
def get_cpu_frequency() -> List[dict]:
"""获取CPU频率"""
freqs = []
for name in _list_dir("/sys/devices/system/cpu/"):
if name.startswith("cpu") and name[3:].isdigit():
cpu_id = name[3:]
cur = _read_file_int(f"/sys/devices/system/cpu/{name}/cpufreq/scaling_cur_freq")
if cur is not None:
freqs.append({
"cpu": int(cpu_id),
"freq_mhz": cur / 1000.0,
})
return freqs
@staticmethod
def get_light_level() -> Optional[float]:
"""获取环境光传感器 (如果有)"""
for name in _list_dir("/sys/class/hwmon/"):
for f in _list_dir(f"/sys/class/hwmon/{name}/"):
if "in" in f and f.endswith("_input"):
val = _read_file_int(f"/sys/class/hwmon/{name}/{f}")
if val is not None:
return val / 1000.0
return None
@staticmethod
def get_all_sensors() -> dict:
"""获取所有传感器数据"""
return {
"temperatures": LinuxSensors.get_temperatures(),
"fans": LinuxSensors.get_fan_speeds(),
"cpu_freq": LinuxSensors.get_cpu_frequency(),
"light": LinuxSensors.get_light_level(),
}
# ═══════════════════════════════════════════════════════════════
# 音频 — 真实实现 (amixer / ALSA)
# ═══════════════════════════════════════════════════════════════
class LinuxAudio:
"""真实音频控制 — 通过 amixer"""
@staticmethod
def get_volume() -> int:
"""获取当前音量 (0-100)"""
if not HAS_AMIXER:
return 0
code, out, _ = _run(["amixer", "sget", "Master"])
if code == 0:
match = re.search(r'\[(\d+)%\]', out)
if match:
return int(match.group(1))
return 0
@staticmethod
def set_volume(volume: int) -> bool:
"""设置音量 (0-100)"""
if not HAS_AMIXER:
return False
volume = max(0, min(100, volume))
code, _, _ = _run(["amixer", "sset", "Master", f"{volume}%"])
return code == 0
@staticmethod
def get_mute() -> bool:
"""获取静音状态"""
if not HAS_AMIXER:
return False
code, out, _ = _run(["amixer", "sget", "Master"])
if code == 0:
return "[on]" not in out
return False
@staticmethod
def set_mute(mute: bool) -> bool:
"""设置静音"""
if not HAS_AMIXER:
return False
cmd = "mute" if mute else "unmute"
code, _, _ = _run(["amixer", "sset", "Master", cmd])
return code == 0
@staticmethod
def get_audio_devices() -> List[dict]:
"""获取音频设备列表"""
devices = []
if HAS_AMIXER:
code, out, _ = _run(["amixer", "controls"])
if code == 0:
for line in out.split("\n"):
devices.append({"name": line, "type": "alsa"})
return devices
# ═══════════════════════════════════════════════════════════════
# 剪贴板 — 真实实现 (xclip)
# ═══════════════════════════════════════════════════════════════
class LinuxClipboard:
"""真实剪贴板 — 通过 xclip"""
@staticmethod
def read() -> str:
if HAS_XCLIP:
code, out, _ = _run(["xclip", "-selection", "clipboard", "-o"])
if code == 0:
return out
# 回退到 tkinter
try:
import tkinter
root = tkinter.Tk()
root.withdraw()
text = root.clipboard_get()
root.destroy()
return text
except Exception:
return ""
@staticmethod
def write(text: str) -> bool:
if HAS_XCLIP:
try:
proc = subprocess.Popen(
["xclip", "-selection", "clipboard"],
stdin=subprocess.PIPE, text=True
)
proc.communicate(text)
return proc.returncode == 0
except Exception:
pass
# 回退到 tkinter
try:
import tkinter
root = tkinter.Tk()
root.withdraw()
root.clipboard_clear()
root.clipboard_append(text)
root.update()
root.destroy()
return True
except Exception:
return False
# ═══════════════════════════════════════════════════════════════
# 电源/电池 — 真实实现 (/sys/class/power_supply, psutil)
# ═══════════════════════════════════════════════════════════════
class LinuxPower:
"""真实电源/电池信息"""
BATTERY_STATUS_UNKNOWN = 1
BATTERY_STATUS_CHARGING = 2
BATTERY_STATUS_DISCHARGING = 3
BATTERY_STATUS_NOT_CHARGING = 4
BATTERY_STATUS_FULL = 5
@staticmethod
def get_battery_info() -> dict:
"""获取电池信息"""
info = {"present": False, "level": -1, "status": "unknown",
"temperature": 0, "voltage": 0, "technology": ""}
# /sys/class/power_supply
for name in _list_dir("/sys/class/power_supply/"):
typ = _read_file(f"/sys/class/power_supply/{name}/type") or ""
if "Battery" in typ:
info["present"] = True
info["technology"] = _read_file(f"/sys/class/power_supply/{name}/technology") or ""
capacity = _read_file_int(f"/sys/class/power_supply/{name}/capacity")
if capacity is not None:
info["level"] = capacity
status = _read_file(f"/sys/class/power_supply/{name}/status") or ""
info["status"] = status.lower()
# 电压 (微伏)
volt = _read_file_int(f"/sys/class/power_supply/{name}/voltage_now")
if volt is not None:
info["voltage"] = volt / 1000000.0
# 温度 (十分之一度)
temp = _read_file_int(f"/sys/class/power_supply/{name}/temp")
if temp is not None:
info["temperature"] = temp / 10.0
return info
# psutil 回退
if HAS_PSUTIL:
try:
battery = psutil.sensors_battery()
if battery:
info["present"] = True
info["level"] = battery.percent
info["status"] = "charging" if battery.power_plugged else "discharging"
except Exception:
pass
return info
@staticmethod
def get_power_status() -> dict:
"""获取电源状态"""
info = LinuxPower.get_battery_info()
return {
"battery_present": info["present"],
"battery_level": info["level"],
"battery_status": info["status"],
"is_charging": "charging" in info["status"],
"temperature": info["temperature"],
"voltage": info["voltage"],
}
@staticmethod
def get_cpu_usage() -> float:
"""获取CPU使用率"""
if HAS_PSUTIL:
return psutil.cpu_percent(interval=0.5)
# /proc/stat 回退
def read_cpu():
data = _read_file("/proc/stat")
if data:
parts = data.split("\n")[0].split()[1:]
return [int(x) for x in parts]
return [0] * 10
c1 = read_cpu()
time.sleep(0.5)
c2 = read_cpu()
idle = c2[3] - c1[3]
total = sum(c2) - sum(c1)
if total > 0:
return (1 - idle / total) * 100
return 0.0
@staticmethod
def get_memory_info() -> dict:
"""获取内存信息"""
if HAS_PSUTIL:
mem = psutil.virtual_memory()
return {
"total": mem.total,
"available": mem.available,
"used": mem.used,
"free": mem.free,
"percent": mem.percent,
}
# /proc/meminfo 回退
data = _read_file("/proc/meminfo")
info = {}
if data:
for line in data.split("\n"):
parts = line.split()
if len(parts) >= 2:
info[parts[0].rstrip(":")] = int(parts[1]) * 1024
return {
"total": info.get("MemTotal", 0),
"available": info.get("MemAvailable", 0),
"used": info.get("MemTotal", 0) - info.get("MemAvailable", 0),
"free": info.get("MemFree", 0),
"percent": 0,
}
# ═══════════════════════════════════════════════════════════════
# 位置 — 真实实现 (IP 地理定位)
# ═══════════════════════════════════════════════════════════════
class LinuxLocation:
"""真实位置信息 — 通过 IP 地理定位"""
_cached_location = None
_cache_time = 0
@staticmethod
def get_location() -> dict:
"""通过 IP 获取地理位置"""
if not HAS_REQUESTS:
return {"latitude": 0.0, "longitude": 0.0, "accuracy": 0, "source": "none"}
# 缓存 5 分钟
now = time.time()
if LinuxLocation._cached_location and now - LinuxLocation._cache_time < 300:
return LinuxLocation._cached_location
# 尝试多个 IP 定位服务
services = [
("http://ip-api.com/json/", "ip-api"),
("https://ipapi.co/json/", "ipapi"),
("https://freeipapi.com/api/json", "freeipapi"),
]
for url, source in services:
try:
resp = requests.get(url, timeout=5)
if resp.status_code == 200:
data = resp.json()
lat = data.get("lat") or data.get("latitude") or 0.0
lon = data.get("lon") or data.get("longitude") or 0.0
location = {
"latitude": float(lat),
"longitude": float(lon),
"accuracy": 50000, # IP定位精度约 50km
"source": source,
"city": data.get("city", ""),
"region": data.get("regionName", data.get("region", "")),
"country": data.get("country", data.get("country_name", "")),
"country_code": data.get("countryCode", data.get("country", "")),
"timezone": data.get("timezone", ""),
"isp": data.get("isp", ""),
}
LinuxLocation._cached_location = location
LinuxLocation._cache_time = now
return location
except Exception:
continue
return {"latitude": 0.0, "longitude": 0.0, "accuracy": 0, "source": "failed"}
# ═══════════════════════════════════════════════════════════════
# 通知 — 真实实现 (notify-send)
# ═══════════════════════════════════════════════════════════════
class LinuxNotify:
"""真实桌面通知 — 通过 notify-send"""
@staticmethod
def send(title: str, text: str, icon: str = None, urgency: str = "normal",
timeout: int = -1) -> bool:
if not HAS_NOTIFY_SEND:
# 回退到 stderr
print(f"[{title}] {text}", file=sys.stderr)
return False
cmd = ["notify-send"]
cmd.extend(["-u", urgency])
if timeout >= 0:
cmd.extend(["-t", str(timeout)])
if icon:
cmd.extend(["-i", icon])
cmd.extend([title, text])
code, _, _ = _run(cmd)
return code == 0
# ═══════════════════════════════════════════════════════════════
# 显示/屏幕 — 真实实现 (xdotool, X11)
# ═══════════════════════════════════════════════════════════════
class LinuxDisplay:
"""真实显示/屏幕信息"""
@staticmethod
def get_screen_size() -> Tuple[int, int]:
"""获取屏幕尺寸"""
if HAS_XDOTOOL:
code, out, _ = _run(["xdotool", "getdisplaygeometry"])
if code == 0:
parts = out.split()
if len(parts) >= 2:
return int(parts[0]), int(parts[1])
if HAS_XRANDR:
code, out, _ = _run(["xrandr", "--current"])
if code == 0:
for line in out.split("\n"):
if " connected" in line and "*" in line:
match = re.search(r'(\d+)x(\d+)', line)
if match:
return int(match.group(1)), int(match.group(2))
# 回退
try:
import tkinter
root = tkinter.Tk()
w = root.winfo_screenwidth()
h = root.winfo_screenheight()
root.destroy()
return w, h
except Exception:
return 1920, 1080
@staticmethod
def screenshot(path: str = None) -> Optional[str]:
"""截屏"""
if HAS_PIL:
try:
img = ImageGrab.grab()
if path:
img.save(path)
return path
else:
buf = f"/tmp/screenshot_{int(time.time())}.png"
img.save(buf)
return buf
except Exception:
pass
if HAS_XDOTOOL:
# 用 xdotool + import
if not path:
path = f"/tmp/screenshot_{int(time.time())}.png"
code, _, _ = _run(["import", "-window", "root", path])
if code == 0:
return path
return None
@staticmethod
def get_mouse_position() -> Tuple[int, int]:
"""获取鼠标位置"""
if HAS_XDOTOOL:
code, out, _ = _run(["xdotool", "getmouselocation"])
if code == 0:
parts = out.split()
x = int(parts[0].split(":")[1])
y = int(parts[1].split(":")[1])
return x, y
return 0, 0
# ═══════════════════════════════════════════════════════════════
# UI 自动化 — 真实实现 (xdotool)
# ═══════════════════════════════════════════════════════════════
class LinuxUIAutomation:
"""真实 UI 自动化 — 通过 xdotool"""
@staticmethod
def click(x: int, y: int) -> bool:
if not HAS_XDOTOOL:
return False
code, _, _ = _run(["xdotool", "mousemove", str(x), str(y), "click", "1"])
return code == 0
@staticmethod
def double_click(x: int, y: int) -> bool:
if not HAS_XDOTOOL:
return False
code, _, _ = _run(["xdotool", "mousemove", str(x), str(y), "click", "--repeat", "2", "1"])
return code == 0
@staticmethod
def right_click(x: int, y: int) -> bool:
if not HAS_XDOTOOL:
return False
code, _, _ = _run(["xdotool", "mousemove", str(x), str(y), "click", "3"])
return code == 0
@staticmethod
def type_text(text: str) -> bool:
if not HAS_XDOTOOL:
return False
code, _, _ = _run(["xdotool", "type", "--clearmodifiers", text])
return code == 0
@staticmethod
def press_key(key: str) -> bool:
if not HAS_XDOTOOL:
return False
# 转换常见键名
key_map = {
"enter": "Return", "return": "Return",
"tab": "Tab", "escape": "Escape", "esc": "Escape",
"backspace": "BackSpace", "delete": "Delete",
"space": "space", "up": "Up", "down": "Down",
"left": "Left", "right": "Right",
"home": "Home", "end": "End",
"page_up": "Page_Up", "page_down": "Page_Down",
}
xdotool_key = key_map.get(key.lower(), key)
code, _, _ = _run(["xdotool", "key", xdotool_key])
return code == 0
@staticmethod
def hotkey(*keys) -> bool:
if not HAS_XDOTOOL:
return False
key_map = {
"ctrl": "ctrl", "control": "ctrl",
"alt": "alt", "shift": "shift",
"super": "super", "win": "super", "meta": "super",
}
mapped = [key_map.get(k.lower(), k) for k in keys]
combo = "+".join(mapped)
code, _, _ = _run(["xdotool", "key", combo])
return code == 0
@staticmethod
def scroll(amount: int) -> bool:
if not HAS_XDOTOOL:
return False
button = "4" if amount > 0 else "5"
count = abs(amount)
code, _, _ = _run(["xdotool", "click", "--repeat", str(count), button])
return code == 0
@staticmethod
def find_window(title: str) -> Optional[int]:
"""查找窗口"""
if not HAS_XDOTOOL:
return None
code, out, _ = _run(["xdotool", "search", "--name", title])
if code == 0 and out:
return int(out.split("\n")[0])
return None
@staticmethod
def activate_window(window_id: int) -> bool:
if not HAS_XDOTOOL:
return False
code, _, _ = _run(["xdotool", "windowactivate", str(window_id)])
return code == 0
@staticmethod
def close_window(window_id: int) -> bool:
if not HAS_XDOTOOL:
return False
code, _, _ = _run(["xdotool", "windowclose", str(window_id)])
return code == 0
# ═══════════════════════════════════════════════════════════════
# 摄像头 — 真实实现 (opencv/V4L2)
# ═══════════════════════════════════════════════════════════════
class LinuxCamera:
"""真实摄像头 — 通过 OpenCV/V4L2"""
@staticmethod
def list_cameras() -> List[int]:
"""列出可用摄像头"""
cameras = []
for dev in _list_dir("/dev/"):
if dev.startswith("video"):
try:
idx = int(dev[5:])
cameras.append(idx)
except ValueError:
pass
return cameras
@staticmethod
def capture(camera_id: int = 0, output_path: str = None) -> Optional[str]:
"""拍照"""
if not HAS_CV2:
return None
cap = cv2.VideoCapture(camera_id)
if not cap.isOpened():
return None
ret, frame = cap.read()
cap.release()
if not ret:
return None
if not output_path:
output_path = f"/tmp/camera_{int(time.time())}.jpg"
cv2.imwrite(output_path, frame)
return output_path
@staticmethod
def get_camera_info(camera_id: int = 0) -> dict:
"""获取摄像头信息"""
info = {"id": camera_id, "available": False, "width": 0, "height": 0}
if HAS_CV2:
cap = cv2.VideoCapture(camera_id)
if cap.isOpened():
info["available"] = True
info["width"] = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
info["height"] = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
info["fps"] = cap.get(cv2.CAP_PROP_FPS)
cap.release()
return info
# ═══════════════════════════════════════════════════════════════
# 进程/应用管理 — 真实实现 (psutil, /proc)
# ═══════════════════════════════════════════════════════════════
class LinuxProcess:
"""真实进程/应用管理"""
@staticmethod
def list_processes() -> List[dict]:
"""列出所有进程"""
if HAS_PSUTIL:
procs = []
for p in psutil.process_iter(['pid', 'name', 'username', 'memory_percent',
'cpu_percent', 'status', 'cmdline']):
try:
info = p.info
procs.append({
"pid": info['pid'],
"name": info['name'],
"user": info['username'],
"memory": info.get('memory_percent', 0),
"cpu": info.get('cpu_percent', 0),
"status": info['status'],
"cmdline": " ".join(info.get('cmdline') or []),
})
except (psutil.NoSuchProcess, psutil.AccessDenied):
pass
return procs
# /proc 回退
procs = []
for name in _list_dir("/proc/"):
if name.isdigit():
pid = int(name)
comm = _read_file(f"/proc/{name}/comm") or ""
cmdline = _read_file(f"/proc/{name}/cmdline") or ""
status = _read_file(f"/proc/{name}/status") or ""
user = ""
for line in status.split("\n"):
if line.startswith("Uid:"):
uid = line.split()[1]
try:
import pwd
user = pwd.getpwuid(int(uid)).pw_name
except Exception:
user = uid
break
procs.append({
"pid": pid, "name": comm, "user": user,
"cmdline": cmdline.replace("\x00", " "),
"status": "", "memory": 0, "cpu": 0,
})
return procs
@staticmethod
def get_running_apps() -> List[dict]:
"""获取运行中的应用 (GUI 窗口)"""
if not HAS_XDOTOOL:
return []
code, out, _ = _run(["xdotool", "search", "--onlyvisible", "--class", ""])
apps = []
if code == 0:
seen = set()
for wid in out.split("\n"):
if wid and wid not in seen:
seen.add(wid)
# 获取窗口名称
code2, name, _ = _run(["xdotool", "getwindowname", wid])
if name:
apps.append({"window_id": int(wid), "name": name})
return apps
@staticmethod
def kill_process(pid: int) -> bool:
"""终止进程"""
if HAS_PSUTIL:
try:
psutil.Process(pid).terminate()
return True
except Exception:
return False
code, _, _ = _run(["kill", str(pid)])
return code == 0
@staticmethod
def start_app(name: str) -> bool:
"""启动应用"""
code, _, _ = _run([name], timeout=1)
# 非阻塞启动
try:
subprocess.Popen([name], stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL)
return True
except Exception:
return False
@staticmethod
def get_system_info() -> dict:
"""获取系统信息"""
info = {
"platform": platform.system(),
"hostname": socket.gethostname(),
"kernel": platform.release(),
"arch": platform.machine(),
"python": platform.python_version(),
}
if HAS_PSUTIL:
info["cpu_count"] = psutil.cpu_count()
info["cpu_freq"] = psutil.cpu_freq().current if psutil.cpu_freq() else 0
boot_time = psutil.boot_time()
info["uptime"] = time.time() - boot_time
mem = psutil.virtual_memory()
info["memory_total"] = mem.total
info["memory_used"] = mem.used
info["memory_percent"] = mem.percent
disk = psutil.disk_usage('/')
info["disk_total"] = disk.total
info["disk_used"] = disk.used
info["disk_percent"] = disk.percent
return info
# ═══════════════════════════════════════════════════════════════
# Shell — 真实实现
# ═══════════════════════════════════════════════════════════════
class LinuxShell:
"""真实 Shell 执行"""
@staticmethod
def exec(command: str, timeout: int = 30, root: bool = False) -> dict:
"""执行 Shell 命令"""
if root and os.geteuid() != 0:
command = f"sudo {command}"
code, out, err = _run(command, timeout=timeout)
return {
"success": code == 0,
"output": out + ("\n" + err if err else ""),
"exit_code": code,
"stdout": out,
"stderr": err,
}
@staticmethod
def exec_async(command: str) -> int:
"""异步执行命令,返回 PID"""
proc = subprocess.Popen(command, shell=True,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL)
return proc.pid
# ═══════════════════════════════════════════════════════════════
# 统一入口
# ═══════════════════════════════════════════════════════════════
def get_linux_capabilities() -> dict:
"""获取当前 Linux 环境的可用能力"""
return {
"is_linux": IS_LINUX,
"network": HAS_IP,
"bluetooth": HAS_BLUETOOTHCTL,
"audio": HAS_AMIXER,
"clipboard": HAS_XCLIP,
"ui_automation": HAS_XDOTOOL,
"notifications": HAS_NOTIFY_SEND,
"sensors": True, # /sys 总是可用
"display": True,
"camera": HAS_CV2,
"ffmpeg": HAS_FFMPEG,
"ffprobe": HAS_FFPROBE,
"psutil": HAS_PSUTIL,
"requests": HAS_REQUESTS,
"PIL": HAS_PIL,
"opencv": HAS_CV2,
"numpy": HAS_NUMPY,
"dbus": HAS_DBUS,
"xrandr": HAS_XRANDR,
}
# 导出所有实现类
__all__ = [
"IS_LINUX", "get_linux_capabilities",
"LinuxNetwork", "LinuxBluetooth", "LinuxSensors",
"LinuxAudio", "LinuxClipboard", "LinuxPower",
"LinuxLocation", "LinuxNotify", "LinuxDisplay",
"LinuxUIAutomation", "LinuxCamera", "LinuxProcess",
"LinuxShell",
# 能力标志
"HAS_IP", "HAS_BLUETOOTHCTL", "HAS_AMIXER", "HAS_XCLIP",
"HAS_XDOTOOL", "HAS_NOTIFY_SEND", "HAS_SENSORS",
"HAS_FFMPEG", "HAS_FFPROBE", "HAS_PSUTIL", "HAS_REQUESTS",
"HAS_CV2", "HAS_NUMPY", "HAS_PIL", "HAS_DBUS", "HAS_XRANDR",
]