Release Vulpes OS 2.7-preview.24

This commit is contained in:
OverSu 2026-11-04 12:52:20 +01:00
commit 97ef99f827
135 changed files with 7717 additions and 2524 deletions

View file

@ -2,6 +2,7 @@
# SPDX-License-Identifier: MPL-2.0
"""Read readiness, never dial, send messages, enter PINs or open camera nodes."""
import json
import os
from pathlib import Path
import subprocess
import time
@ -38,6 +39,150 @@ def selected(values, names):
if key in values and isinstance(values[key], dict)}
def process_cpu_snapshot():
"""Return process names and CPU ticks without command lines or arguments."""
result = {}
for entry in Path('/proc').iterdir():
if not entry.name.isdigit():
continue
try:
raw = (entry / 'stat').read_text()
# comm can contain spaces and parentheses; fields after the last )
# start at field 3 (state), so utime/stime are offsets 11 and 12.
fields = raw[raw.rfind(')') + 2:].split()
name = raw[raw.find('(') + 1:raw.rfind(')')]
result[int(entry.name)] = (name,
int(fields[11]) + int(fields[12]))
except (OSError, ValueError, IndexError):
continue
return result
def thread_cpu_snapshot():
"""Return thread names and CPU ticks; never collect command-line data."""
result = {}
for task in Path('/proc').glob('[0-9]*/task/[0-9]*'):
try:
raw = (task/'stat').read_text()
fields = raw[raw.rfind(')') + 2:].split()
name = raw[raw.find('(') + 1:raw.rfind(')')]
result[(int(task.parent.parent.name), int(task.name))] = (
name, int(fields[11]) + int(fields[12]))
except (OSError, ValueError, IndexError):
continue
return result
def cpu_idle_snapshot():
"""Read per-CPU idle-state residency counters where the kernel exports them."""
result = {}
for cpu in sorted(Path('/sys/devices/system/cpu').glob('cpu[0-9]*')):
if not cpu.name[3:].isdigit():
continue
states = []
for state in sorted((cpu/'cpuidle').glob('state[0-9]*')):
try:
states.append({key:(state/key).read_text().strip() for key in
('name','desc','usage','time','disable')
if (state/key).is_file()})
except OSError:
continue
if states:
result[cpu.name] = states
return result
def cpu_frequency_snapshot():
result = {}
for cpu in sorted(Path('/sys/devices/system/cpu').glob('cpu[0-9]*')):
if not cpu.name[3:].isdigit():
continue
path = cpu/'cpufreq'
values = {}
for key in ('scaling_governor','scaling_cur_freq','cpuinfo_cur_freq',
'scaling_min_freq','scaling_max_freq'):
try:
values[key] = (path/key).read_text().strip()
except OSError:
pass
if values:
result[cpu.name] = values
return result
def thermal_readings():
readings = {}
for zone in Path('/sys/class/thermal').glob('thermal_zone*'):
try:
label = (zone / 'type').read_text().strip()
value = int((zone / 'temp').read_text().strip())
# Linux thermal sysfs normally reports millidegrees Celsius.
readings[label or zone.name] = round(value / 1000, 1)
except (OSError, ValueError):
continue
return readings
def power_supplies():
result = {}
for supply in Path('/sys/class/power_supply').glob('*'):
data = {}
for name in ('type', 'status', 'capacity', 'voltage_now',
'current_now', 'power_now', 'temp', 'online'):
try:
data[name] = (supply / name).read_text().strip()
except OSError:
pass
if data:
result[supply.name] = data
return result
def wakeup_snapshot():
"""Read kernel wakeup counters; source labels are kernel driver names."""
for path in (Path('/sys/kernel/debug/wakeup_sources'),
Path('/sys/kernel/tracing/wakeup_sources')):
try:
lines = path.read_text().splitlines()
except OSError:
continue
if len(lines) < 2:
continue
headers = lines[0].split()
active_index = next((i for i, value in enumerate(headers)
if value.lower() == 'active_count'), None)
event_index = next((i for i, value in enumerate(headers)
if value.lower() == 'event_count'), None)
if active_index is None or event_index is None:
return None
result = {}
for line in lines[1:]:
fields = line.split()
try:
if len(fields) <= max(active_index, event_index):
continue
result[fields[0]] = {
'activeCount':int(fields[active_index]),
'eventCount':int(fields[event_index]),
}
except (ValueError, IndexError):
continue
return result
return None
def counter_delta(before, after, keys):
"""Return positive counter deltas, tolerating counters reset at reboot."""
result = {}
for name, values in (after or {}).items():
prior = (before or {}).get(name, {})
delta = {key:max(0, int(values[key])-int(prior.get(key, values[key])))
for key in keys if key in values}
if any(delta.values()):
result[name] = delta
return result
def inspect():
native = 'tundra.rescue=1' in Path('/proc/cmdline').read_text()
report = {'recordedAtUnix': time.time(), 'environment': 'tundra' if native else 'reference',
@ -80,12 +225,114 @@ def inspect():
'applicationInstalled': Path('/usr/bin/droidian-camera').exists(),
'captureTested': False,
}
report['battery'] = {}
for name in ('capacity', 'status'):
report['battery'] = power_supplies()
report['thermalCelsius'] = thermal_readings()
backlights = {}
for path in Path('/sys/class/backlight').glob('*/brightness'):
try:
report['battery'][name] = Path('/sys/class/power_supply/battery', name).read_text().strip()
backlights[path.parent.name] = (path.read_text().strip(),
(path.parent / 'max_brightness').read_text().strip())
except OSError:
continue
report['backlight'] = backlights
before = process_cpu_snapshot()
threads_before = thread_cpu_snapshot()
idle_before = cpu_idle_snapshot()
wakeups_before = wakeup_snapshot()
battery_before = power_supplies()
started = time.monotonic()
time.sleep(5)
elapsed = time.monotonic() - started
after = process_cpu_snapshot()
threads_after = thread_cpu_snapshot()
ticks_per_second = os.sysconf('SC_CLK_TCK')
cpu = []
for pid, (name, ticks) in after.items():
if pid not in before or before[pid][0] != name:
continue
delta = max(0, ticks - before[pid][1])
percent = 100 * delta / ticks_per_second / elapsed
if percent >= 1:
cpu.append({'process': name, 'percentOfOneCore': round(percent, 1)})
report['cpuSample'] = {'durationSeconds': round(elapsed, 2),
'busyProcesses': sorted(cpu,
key=lambda item: item['percentOfOneCore'],
reverse=True)[:12]}
busy_threads = []
for key, (name, ticks) in threads_after.items():
prior = threads_before.get(key)
if prior is None or prior[0] != name:
continue
percent = 100 * max(0, ticks-prior[1]) / ticks_per_second / elapsed
if percent >= 1:
busy_threads.append({'processId':key[0], 'threadId':key[1],
'thread':name,
'percentOfOneCore':round(percent,1)})
report['cpuSample']['busyThreads'] = sorted(
busy_threads, key=lambda item:item['percentOfOneCore'], reverse=True)[:20]
idle_after = cpu_idle_snapshot()
residency = {}
for cpu_name, states in idle_after.items():
previous = idle_before.get(cpu_name, [])
before_by_name = {state.get('name'):state for state in previous}
deltas = []
for state in states:
prior = before_by_name.get(state.get('name'), {})
item = {'name':state.get('name')}
for key in ('usage','time'):
if key in state:
try:
item[key+'Delta'] = max(0,int(state[key])-int(prior.get(key,state[key])))
except ValueError:
pass
if 'disable' in state:
item['disabled'] = state['disable']
deltas.append(item)
if deltas:
residency[cpu_name] = deltas
report['cpuIdle'] = {'available':bool(residency),
'statesDuringSample':residency,
'frequencyAfterSample':cpu_frequency_snapshot(),
'kernelCommandLineFlags':[
token for token in Path('/proc/cmdline').read_text().split()
if token.startswith(('lpm_levels.','cpuidle.','nohz='))],
'lowPowerParameter':None,
'systemSuspend':{}}
for key, path in {
'sleepDisabled':'/sys/module/lpm_levels/parameters/sleep_disabled',
'cpuIdleEnabled':'/sys/module/lpm_levels/parameters/enable_lpm',
'powerState':'/sys/power/state',
'memorySleep':'/sys/power/mem_sleep',
}.items():
try:
value = Path(path).read_text().strip()
if key in ('powerState','memorySleep'):
report['cpuIdle']['systemSuspend'][key] = value
else:
report['cpuIdle']['lowPowerParameter'] = {
**(report['cpuIdle']['lowPowerParameter'] or {}), key:value}
except OSError:
pass
suspend_stats = {}
for path in Path('/sys/power/suspend_stats').glob('*'):
if path.is_file():
try:
suspend_stats[path.name] = path.read_text().strip()
except OSError:
pass
report['cpuIdle']['systemSuspend']['statistics'] = suspend_stats or None
wakeups_after = wakeup_snapshot()
report['wakeupSources'] = {
'available':wakeups_after is not None,
'eventDeltas':counter_delta(wakeups_before,wakeups_after,('eventCount',)),
}
report['batteryDelta'] = {}
for supply, values in power_supplies().items():
prior = battery_before.get(supply,{})
for key in ('current_now','voltage_now','power_now','capacity','status'):
if key in values:
report['batteryDelta'].setdefault(supply,{})[key] = {
'before':prior.get(key), 'after':values[key]}
return report