SQLite真实数据库: - SQLiteDatabase: execSQL/insert/insertOrThrow/replace/update/delete/query 用sqlite3 - 修复getVersion/setVersion用PRAGMA user_version - 修复getMaximumSize/getPageSize/setMaximumSize/setPageSize用PRAGMA - SQLiteStatement: execute/simpleQueryForLong/simpleQueryForString/execQuery - SQLiteOpenHelper: onCreate/onUpgrade/onDowngrade回调正确触发 - 修复SQLiteCursor.moveToPosition越界处理 - 修复ContentValues值对提取bug JSON真实实现: - JSONObject: isNull/append/putOnce/putOpt/getNames - JSONArray: 完整put/get/opt/length/toString 网络真实实现: - NetworkInterface: getByName/getNetworkInterfaces/getHardwareAddress 用/sys/class/net+fcntl.ioctl - InterfaceAddress: 新增类 - ConnectivityManager: getActiveNetwork/getAllNetworks 用LinuxNetwork - WifiManager: getScanResults用nmcli, setWifiEnabled用nmcli/rfkill - BluetoothAdapter: getAddress/getName 用bluetoothctl 媒体真实实现: - MediaPlayer: prepare检查文件, getCurrentPosition实时计算, getDuration/getVideoWidth用ffprobe - MediaRecorder: setVideoSize/setVideoFrameRate/setVideoEncodingBitRate配置存储 - MediaMetadataRetriever: extractMetadata用ffprobe, getFrameAtTime用ffmpeg, getEmbeddedPicture用ffmpeg 文本真实实现: - TextUtils: split用re.split, concat/indexOf/lastIndexOf/urlEncode/urlDecode - SpannableString: setSpan/getSpans/getSpanFlags/nextSpanTransition 真实实现 - SpannableStringBuilder: 修复append重载bug Provider增强: - Settings: putBoolean/getBoolean - MediaStore: getContentUri/VOLUME常量 - ContactsContract: 补全常量和CommonDataKinds Util增强: - Log: w/e/wtf支持Throwable参数, 修复wtf重复定义bug - TypedValue: 修复complexToDimension/complexToFloat/getFloat实现 修复: - __init__.py: 补全NetworkInterface/InetAddress/MediaMetadataRetriever/SpannableStringBuilder导出 统计: 221模块, 3666类, 31005方法, 183529行
1267 lines
45 KiB
Python
1267 lines
45 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 get_address() -> Optional[str]:
|
|
"""获取本机蓝牙适配器地址"""
|
|
if not HAS_BLUETOOTHCTL:
|
|
return None
|
|
code, out, _ = _run(["bluetoothctl", "show"])
|
|
if code == 0 and out:
|
|
for line in out.split("\n"):
|
|
line = line.strip()
|
|
if line.startswith("Controller "):
|
|
parts = line.split()
|
|
if len(parts) >= 2:
|
|
return parts[1]
|
|
return None
|
|
|
|
@staticmethod
|
|
def get_name() -> Optional[str]:
|
|
"""获取本机蓝牙适配器名称 (Alias 优先, 回退到 Controller 行括号内名称)"""
|
|
if not HAS_BLUETOOTHCTL:
|
|
return None
|
|
code, out, _ = _run(["bluetoothctl", "show"])
|
|
if code == 0 and out:
|
|
alias = None
|
|
controller_name = None
|
|
for line in out.split("\n"):
|
|
stripped = line.strip()
|
|
if stripped.startswith("Controller ") and "(" in stripped:
|
|
start = stripped.find("(")
|
|
end = stripped.find(")", start)
|
|
if start != -1 and end != -1:
|
|
controller_name = stripped[start + 1:end]
|
|
elif stripped.startswith("Alias:"):
|
|
alias = stripped[len("Alias:"):].strip()
|
|
if alias:
|
|
return alias
|
|
if controller_name:
|
|
return controller_name
|
|
return None
|
|
|
|
@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",
|
|
]
|