Release Preview 20 shared desktop, Android and Sargo updates
Restore Gaia settings and media workflows, extend native network adapters, and align the APK payload. Generate French/English changelogs and website entries from the simplified JSON source. Validation: shared JS and Tundra unit tests; desktop workflows and persistence; signed APK payload parity; Sargo RAM and installed startup with boot readback. Android runtime qualification is recorded separately.
This commit is contained in:
parent
be2cdce823
commit
1311bed5e9
170 changed files with 19698 additions and 2495 deletions
116
tundra/tools/diagnose-telephony.py
Normal file
116
tundra/tools/diagnose-telephony.py
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
#!/usr/bin/env python3
|
||||
# SPDX-License-Identifier: MPL-2.0
|
||||
"""Report native telephony health without dialing or exposing subscriber data."""
|
||||
import datetime
|
||||
import json
|
||||
from pathlib import Path
|
||||
import re
|
||||
import subprocess
|
||||
|
||||
|
||||
FIELDS = {
|
||||
'Modem': ('Powered', 'Online', 'Emergency', 'Revision'),
|
||||
'SimManager': ('Present', 'PinRequired', 'LockedPins'),
|
||||
'NetworkRegistration': ('Status', 'Mode', 'Technology', 'Strength'),
|
||||
'RadioSettings': ('TechnologyPreference', 'AvailableTechnologies'),
|
||||
'ConnectionManager': ('Attached', 'Powered', 'RoamingAllowed'),
|
||||
'IpMultimediaSystem': ('Registered', 'VoiceCapable', 'SmsCapable'),
|
||||
'CallVolume': ('Muted', 'SpeakerVolume', 'MicrophoneVolume'),
|
||||
}
|
||||
|
||||
|
||||
def select_properties(interface, properties):
|
||||
return {key: properties[key].get('data') for key in FIELDS[interface]
|
||||
if isinstance(properties.get(key), dict) and 'data' in properties[key]}
|
||||
|
||||
|
||||
def registration_events(text):
|
||||
"""Keep RAT and denial reason; never copy cell location or raw log lines."""
|
||||
events = []
|
||||
pattern = re.compile(
|
||||
r'binder_network_poll_(voice|data)_state_cb\(\).*?slot\d+ '
|
||||
r'(registered|roaming|searching|unregistered|denied|unknown),'
|
||||
r'([^,]*),(-?\d+),(-?\d+),(-?\d+),(-?\d+)')
|
||||
for line in text.splitlines():
|
||||
match = pattern.search(line)
|
||||
if not match:
|
||||
continue
|
||||
domain, status, technology, rat, _lac, _cell, reason = match.groups()
|
||||
event = {'domain': domain, 'status': status,
|
||||
'technology': technology, 'radioTechnology': int(rat),
|
||||
'denialReason': int(reason)}
|
||||
if not events or event != events[-1]:
|
||||
events.append(event)
|
||||
return events[-40:]
|
||||
|
||||
|
||||
def run(command):
|
||||
return subprocess.run(command, capture_output=True, text=True, timeout=12)
|
||||
|
||||
|
||||
def modem_crashes(text):
|
||||
signals = []
|
||||
for line in text.splitlines():
|
||||
if 'qcrild' not in line:
|
||||
continue
|
||||
match = re.search(r'Fatal signal (\d+) \((SIG[A-Z]+)\)', line)
|
||||
if match:
|
||||
event = {'signal': int(match[1]), 'name': match[2]}
|
||||
timestamp = re.match(r'\s*(\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d+)\s', line)
|
||||
if timestamp:
|
||||
event['logTime'] = timestamp[1]
|
||||
signals.append(event)
|
||||
return {'signals': signals,
|
||||
'initialAttachTimeoutInBacktrace': 'qcril_qmi_nas_initial_attach_timeout_handler' in text}
|
||||
|
||||
|
||||
def collect(command=run, root=Path('/')):
|
||||
report = {'recordedAt': datetime.datetime.now(datetime.timezone.utc).isoformat(),
|
||||
'properties': {}, 'services': {}, 'errors': {}}
|
||||
for interface in FIELDS:
|
||||
try:
|
||||
result = command(['busctl', '--system', '--timeout=8', '--json=short',
|
||||
'call', 'org.ofono', '/ril_0',
|
||||
'org.ofono.' + interface, 'GetProperties'])
|
||||
if result.returncode:
|
||||
report['errors'][interface] = 'unavailable'
|
||||
continue
|
||||
data = json.loads(result.stdout)['data'][0]
|
||||
report['properties'][interface] = select_properties(interface, data)
|
||||
except (OSError, subprocess.TimeoutExpired, ValueError, KeyError, IndexError, TypeError):
|
||||
report['errors'][interface] = 'unavailable'
|
||||
for service in ('ofono', 'tundra-radio', 'tundra-sms', 'tundra-audio', 'tundra-call-audio'):
|
||||
try:
|
||||
result = command(['systemctl', 'is-active', service])
|
||||
state = result.stdout.strip()
|
||||
report['services'][service] = state if state in (
|
||||
'active', 'inactive', 'failed', 'activating', 'deactivating') else 'unknown'
|
||||
except (OSError, subprocess.TimeoutExpired):
|
||||
report['services'][service] = 'unknown'
|
||||
try:
|
||||
result = command(['journalctl', '-u', 'ofono', '--since', '-10 min',
|
||||
'--no-pager', '-n', '2000'])
|
||||
report['registrationEvents'] = registration_events(result.stdout)
|
||||
report['registrationLogAvailable'] = result.returncode == 0
|
||||
except (OSError, subprocess.TimeoutExpired):
|
||||
report['registrationEvents'] = []
|
||||
report['registrationLogAvailable'] = False
|
||||
try:
|
||||
result = command(['lxc-attach', '-n', 'tundra-hal', '--', '/system/bin/logcat',
|
||||
'-b', 'crash', '-d', '-t', '300'])
|
||||
report['qualcommCrashes'] = modem_crashes(result.stdout)
|
||||
report['qualcommCrashes']['logAvailable'] = result.returncode == 0
|
||||
except (OSError, subprocess.TimeoutExpired):
|
||||
report['qualcommCrashes'] = {'logAvailable': False}
|
||||
try:
|
||||
data = json.loads((root/'run/tundra-call-audio/status.json').read_text())
|
||||
report['audio'] = {key: data[key] for key in (
|
||||
'active', 'routed', 'speaker', 'muted', 'volume', 'recordedAt') if key in data}
|
||||
report['audio']['hasError'] = bool(data.get('error'))
|
||||
except (OSError, ValueError, TypeError):
|
||||
report['audio'] = {'available': False}
|
||||
return report
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
print(json.dumps(collect(), indent=2))
|
||||
Loading…
Reference in a new issue