Skip to content
Open
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
9ce4f38
feat: add device.webview.* method stubs
gmegidish May 11, 2026
8dabdb1
feat: align webview methods with trimmed OpenRPC spec
gmegidish May 12, 2026
3945a67
feat: add webview CLI commands
gmegidish May 12, 2026
36729b8
feat: implement WebViewListCommand for Android
gmegidish May 12, 2026
cef8b30
fix: remove hardcoded DEX_PATH from jvmti_agent.c
gmegidish May 12, 2026
559d39d
feat: implement webview goto, evaluate (url + title) for Android
gmegidish May 12, 2026
a6e7d1e
fix: webview url/title/eval returning empty result
gmegidish May 12, 2026
37687ee
feat: implement webview back, forward, content
gmegidish May 12, 2026
d8109a6
fix: ensure bare expressions are wrapped with return in WebViewEvaluate
gmegidish May 12, 2026
46f452c
feat: implement webview reload
gmegidish May 12, 2026
e6a0c33
fix: handle null evalJs result and ClassCastException in evaluateExpr…
gmegidish May 12, 2026
98d936d
build: wire agents/android into root Makefile
gmegidish May 12, 2026
7808755
feat: implement webview waitForLoadState
gmegidish May 12, 2026
31386a7
refactor: replace map[string]any with typed result structs
gmegidish May 12, 2026
4abe35b
chore: add agent sources, Makefile, and adb client
gmegidish May 12, 2026
f42b53c
chore: remove built agent binaries from git, add to gitignore
gmegidish May 12, 2026
581330f
fix: cross-platform agent Makefile and CI agent build job
gmegidish May 12, 2026
b1cfde6
feat: iOS simulator webview agent (list, url, goto)
gmegidish May 12, 2026
c6d1eea
no dylib in git
gmegidish May 12, 2026
49d63ee
feat: complete iOS simulator webview support + WebViewable interface
gmegidish May 12, 2026
c74d7b7
feat: iOS real device webview list (inject via lldb, forward via go-ios)
gmegidish May 12, 2026
c0e2d01
fix: iOS 26 SDK compat for real device webview injection via LLDB
gmegidish May 13, 2026
0391621
performance
gmegidish May 13, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 61 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ A universal command-line tool for managing iOS and Android devices, simulators,
- **Device Control**: Reboot devices, tap screen coordinates, press hardware buttons
- **App Management**: Launch, terminate, install, uninstall, list, and get foreground apps
- **Crash Reports**: List and fetch crash reports from iOS and Android devices
- **Webview Inspection**: List, navigate, query DOM, and evaluate JavaScript in embedded webviews

### 🎯 Platform Support

Expand Down Expand Up @@ -255,6 +256,66 @@ Example output for `agent status`:
}
```

### Webview Inspection 🌐

Inspect and interact with embedded webviews (`WKWebView` on iOS, `android.webkit.WebView` on Android) running inside native apps.

```bash
# List embedded webviews in the foreground app
mobilecli webview list --device <device-id>

# Navigate a webview to a URL
mobilecli webview goto <id> https://example.com --device <device-id>

# Reload, go back or forward
mobilecli webview reload <id> --device <device-id>
mobilecli webview back <id> --device <device-id>
mobilecli webview forward <id> --device <device-id>

# Get current URL and page title
mobilecli webview url <id> --device <device-id>
mobilecli webview title <id> --device <device-id>

# Dump the full HTML content of the page
mobilecli webview content <id> --device <device-id>

# Query DOM elements by CSS selector
mobilecli webview query <id> "button" --device <device-id>
mobilecli webview query <id> "[data-testid='submit']" --device <device-id>

# Evaluate arbitrary JavaScript
mobilecli webview eval <id> "document.querySelectorAll('a').length" --device <device-id>

# Wait for the page to finish loading
mobilecli webview wait <id> --state load --device <device-id>
mobilecli webview wait <id> --state domcontentloaded --timeout 5000 --device <device-id>
```

Example output for `webview list`:
```json
{
"status": "ok",
"data": [
{
"id": "1",
"url": "https://example.com",
"title": "Example Domain"
}
]
}
```

Example output for `webview query <id> "button"`:
```json
{
"status": "ok",
"data": [
{ "tag": "button", "text": "Sign In", "id": "login-btn", "class": "btn-primary", "value": null, "href": null },
{ "tag": "button", "text": "Cancel", "id": null, "class": "btn-secondary", "value": null, "href": null }
]
}
```

### Crash Reports 💥

```bash
Expand Down
9 changes: 9 additions & 0 deletions agents/agents.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package agents

import _ "embed"

//go:embed android/devicekit.so
var AndroidDevicekitSO []byte

//go:embed android/devicekit.dex

Check failure on line 8 in agents/agents.go

