Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
35 changes: 35 additions & 0 deletions .github/workflows/checks.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
name: Checks

on:
push:
branches: [ "**" ]
pull_request:
branches: [ "**" ]
workflow_dispatch:

concurrency:
group: checks-${{ github.ref }}
cancel-in-progress: true

jobs:
lint:
runs-on: ubuntu-latest

steps:
- name: Checkout repository
uses: actions/checkout@v4
# No submodules: these checks only look at this repo's own sources.

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.x'

- name: No non-ASCII in code/config
run: python3 scripts/ci/check_ascii.py

- name: Translations complete across all locales
run: python3 scripts/ci/check_translations.py

- name: Use fmt() instead of string concatenation
run: python3 scripts/ci/check_string_concat.py
2 changes: 1 addition & 1 deletion app/src/main/assets/prebuilts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package cn.classfun.droidvm.daemon.display;

import static cn.classfun.droidvm.lib.utils.StringUtils.fmt;
import android.annotation.SuppressLint;
import android.content.Context;
import android.os.Looper;
Expand Down Expand Up @@ -34,7 +35,7 @@ public static synchronized void init() {
try {
if (Looper.myLooper() == null) Looper.prepareMainLooper();
} catch (Throwable t) {
Log.w(TAG, "prepareMainLooper: " + t);
Log.w(TAG, fmt("prepareMainLooper: %s", t));
}
try {
Class<?> at = Class.forName("android.app.ActivityThread");
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package cn.classfun.droidvm.daemon.display;

import static cn.classfun.droidvm.lib.utils.StringUtils.fmt;
import static cn.classfun.droidvm.BuildConfig.APPLICATION_ID;

import android.content.Intent;
Expand Down Expand Up @@ -78,7 +79,7 @@ private static void broadcast(@NonNull IBinder binder, @NonNull String nonce) {
intent.setPackage(APPLICATION_ID);
intent.putExtra(NativeDisplay.EXTRA_BUNDLE, bundle);
context.sendBroadcast(intent);
Log.i(TAG, "broadcast native-display binder (nonce=" + nonce + ")");
Log.i(TAG, fmt("broadcast native-display binder (nonce=%s)", nonce));
}

private static IBinder smCall(@NonNull String method, @NonNull String name) {
Expand All @@ -87,14 +88,14 @@ private static IBinder smCall(@NonNull String method, @NonNull String name) {
Method m = sm.getMethod(method, String.class);
return (IBinder) m.invoke(null, name);
} catch (Exception e) {
Log.w(TAG, method + " reflection failed: " + e.getMessage());
Log.w(TAG, fmt("%s reflection failed: %s", method, e.getMessage()));
return null;
}
}

private static IBinder waitForServiceWithTimeout(@NonNull String name, long timeoutMs) {
var holder = new IBinder[1];
var t = new Thread(() -> holder[0] = smCall("waitForService", name), "WaitSvc-" + name);
var t = new Thread(() -> holder[0] = smCall("waitForService", name), fmt("WaitSvc-%s", name));
t.setDaemon(true);
t.start();
try {
Expand All @@ -105,7 +106,7 @@ private static IBinder waitForServiceWithTimeout(@NonNull String name, long time
}

private static IBinder doWaitForDisplayBinder(@NonNull String serviceName) {
Log.i(TAG, "waitForDisplayBinder('" + serviceName + "')");
Log.i(TAG, fmt("waitForDisplayBinder('%s')", serviceName));
var direct = smCall("checkService", serviceName);
if (direct != null) {
Log.i(TAG, "OK: got display binder directly from ServiceManager");
Expand All @@ -117,8 +118,8 @@ private static IBinder doWaitForDisplayBinder(@NonNull String serviceName) {
Log.i(TAG, "OK: got display binder via waitForService");
return waited;
}
Log.e(TAG, "'" + serviceName + "' not found - is crosvm running with "
+ "--android-display-service " + serviceName + "?");
Log.e(TAG, fmt("'%s' not found - is crosvm running with "
+ "--android-display-service %s?", serviceName, serviceName));
return null;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,7 @@ protected boolean load(@NonNull DataStore<NetworkInstance> store, @NonNull JSONO
JsonUtils.forEachArray(obj, getTypeName(), (JSONObject entry) -> {
var migrated = NetworkConfig.migrate(entry);
if (migrated == null) {
Log.w(TAG, "Skipping network config with unsupported schema: "
+ entry.optString("name"));
Log.w(TAG, fmt("Skipping network config with unsupported schema: %s", entry.optString("name")));
return;
}
if (migrated != entry) {
Expand All @@ -63,8 +62,7 @@ protected boolean load(@NonNull DataStore<NetworkInstance> store, @NonNull JSONO
try {
NetworkConfigValidator.validate(new NetworkConfig(migrated));
} catch (Exception e) {
Log.w(TAG, "Skipping legacy network that failed migration/validation: "
+ entry.optString("name"), e);
Log.w(TAG, fmt("Skipping legacy network that failed migration/validation: %s", entry.optString("name")), e);
return;
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package cn.classfun.droidvm.daemon.network.backend;

import static cn.classfun.droidvm.lib.utils.FileUtils.shellReadFile;
import static cn.classfun.droidvm.lib.utils.StringUtils.fmt;

import android.util.Log;
Expand All @@ -24,6 +25,7 @@
import cn.classfun.droidvm.lib.store.network.BridgeType;
import cn.classfun.droidvm.lib.store.network.NetworkState;
import cn.classfun.droidvm.lib.store.network.UplinkMode;
import cn.classfun.droidvm.lib.utils.RunUtils;

public final class DefaultRouterWatcher {
private static final String TAG = "DefaultRouterWatcher";
Expand Down Expand Up @@ -192,7 +194,7 @@ private void reconcileRules(boolean ipv6, @NonNull List<String> tables) {
// lowest priority so it is evaluated first.
var desired = new ArrayList<String>();
for (int i = 0; i < tables.size(); i++)
desired.add((RULE_PRIORITY_BASE + i) + " " + tables.get(i));
desired.add(fmt("%d %s", RULE_PRIORITY_BASE + i, tables.get(i)));

// Group our installed rules per device as "priority table" specs. Ours
// are the plain rules: an iif owned by a bridge, a lookup action, no
Expand All @@ -213,7 +215,7 @@ private void reconcileRules(boolean ipv6, @NonNull List<String> tables) {
if (dev.startsWith(br)) { ours = true; break; }
if (!ours) continue;
installed.computeIfAbsent(dev, k -> new ArrayList<>())
.add(r.optInt("priority", 0) + " " + r.optInt("table", 0));
.add(fmt("%d %d", r.optInt("priority", 0), r.optInt("table", 0)));
if (r.optBoolean("detached", false)) detached.add(dev);
}
installed.values().forEach(specs -> specs.sort(Comparator.comparingInt(
Expand Down Expand Up @@ -253,6 +255,64 @@ private synchronized void runOnce() {
ensureHostIpMonitor();
updateHostIps();
updateDefaultRouter();
reassertForwarding();
}

/**
* Re-asserts the kernel forwarding sysctls that guest L3 routing depends
* on. netd owns these globally and clears them whenever it retunes
* tethering, which silently black-holes every forwarded guest packet.
* {@link cn.classfun.droidvm.daemon.network.backend.iptables.IptablesBackend}
* enables them once at init; this keeps them enabled for as long as a
* Linux-bridge L3 network is running.
*
* <p>Gated on {@link #hasL3LinuxBridge()} -- the same "running LINUX + L3"
* condition the routing rules use. With no such network there is nothing
* to forward, so netd's value is left alone rather than forcing global
* forwarding on the phone. Both families are covered because netd flips
* v4 and v6 together (as does {@code enableIpForward}). Reads before
* writing: an untouched value costs one cheap cat and stays silent; only a
* value netd actually reset is corrected and logged (it means netd raced
* us). Never throws out of the tick -- a transient shell failure must not
* tear down the routing rules.
*/
private void reassertForwarding() {
try {
if (!hasL3LinuxBridge()) return;
ensureSysctlOne("/proc/sys/net/ipv4/ip_forward", "net.ipv4.ip_forward");
ensureSysctlOne("/proc/sys/net/ipv6/conf/all/forwarding",
"net.ipv6.conf.all.forwarding");
} catch (Exception e) {
Log.w(TAG, "Failed to re-assert forwarding sysctls", e);
}
}

/**
* Writes 1 to {@code path} only if it is not already 1; logs corrections.
* The read ({@code shellReadFile}) and the write ({@code runQuiet}) are both
* quiet, so the 5s poll stays out of logcat -- {@code RunContext.run} would
* otherwise log a "Running command: cat ..." line every tick. The only line
* this emits on a normal tick is the warning below, and only when netd
* actually reset the value.
*/
private void ensureSysctlOne(@NonNull String path, @NonNull String name) {
var cur = shellReadFile(path);
if ("1".equals(cur)) return;
Log.w(TAG, fmt("%s=%s (netd reset it); re-enabling forwarding",
name, cur.isEmpty() ? "?" : cur));
RunUtils.runQuiet(fmt("echo 1 > %s", path));
}

/** True while any RUNNING Linux-bridge L3 network needs kernel forwarding. */
private boolean hasL3LinuxBridge() {
var found = new boolean[]{false};
context.getNetworks().forEach((uuid, inst) -> {
if (inst.getState() == NetworkState.RUNNING
&& inst.getBridgeType() == BridgeType.LINUX
&& inst.getUplinkMode() == UplinkMode.L3)
found[0] = true;
});
return found[0];
}

private void updateDefaultRouter() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -377,7 +377,7 @@ private void postForward(
fmt("/api/v1/gateways/%d/forwards", vlanId), rule);
if (resp.isSuccess()) {
var id = resp.json().optString("id", "");
if (!id.isEmpty()) ids.add(vlanId + "/" + id);
if (!id.isEmpty()) ids.add(fmt("%d/%s", vlanId, id));
} else {
failures.add(fmt("%s %s (%s)", proto, bind, briefBody(resp.body)));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -231,7 +231,7 @@ static void runDownload(@NonNull Context context, long id) {
var ctx = context.getApplicationContext();
var d = JOBS.get(id);
if (d == null) return;
var part = new File(d.folder, d.name + ".part");
var part = new File(d.folder, fmt("%s.part", d.name));
var dest = new File(d.folder, d.name);
try {
var parent = dest.getParentFile();
Expand Down Expand Up @@ -285,15 +285,15 @@ private static void downloadOnce(Download d, File part) throws IOException {
conn.setInstanceFollowRedirects(true);
conn.setRequestProperty("User-Agent", d.userAgent);
conn.setRequestProperty("Accept-Encoding", "identity");
if (existing > 0) conn.setRequestProperty("Range", "bytes=" + existing + "-");
if (existing > 0) conn.setRequestProperty("Range", fmt("bytes=%d-", existing));
d.activeConn = conn;
try {
int code = conn.getResponseCode();
boolean resumed = code == HttpURLConnection.HTTP_PARTIAL; // 206
if (code != HttpURLConnection.HTTP_OK && code != HttpURLConnection.HTTP_PARTIAL)
throw new IOException("HTTP " + code);
throw new IOException(fmt("HTTP %d", code));
Log.i(TAG, fmt("Download #%d %s from %s (HTTP %d)",
d.id, existing > 0 ? "resuming@" + existing : "starting", conn.getURL(), code));
d.id, existing > 0 ? fmt("resuming@%d", existing) : "starting", conn.getURL(), code));
// Server ignored our Range and is sending the whole file again.
long start = resumed ? existing : 0;
d.total = resolveTotal(conn, resumed, start);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package cn.classfun.droidvm.lib.download;

import static cn.classfun.droidvm.lib.utils.StringUtils.fmt;
import static cn.classfun.droidvm.lib.size.SizeUtils.formatSize;
import static cn.classfun.droidvm.lib.utils.StringUtils.formatDuration;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package cn.classfun.droidvm.lib.store.network;

import static cn.classfun.droidvm.lib.utils.StringUtils.fmt;
import android.util.Log;

import androidx.annotation.NonNull;
Expand Down Expand Up @@ -30,7 +31,7 @@ public NetworkConfig() {
public NetworkConfig(@NonNull JSONObject obj) throws JSONException {
item.set(obj);
if (!isSupportedSchema(obj)) throw new JSONException(
"Unsupported network config schema (expected " + SCHEMA_VERSION + ")"
fmt("Unsupported network config schema (expected %s)", SCHEMA_VERSION)
);
}

Expand All @@ -57,7 +58,7 @@ public static JSONObject migrate(@NonNull JSONObject obj) {
try {
return migrateLegacyToV2(obj);
} catch (Exception e) {
Log.w(TAG, "Failed to migrate legacy network config: " + obj.optString("name"), e);
Log.w(TAG, fmt("Failed to migrate legacy network config: %s", obj.optString("name")), e);
return null;
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package cn.classfun.droidvm.lib.store.network;

import static cn.classfun.droidvm.lib.utils.StringUtils.fmt;
import android.content.Context;
import android.util.Log;

Expand Down Expand Up @@ -42,8 +43,7 @@ protected boolean load(@NonNull DataStore<NetworkConfig> store, @NonNull JSONObj
JsonUtils.forEachArray(obj, getTypeName(), (JSONObject entry) -> {
var migrated = NetworkConfig.migrate(entry);
if (migrated == null) {
Log.w(TAG, "Skipping network config with unsupported schema: "
+ entry.optString("name"));
Log.w(TAG, fmt("Skipping network config with unsupported schema: %s", entry.optString("name")));
return;
}
if (migrated == entry) { // already current schema
Expand All @@ -58,8 +58,7 @@ protected boolean load(@NonNull DataStore<NetworkConfig> store, @NonNull JSONObj
cfg = new NetworkConfig(migrated);
NetworkConfigValidator.validate(cfg);
} catch (Exception e) {
Log.w(TAG, "Skipping legacy network that failed migration/validation: "
+ entry.optString("name"), e);
Log.w(TAG, fmt("Skipping legacy network that failed migration/validation: %s", entry.optString("name")), e);
return;
}
store.add(cfg);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ public static String serviceName(@NonNull VMConfig config) {
/** Same as {@link #serviceName(VMConfig)} but from a raw VM id (e.g. an Intent extra). */
@NonNull
public static String serviceNameFromId(@NonNull String vmId) {
return "droidvm_disp_" + sanitize(vmId);
return fmt("droidvm_disp_%s", sanitize(vmId));
}

/** The socket path crosvm connects to for [vmKey]'s [channel]. Must match across all callers. */
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package cn.classfun.droidvm.lib.ui.termux;

import static cn.classfun.droidvm.lib.utils.StringUtils.fmt;
import android.content.Context;
import android.graphics.Typeface;
import android.util.Log;
Expand Down Expand Up @@ -49,7 +50,7 @@ public static Typeface load(@NonNull Context ctx) {
try {
return Typeface.createFromFile(userFont);
} catch (Exception e) {
Log.w(TAG, "Ignoring unreadable user font " + userFont, e);
Log.w(TAG, fmt("Ignoring unreadable user font %s", userFont), e);
}
}
if (bundled == null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -316,7 +316,7 @@ private static void extractTarXz(@NonNull InputStream rawIn, @NonNull File destR
if (isZeroBlock(header)) break;
var name = cString(header, 0, 100);
var prefix = cString(header, 345, 155);
var path = prefix.isEmpty() ? name : prefix + "/" + name;
var path = prefix.isEmpty() ? name : pathJoin(prefix, name);
var size = parseOctal(header, 124, 12);
var mode = (int) parseOctal(header, 100, 8);
var type = (char) (header[156] & 0xff);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ public static String extensionLower(@NonNull String path) {
public static String pathJoin(@NonNull String base, @NonNull String child) {
if (base.endsWith("/")) base = base.substring(0, base.length() - 1);
if (child.startsWith("/")) child = child.substring(1);
return base + "/" + child;
return base + "/" + child; // concat-ok: the canonical path join; fmt() would be a hot-path perf hit
}

@NonNull
Expand Down
Loading
Loading