Add headless Android runtime diagnostics
Sync changed files to Google Drive / sync (push) Successful in 1m9s
Sync changed files to Google Drive / sync (push) Successful in 1m9s
This commit is contained in:
1 parent
809b381918
commit
e253946fc8
5 files changed
+1540
-19
No files matched your search
@@ -0,0 +1,7 @@
|
|||||||
|
serial: emulator-5554
|
||||||
|
android: 11/30
|
||||||
|
ro_product_cpu_abi: x86_64
|
||||||
|
ro_product_cpu_abilist: x86_64,x86,arm64-v8a,armeabi-v7a,armeabi
|
||||||
|
launcher_activity: org.kammobile.kamremake/io.castleengine.MainActivity
|
||||||
|
pid_after_startup_wait: 4139
|
||||||
|
runtime_result: RUNTIME PASS
|
||||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1 @@
|
|||||||
|
{ "schema" : "kam-mobile-diagnostics/1", "application" : { "name" : "kam_mobile", "package" : "org.kammobile.kamremake", "version" : "0.4.0", "version_code" : 30, "build_id" : null }, "runtime" : { "platform" : "android", "os_version" : null, "device_model" : null, "cpu_arch" : null }, "selected_map" : { "relative_path" : "Maps/CatchTheSnake/CatchTheSnake.map", "url" : "castle-data:/game/Maps/CatchTheSnake/CatchTheSnake.map" }, "overall_status" : "PASS", "actions" : [{ "id" : "file_io", "status" : "PASS", "failure_stage" : null, "error" : null, "stages" : [{ "id" : "resource_exists", "status" : "PASS" }, { "id" : "resource_open", "status" : "PASS" }, { "id" : "caller_stream_load", "status" : "PASS" }, { "id" : "stream_state", "status" : "PASS" }, { "id" : "seek_read_consistency", "status" : "PASS" }], "stream" : { "size" : 10286, "position_after_load" : 0, "read_offset" : 0, "requested_read_count" : 32, "first_read_count" : 32, "position_after_first_read" : 32, "seek_back_position" : 0, "second_read_count" : 32, "position_after_second_read" : 32, "repeat_equal" : true } }] }
|
||||||
+77
-17
@@ -1,18 +1,21 @@
|
|||||||
"""Install and launch the current Android application package."""
|
"""Install, launch, and capture startup diagnostics for the current APK."""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
import argparse, json, shutil, subprocess, xml.etree.ElementTree as ET
|
import argparse, json, os, shutil, subprocess, time, xml.etree.ElementTree as ET
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from common import (ROOT, CPUS, ProbeError, sha256, CURRENT_BUILD_METADATA,
|
from common import (ROOT, CPUS, ProbeError, sha256, CURRENT_BUILD_METADATA,
|
||||||
CURRENT_PACKAGE_METADATA, CURRENT_APK, CURRENT_BUILD_SCHEMA,
|
CURRENT_PACKAGE_METADATA, CURRENT_APK, CURRENT_BUILD_SCHEMA,
|
||||||
CURRENT_PACKAGE_SCHEMA)
|
CURRENT_PACKAGE_SCHEMA)
|
||||||
|
|
||||||
|
AVD_NAME='kam_mobile_api30'
|
||||||
|
BOOT_TIMEOUT=180
|
||||||
|
|
||||||
class Adb:
|
class Adb:
|
||||||
def __init__(self, serial: str | None): self.base=['adb']+(['-s',serial] if serial else [])
|
def __init__(self, serial: str | None): self.base=[shutil.which('adb') or 'adb']+(['-s',serial] if serial else [])
|
||||||
def run(self,*args:str,check:bool=True,timeout:int=120)->subprocess.CompletedProcess:
|
def run(self,*args:str,check:bool=True,timeout:int=120)->subprocess.CompletedProcess:
|
||||||
p=subprocess.run(self.base+list(args),capture_output=True,timeout=timeout)
|
p=subprocess.run(self.base+list(args),capture_output=True,timeout=timeout)
|
||||||
if check and p.returncode: raise ProbeError('ADB command failed: '+' '.join(args)+'\n'+p.stderr.decode(errors='replace'))
|
if check and p.returncode: raise ProbeError('ADB command failed: '+' '.join(args)+'\n'+p.stderr.decode(errors='replace'))
|
||||||
return p
|
return p
|
||||||
def text(self,*args:str,check:bool=True)->str: return self.run(*args,check=check).stdout.decode(errors='replace').strip()
|
def text(self,*args:str,check:bool=True,timeout:int=120)->str: return self.run(*args,check=check,timeout=timeout).stdout.decode(errors='replace').strip()
|
||||||
|
|
||||||
def fail(message: str) -> None: raise ProbeError(message)
|
def fail(message: str) -> None: raise ProbeError(message)
|
||||||
|
|
||||||
@@ -50,6 +53,46 @@ def current_package() -> tuple[dict, str]:
|
|||||||
if not CURRENT_APK.is_file() or apk.get('sha256')!=sha256(CURRENT_APK): fail('Current APK missing or changed')
|
if not CURRENT_APK.is_file() or apk.get('sha256')!=sha256(CURRENT_APK): fail('Current APK missing or changed')
|
||||||
return package, identity['package']
|
return package, identity['package']
|
||||||
|
|
||||||
|
def authorized_devices() -> list[str]:
|
||||||
|
return [line.split()[0] for line in Adb(None).text('devices').splitlines()[1:]
|
||||||
|
if len(line.split()) >= 2 and line.split()[1] == 'device']
|
||||||
|
|
||||||
|
def start_avd() -> str:
|
||||||
|
emulator=shutil.which('emulator')
|
||||||
|
if not emulator: fail('Android Emulator is not installed on host')
|
||||||
|
if AVD_NAME not in subprocess.run([emulator,'-list-avds'],capture_output=True,text=True,timeout=30).stdout.splitlines():
|
||||||
|
fail(f'Android AVD {AVD_NAME} is not available')
|
||||||
|
accel='on' if subprocess.run([emulator,'-accel-check'],capture_output=True,timeout=30).returncode==0 else 'off'
|
||||||
|
subprocess.Popen([emulator,'-avd',AVD_NAME,'-no-window','-no-audio','-no-boot-anim',
|
||||||
|
'-gpu','swiftshader_indirect','-accel',accel,'-no-snapshot-save'],
|
||||||
|
stdout=subprocess.DEVNULL,stderr=subprocess.DEVNULL,start_new_session=True,
|
||||||
|
env={**os.environ,'ANDROID_EMULATOR_WAIT_TIME_BEFORE_KILL':'180'})
|
||||||
|
Adb(None).run('wait-for-device',timeout=BOOT_TIMEOUT)
|
||||||
|
deadline=time.monotonic()+BOOT_TIMEOUT
|
||||||
|
while time.monotonic()<deadline:
|
||||||
|
devices=authorized_devices()
|
||||||
|
if len(devices)==1:
|
||||||
|
adb=Adb(devices[0])
|
||||||
|
if adb.text('shell','getprop','sys.boot_completed',check=False,timeout=10)=='1': return devices[0]
|
||||||
|
time.sleep(2)
|
||||||
|
fail(f'Android AVD {AVD_NAME} did not boot within {BOOT_TIMEOUT} seconds')
|
||||||
|
|
||||||
|
def choose_device(requested: str | None) -> str:
|
||||||
|
devices=authorized_devices()
|
||||||
|
if requested:
|
||||||
|
if requested in devices: return requested
|
||||||
|
if devices: fail('Requested serial is not authorized')
|
||||||
|
if not devices:
|
||||||
|
serial=start_avd()
|
||||||
|
if requested and serial!=requested: fail('Requested serial is not the prepared emulator')
|
||||||
|
return serial
|
||||||
|
if len(devices)!=1: fail('Require exactly one authorized device or use --serial')
|
||||||
|
return devices[0]
|
||||||
|
|
||||||
|
def write_runtime_device(path: Path, **values: str) -> None:
|
||||||
|
path.parent.mkdir(parents=True,exist_ok=True)
|
||||||
|
path.write_text(''.join(f'{key}: {value}\n' for key,value in values.items()),encoding='utf-8')
|
||||||
|
|
||||||
def main() -> int:
|
def main() -> int:
|
||||||
ap=argparse.ArgumentParser(description=__doc__); ap.add_argument('--serial'); a=ap.parse_args()
|
ap=argparse.ArgumentParser(description=__doc__); ap.add_argument('--serial'); a=ap.parse_args()
|
||||||
try:
|
try:
|
||||||
@@ -63,24 +106,41 @@ def main() -> int:
|
|||||||
if not shutil.which('adb'):
|
if not shutil.which('adb'):
|
||||||
print('BLOCKED\nadb is not installed on host'); return 2
|
print('BLOCKED\nadb is not installed on host'); return 2
|
||||||
try:
|
try:
|
||||||
devices=[line.split()[:2] for line in Adb(None).text('devices').splitlines()[1:] if line.strip()]
|
serial=choose_device(a.serial); adb=Adb(serial)
|
||||||
if len(devices)!=1 or len(devices[0])!=2 or devices[0][1]!='device': fail('Require exactly one authorized device')
|
abi=adb.text('shell','getprop','ro.product.cpu.abi')
|
||||||
serial=devices[0][0]
|
abilist=adb.text('shell','getprop','ro.product.cpu.abilist')
|
||||||
if a.serial and a.serial!=serial: fail('Requested serial is not the sole authorized device')
|
android=adb.text('shell','getprop','ro.build.version.release')+'/'+adb.text('shell','getprop','ro.build.version.sdk')
|
||||||
adb=Adb(serial)
|
if package['abi'] not in abilist.split(','): fail('Device ABI does not support package ABI')
|
||||||
if package['abi'] not in adb.text('shell','getprop','ro.product.cpu.abilist').split(','): fail('Device ABI does not support package ABI')
|
|
||||||
except (ProbeError, OSError, subprocess.TimeoutExpired) as error:
|
except (ProbeError, OSError, subprocess.TimeoutExpired) as error:
|
||||||
print('BLOCKED\n'+str(error)); return 2
|
print('BUILD PASS\nRUNTIME BLOCKED\n'+str(error)); return 2
|
||||||
|
logs=ROOT/'build-evidence'/package['build_id']/'logs'
|
||||||
|
logcat=logs/'runtime-logcat.txt'; device=logs/'runtime-device.txt'
|
||||||
|
launcher=''; pid=''; result='RUNTIME FAIL'
|
||||||
try:
|
try:
|
||||||
install=adb.run('install','-r',str(CURRENT_APK),check=False)
|
install=adb.run('install','-r',str(CURRENT_APK),check=False)
|
||||||
if install.returncode and b'INSTALL_FAILED_UPDATE_INCOMPATIBLE' in install.stdout+install.stderr:
|
if install.returncode and b'INSTALL_FAILED_UPDATE_INCOMPATIBLE' in install.stdout+install.stderr:
|
||||||
adb.run('uninstall',pkg,check=False); install=adb.run('install',str(CURRENT_APK),check=False)
|
adb.run('uninstall',pkg,check=False); install=adb.run('install',str(CURRENT_APK),check=False)
|
||||||
if install.returncode: fail('APK install failed')
|
if install.returncode: fail('APK install failed: '+(install.stdout+install.stderr).decode(errors='replace').strip())
|
||||||
candidates=[line.strip() for line in adb.text('shell','cmd','package','resolve-activity','--brief','-a','android.intent.action.MAIN','-c','android.intent.category.LAUNCHER',pkg).splitlines() if line.strip()]
|
candidates=[line.strip() for line in adb.text('shell','cmd','package','resolve-activity','--brief','-a','android.intent.action.MAIN','-c','android.intent.category.LAUNCHER',pkg).splitlines() if line.strip().startswith(pkg+'/')]
|
||||||
if len(candidates)!=1 or not candidates[0].startswith(pkg+'/'): fail('Cannot resolve exactly one application launcher activity')
|
if len(candidates)!=1: fail('Cannot resolve exactly one application launcher activity')
|
||||||
if adb.run('shell','am','start','-W','-n',candidates[0],check=False).returncode: fail('Launch failed')
|
launcher=candidates[0]
|
||||||
|
adb.run('shell','am','force-stop',pkg)
|
||||||
|
adb.run('logcat','-c')
|
||||||
|
if adb.run('shell','am','start','-W','-n',launcher,check=False).returncode: fail('Launch failed')
|
||||||
|
time.sleep(5)
|
||||||
|
pid=adb.text('shell','pidof',pkg,check=False)
|
||||||
|
result='RUNTIME PASS' if pid else 'RUNTIME FAIL'
|
||||||
except (ProbeError, OSError, subprocess.TimeoutExpired) as error:
|
except (ProbeError, OSError, subprocess.TimeoutExpired) as error:
|
||||||
print('FAIL\n'+str(error)); return 1
|
result='RUNTIME FAIL: '+str(error)
|
||||||
print('PASS'); return 0
|
finally:
|
||||||
|
logs.mkdir(parents=True,exist_ok=True)
|
||||||
|
try: captured=adb.run('logcat','-d','-v','threadtime',check=False).stdout.decode(errors='replace')
|
||||||
|
except (OSError, subprocess.TimeoutExpired) as error: captured='Could not capture logcat: '+str(error)+'\n'
|
||||||
|
logcat.write_text(captured,encoding='utf-8')
|
||||||
|
write_runtime_device(device,serial=serial,android=android,ro_product_cpu_abi=abi,
|
||||||
|
ro_product_cpu_abilist=abilist,launcher_activity=launcher,
|
||||||
|
pid_after_startup_wait=pid,runtime_result=result)
|
||||||
|
print('BUILD PASS\n'+result+'\nlogcat: '+str(logcat.relative_to(ROOT)))
|
||||||
|
return 0 if result=='RUNTIME PASS' else 1
|
||||||
|
|
||||||
if __name__=='__main__': raise SystemExit(main())
|
if __name__=='__main__': raise SystemExit(main())
|
||||||
+5
-2
@@ -1,6 +1,6 @@
|
|||||||
"""One-command Android-only compile, full native link, APK and evidence pipeline."""
|
"""One-command Android-only compile, full native link, APK and evidence pipeline."""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
import argparse, json, os, re, shutil, subprocess, uuid, zipfile
|
import argparse, json, os, re, shutil, subprocess, sys, uuid, zipfile
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from common import (ROOT, COMMIT, CPUS, ProbeError, stamp, sha256, write_json, run_command,
|
from common import (ROOT, COMMIT, CPUS, ProbeError, stamp, sha256, write_json, run_command,
|
||||||
first_errors, CURRENT_OUTPUT_DIR, CURRENT_BUILD_METADATA,
|
first_errors, CURRENT_OUTPUT_DIR, CURRENT_BUILD_METADATA,
|
||||||
@@ -242,6 +242,9 @@ def main()->int:
|
|||||||
inherited=os.environ.get('KAM_BUILD_ID')
|
inherited=os.environ.get('KAM_BUILD_ID')
|
||||||
if inherited and not re.fullmatch(r'\d{8}T\d{6}Z-[0-9a-f]{8}',inherited):ap.error('Invalid KAM_BUILD_ID')
|
if inherited and not re.fullmatch(r'\d{8}T\d{6}Z-[0-9a-f]{8}',inherited):ap.error('Invalid KAM_BUILD_ID')
|
||||||
build_id=inherited or stamp()+'-'+uuid.uuid4().hex[:8]
|
build_id=inherited or stamp()+'-'+uuid.uuid4().hex[:8]
|
||||||
return build_local(a,build_id) if a.local else docker_build(a,build_id)
|
result=build_local(a,build_id) if a.local else docker_build(a,build_id)
|
||||||
|
if result==0 and os.environ.get('KAM_CONTAINER_BUILD')!='1':
|
||||||
|
subprocess.run([sys.executable,str(ROOT/'tools/device.py')],cwd=ROOT,check=False)
|
||||||
|
return result
|
||||||
|
|
||||||
if __name__=='__main__':raise SystemExit(main())
|
if __name__=='__main__':raise SystemExit(main())
|
||||||
Reference in new issue
Block a user