View workflow job for this annotation

GitHub Actions / test

pattern android/devicekit.dex: no matching files found
var AndroidDevicekitDEX []byte
Comment on lines +8 to +9
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify whether embed targets exist under agents/
fd -i '^devicekit\.(so|dex)$' agents
fd -i '^android$' agents -t d --exec ls -la

Repository: mobile-next/mobilecli

Length of output: 1958


🏁 Script executed:

#!/bin/bash
# Check if android/devicekit.dex exists in agents/ directory
find . -name "devicekit.dex" 2>/dev/null
find . -name "devicekit.so" 2>/dev/null
find . -type d -name "android" 2>/dev/null

Repository: mobile-next/mobilecli

Length of output: 82


🏁 Script executed:

#!/bin/bash
# List contents of agents/android directory
ls -la agents/android/
# Also check for devicekit files anywhere in the agents directory
find agents/ -type f -name "devicekit*"

Repository: mobile-next/mobilecli

Length of output: 239


🏁 Script executed:

#!/bin/bash
# Read agents/agents.go to see the embed directives
cat -n agents/agents.go | head -20

Repository: mobile-next/mobilecli

Length of output: 289


Missing embed targets cause compile failure

Both android/devicekit.so (line 5) and android/devicekit.dex (line 8) are missing from the repository. The //go:embed directives will fail at build time until these artifacts are added.

🧰 Tools
🪛 GitHub Check: test

