diff --git a/src/cli/android-znca-api-server-frida.ts b/src/cli/android-znca-api-server-frida.ts index 2ed53c1..b77f3d8 100644 --- a/src/cli/android-znca-api-server-frida.ts +++ b/src/cli/android-znca-api-server-frida.ts @@ -38,6 +38,10 @@ export function builder(yargs: Argv) { describe: 'Path to the frida-server executable on the device', type: 'string', default: '/data/local/tmp/frida-server', + }).option('start-method', { + describe: 'Method to ensure the app is running (one of "spawn", "none", "activity", "service")', + type: 'string', + default: 'service', }).option('strict-validate', { describe: 'Validate data exactly matches the format that would be generated by Nintendo\'s Android app', type: 'boolean', @@ -55,6 +59,21 @@ export function builder(yargs: Argv) { type Arguments = YargsArguments>; +enum StartMethod { + /** Spawn the app process with Frida, even if the app is already running (recommended) */ + SPAWN, + /** Start the app's main activity using the am command */ + ACTIVITY, + /** + * Start a background service using the am command (default) + * This tricks Android into not killing the app for some reason and allows the process to be started + * in the background. + */ + SERVICE, + /** Do not attempt to start the app - if it is not already running the server will fail */ + NONE, +} + interface PackageInfo { name: string; version: string; @@ -101,12 +120,18 @@ interface FResult { } export async function handler(argv: ArgumentsCamelCase) { + const start_method = + argv.startMethod === 'spawn' ? StartMethod.SPAWN : + argv.startMethod === 'activity' ? StartMethod.ACTIVITY : + argv.startMethod === 'service' ? StartMethod.SERVICE : + StartMethod.NONE; + await mkdirp(script_dir); const storage = await initStorage(argv.dataPath); - await setup(argv); + await setup(argv, start_method); - let {session, script} = await attach(argv); + let {session, script} = await attach(argv, start_method); let ready: Promise | null = null; let api: { @@ -144,7 +169,7 @@ export async function handler(argv: ArgumentsCamelCase) { debug('Attempting to reconnect to the device'); - ready = attach(argv).then(async a => { + ready = attach(argv, start_method).then(async a => { ready = null; session = a.session; script = a.script; @@ -406,12 +431,14 @@ export async function handler(argv: ArgumentsCamelCase) { const frida_script = ` const perform = callback => new Promise((rs, rj) => { - Java.scheduleOnMainThread(() => { - try { - rs(callback()); - } catch (err) { - rj(err); - } + Java.perform(() => { + Java.scheduleOnMainThread(() => { + try { + rs(callback()); + } catch (err) { + rj(err); + } + }); }); }); @@ -520,6 +547,7 @@ rpc.exports = { const setup_script = (options: { frida_server_path: string; + start_method: StartMethod; }) => `#!/system/bin/sh # Ensure frida-server is running @@ -534,10 +562,16 @@ fi sleep 1 +${(options.start_method === StartMethod.ACTIVITY ? ` +# Ensure the app is running +echo "Starting com.nintendo.znca in foreground" +am start-activity com.nintendo.znca/com.nintendo.coral.ui.boot.BootActivity +` : options.start_method === StartMethod.SERVICE ? ` # Ensure the app is running echo "Starting com.nintendo.znca" am start-foreground-service com.nintendo.znca/com.google.firebase.messaging.FirebaseMessagingService am start-service com.nintendo.znca/com.google.firebase.messaging.FirebaseMessagingService +` : '').trim()} if [ "$?" != "0" ]; then echo "Failed to start com.nintendo.znca" @@ -554,7 +588,7 @@ echo "Releasing wake lock" echo androidzncaapiserver > /sys/power/wake_unlock `; -async function setup(argv: ArgumentsCamelCase) { +async function setup(argv: ArgumentsCamelCase, start_method: StartMethod) { debug('Connecting to device %s', argv.device); let co = execFileSync('adb', [ 'connect', @@ -583,11 +617,12 @@ async function setup(argv: ArgumentsCamelCase) { await pushScript(argv.device, setup_script({ frida_server_path: argv.fridaServerPath, + start_method, }), '/data/local/tmp/android-znca-api-server-setup.sh'); await pushScript(argv.device, shutdown_script, '/data/local/tmp/android-znca-api-server-shutdown.sh'); } -async function attach(argv: ArgumentsCamelCase) { +async function attach(argv: ArgumentsCamelCase, start_method: StartMethod) { const frida = await import('frida'); type Session = import('frida').Session; @@ -602,11 +637,17 @@ async function attach(argv: ArgumentsCamelCase) { let session: Session; try { - const process = await device.getProcess('Nintendo Switch Online'); + const process = start_method === StartMethod.SPAWN ? + {pid: await device.spawn('com.nintendo.znca')} : + await device.getProcess('Nintendo Switch Online'); debug('process', process); session = await device.attach(process.pid); + + if (start_method === StartMethod.SPAWN) { + await device.resume(session.pid); + } } catch (err) { debug('Could not attach to process', err); throw new Error('Failed to attach to process');