[failure] 8-8:
pattern android/devicekit.dex: no matching files found

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@agents/agents.go` around lines 8 - 9, The build is failing because the
//go:embed directives reference missing files; update the embed usage for the
variables AndroidDevicekitSO and AndroidDevicekitDEX by either (A) adding the
actual artifacts android/devicekit.so and android/devicekit.dex into the repo at
those paths, or (B) removing or guarding the //go:embed lines and associated
variables (AndroidDevicekitSO, AndroidDevicekitDEX) behind an appropriate build
tag or feature flag so the embed is not evaluated when the files are absent;
choose one approach and adjust the agents.go file accordingly so the embed
target names and variables match actual files or are not compiled.

151 changes: 151 additions & 0 deletions agents/android/jvmti_agent.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
#include <jvmti.h>
#include <jni.h>
#include <android/log.h>
#include <string.h>
#include <stdlib.h>

#define TAG "devicekit"
#define LOG(...) __android_log_print(ANDROID_LOG_DEBUG, TAG, __VA_ARGS__)

/* ── load devicekit.dex into the target process ─────────────────────────── */

/* dex_path — full path to devicekit.dex on the device
opt_dir — directory used by DexClassLoader for optimised odex output;
typically the same directory that contains the dex */
static void bootstrap(JNIEnv *env, const char *dex_path, const char *opt_dir) {
jclass cls_CL = (*env)->FindClass(env, "java/lang/ClassLoader");
jclass cls_DCL = (*env)->FindClass(env, "dalvik/system/DexClassLoader");
if (!cls_CL || !cls_DCL || (*env)->ExceptionCheck(env)) {
(*env)->ExceptionClear(env);
return;
}

jmethodID getSys = (*env)->GetStaticMethodID(env, cls_CL, "getSystemClassLoader", "()Ljava/lang/ClassLoader;");
jmethodID init = (*env)->GetMethodID(env, cls_DCL, "<init>", "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/ClassLoader;)V");
jmethodID load = (*env)->GetMethodID(env, cls_CL, "loadClass", "(Ljava/lang/String;)Ljava/lang/Class;");
if (!getSys || !init || !load) {
(*env)->ExceptionClear(env);
return;
}

jobject parent = (*env)->CallStaticObjectMethod(env, cls_CL, getSys);
jstring j_dex = (*env)->NewStringUTF(env, dex_path);
jstring j_opt = (*env)->NewStringUTF(env, opt_dir);
jobject loader = (*env)->NewObject(env, cls_DCL, init, j_dex, j_opt, NULL, parent);
(*env)->DeleteLocalRef(env, j_dex);
(*env)->DeleteLocalRef(env, j_opt);

if (!loader || (*env)->ExceptionCheck(env)) {
jthrowable exc = (*env)->ExceptionOccurred(env);
(*env)->ExceptionClear(env);
if (exc) {
jclass ecls = (*env)->GetObjectClass(env, exc);
jmethodID toStr = (*env)->GetMethodID(env, ecls, "toString", "()Ljava/lang/String;");
if (toStr) {
jstring msg = (jstring)(*env)->CallObjectMethod(env, exc, toStr);
if (msg && !(*env)->ExceptionCheck(env)) {
const char *s = (*env)->GetStringUTFChars(env, msg, NULL);
LOG("DexClassLoader exception: %s", s ? s : "(null)");
if (s) (*env)->ReleaseStringUTFChars(env, msg, s);
} else (*env)->ExceptionClear(env);
}
}
LOG("DexClassLoader failed — push devicekit.dex to %s", dex_path);
return;
}

jstring j_cls = (*env)->NewStringUTF(env, "com.mobilenext.mobilecli.MobileCliAgent");
jclass agentCls = (jclass)(*env)->CallObjectMethod(env, loader, load, j_cls);
(*env)->DeleteLocalRef(env, j_cls);
if (!agentCls || (*env)->ExceptionCheck(env)) {
jthrowable exc = (*env)->ExceptionOccurred(env);
(*env)->ExceptionClear(env);
if (exc) {
jclass ecls = (*env)->GetObjectClass(env, exc);
jmethodID toStr = (*env)->GetMethodID(env, ecls, "toString", "()Ljava/lang/String;");
if (toStr) {
jstring msg = (jstring)(*env)->CallObjectMethod(env, exc, toStr);
if (msg && !(*env)->ExceptionCheck(env)) {
const char *s = (*env)->GetStringUTFChars(env, msg, NULL);
LOG("loadClass exception: %s", s ? s : "(null)");
if (s) (*env)->ReleaseStringUTFChars(env, msg, s);
} else {
(*env)->ExceptionClear(env);
}
}
}

LOG("loadClass(MobileCliAgent) failed");
return;
}

jmethodID start = (*env)->GetStaticMethodID(env, agentCls, "start", "()V");
if (!start || (*env)->ExceptionCheck(env)) {
(*env)->ExceptionClear(env);
return;
}

(*env)->CallStaticVoidMethod(env, agentCls, start);
if ((*env)->ExceptionCheck(env)) {
jthrowable exc = (*env)->ExceptionOccurred(env);
(*env)->ExceptionClear(env);
if (exc) {
jclass ecls = (*env)->GetObjectClass(env, exc);
jmethodID toStr = (*env)->GetMethodID(env, ecls, "toString", "()Ljava/lang/String;");
if (toStr) {
jstring msg = (jstring)(*env)->CallObjectMethod(env, exc, toStr);
if (msg && !(*env)->ExceptionCheck(env)) {
const char *s = (*env)->GetStringUTFChars(env, msg, NULL);
LOG("MobileCliAgent.start() threw: %s", s ? s : "(null)");
if (s) (*env)->ReleaseStringUTFChars(env, msg, s);
} else {
(*env)->ExceptionClear(env);
}
}
}
}

LOG("MobileCliAgent started");
}

/* ── agent entry points ──────────────────────────────────────────────────── */

/* opts is the dex path passed via: am attach-agent <pid> agent.so=<dex_path>
opt_dir is derived as the directory containing the dex file. */
static jint setup(JavaVM *vm, const char *opts) {
if (!opts || opts[0] == '\0') {
LOG("no dex path provided — pass it as agent.so=/path/to/devicekit.dex");
return JNI_ERR;
}

const char *dex_path = opts;

/* derive opt_dir as dirname(dex_path) */
char opt_dir[512];
const char *slash = strrchr(dex_path, '/');
if (slash && slash != dex_path) {
size_t len = (size_t)(slash - dex_path);
if (len >= sizeof(opt_dir)) { len = sizeof(opt_dir) - 1; }
memcpy(opt_dir, dex_path, len);
opt_dir[len] = '\0';
} else {
opt_dir[0] = '.';
opt_dir[1] = '\0';
}

JNIEnv *env = NULL;
(*vm)->AttachCurrentThread(vm, (void **) &env, NULL);
bootstrap(env, dex_path, opt_dir);
Comment on lines +136 to +138
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Check AttachCurrentThread before using env.

If AttachCurrentThread fails here, env stays unset and bootstrap(env, ...) will dereference a null JNI pointer inside the target process.

Suggested fix
     JNIEnv *env = NULL;
-    (*vm)->AttachCurrentThread(vm, (void **) &env, NULL);
+    jint rc = (*vm)->AttachCurrentThread(vm, (void **) &env, NULL);
+    if (rc != JNI_OK || env == NULL) {
+        LOG("AttachCurrentThread failed: %d", rc);
+        return JNI_ERR;
+    }
     bootstrap(env, dex_path, opt_dir);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
JNIEnv *env = NULL;
(*vm)->AttachCurrentThread(vm, (void **) &env, NULL);
bootstrap(env, dex_path, opt_dir);
JNIEnv *env = NULL;
jint rc = (*vm)->AttachCurrentThread(vm, (void **) &env, NULL);
if (rc != JNI_OK || env == NULL) {
LOG("AttachCurrentThread failed: %d", rc);
return JNI_ERR;
}
bootstrap(env, dex_path, opt_dir);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@agents/android/jvmti_agent.c` around lines 136 - 138, AttachCurrentThread
return value is not checked so env may be NULL before calling bootstrap; check
the jint result of (*vm)->AttachCurrentThread(vm, (void**)&env, NULL) and only
call bootstrap(env, dex_path, opt_dir) if the attach succeeded, otherwise
log/error and avoid dereferencing env (references: AttachCurrentThread, env,
bootstrap, JNIEnv, vm).

LOG("agent ready (dex=%s)", dex_path);
return JNI_OK;
Comment on lines +88 to +140
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Return JNI_ERR when bootstrap fails.

bootstrap() can fail at loadClass or start(), but setup() still logs agent ready and returns JNI_OK. That masks the real attach failure and turns it into a later timeout in the caller.

Suggested fix
-static void bootstrap(JNIEnv *env, const char *dex_path, const char *opt_dir) {
+static jint bootstrap(JNIEnv *env, const char *dex_path, const char *opt_dir) {
     jclass cls_CL = (*env)->FindClass(env, "java/lang/ClassLoader");
     jclass cls_DCL = (*env)->FindClass(env, "dalvik/system/DexClassLoader");
     if (!cls_CL || !cls_DCL || (*env)->ExceptionCheck(env)) {
         (*env)->ExceptionClear(env);
-        return;
+        return JNI_ERR;
     }
@@
     if (!getSys || !init || !load) {
         (*env)->ExceptionClear(env);
-        return;
+        return JNI_ERR;
     }
@@
         LOG("DexClassLoader failed — push devicekit.dex to %s", dex_path);
-        return;
+        return JNI_ERR;
     }
@@
         LOG("loadClass(MobileCliAgent) failed");
-        return;
+        return JNI_ERR;
     }
@@
     if (!start || (*env)->ExceptionCheck(env)) {
         (*env)->ExceptionClear(env);
-        return;
+        return JNI_ERR;
     }
@@
     if ((*env)->ExceptionCheck(env)) {
@@
+        return JNI_ERR;
     }
 
     LOG("MobileCliAgent started");
+    return JNI_OK;
 }
@@
-    bootstrap(env, dex_path, opt_dir);
+    if (bootstrap(env, dex_path, opt_dir) != JNI_OK) {
+        return JNI_ERR;
+    }
     LOG("agent ready (dex=%s)", dex_path);
     return JNI_OK;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@agents/android/jvmti_agent.c` around lines 88 - 140, setup() currently
ignores failures from bootstrap() causing setup() to always log "agent ready"
and return JNI_OK; change bootstrap() to return a status (e.g., jint or bool)
indicating success/failure (ensure bootstrap reports failure when
loadClass/start fail), then in setup() capture that return value after calling
bootstrap(env, dex_path, opt_dir), check it, and if bootstrap failed call
(*vm)->DetachCurrentThread(vm) if attached, log an error, and return JNI_ERR
instead of JNI_OK; also remove the unconditional "agent ready" log or only log
it on success.

}

JNIEXPORT jint
JNICALL Agent_OnLoad(JavaVM *vm, char *opts, void *reserved) {
return setup(vm, opts);
}

JNIEXPORT jint
JNICALL Agent_OnAttach(JavaVM *vm, char *opts, void *reserved) {
return setup(vm, opts);
}
4 changes: 4 additions & 0 deletions cli/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,8 @@ var (
fleetNames []string
fleetWait bool
fleetTimeout int

// for webview wait command
webviewWaitState string
webviewWaitTimeout int
)
29 changes: 29 additions & 0 deletions cli/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,35 @@ INPUT/OUTPUT:
# Send text input
mobilecli io text --device <device-id> "Hello World"

WEBVIEW:
# List embedded webviews in the foreground app
mobilecli webview list --device <device-id>

# Navigate a webview to a URL
mobilecli webview goto <id> https://example.com --device <device-id>

# Reload, go back or forward
mobilecli webview reload <id> --device <device-id>
mobilecli webview back <id> --device <device-id>
mobilecli webview forward <id> --device <device-id>

# Get current URL and page title
mobilecli webview url <id> --device <device-id>
mobilecli webview title <id> --device <device-id>

# Dump full HTML content
mobilecli webview content <id> --device <device-id>

# Query DOM elements by CSS selector
mobilecli webview query <id> "button" --device <device-id>
mobilecli webview query <id> "[data-testid='submit']" --device <device-id>

# Evaluate arbitrary JavaScript
mobilecli webview eval <id> "document.querySelectorAll('a').length" --device <device-id>

# Wait for page load
mobilecli webview wait <id> --state load --device <device-id>

CRASH REPORTS:
# List crash reports from a device
mobilecli device crashes list --device <device-id>
Expand Down
Loading
Loading