From 71a26e259ddddf7729246405ccb90cdf0fb87900 Mon Sep 17 00:00:00 2001 From: admin Date: Tue, 19 May 2026 21:14:26 +0400 Subject: [PATCH] v2.2.5: Shebang patching + proot from nativeLib + Termux RUN_COMMAND + F-Droid fallback --- android/app/build.gradle | 4 +- .../main/java/ai/z/chat/BootstrapPlugin.java | 145 ++++++++++++++++-- .../src/main/java/ai/z/chat/ShellPlugin.java | 47 ++---- package.json | 2 +- www/index.html | 13 +- www/js/app.js | 126 ++++++++------- 6 files changed, 230 insertions(+), 107 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index 3dcf654..6c7d76d 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -7,8 +7,8 @@ android { applicationId "ai.z.chat" minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion - versionCode 17 - versionName "2.2.4" + versionCode 18 + versionName "2.2.5" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" aaptOptions { ignoreAssetsPattern = '!.svn:!.git:!.ds_store:!*.scc:.*:!CVS:!thumbs.db:!picasa.ini:!*~' diff --git a/android/app/src/main/java/ai/z/chat/BootstrapPlugin.java b/android/app/src/main/java/ai/z/chat/BootstrapPlugin.java index 43782e6..b63d9b3 100644 --- a/android/app/src/main/java/ai/z/chat/BootstrapPlugin.java +++ b/android/app/src/main/java/ai/z/chat/BootstrapPlugin.java @@ -1,6 +1,9 @@ package ai.z.chat; import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.net.Uri; import android.util.Log; import com.getcapacitor.JSObject; @@ -274,12 +277,16 @@ public class BootstrapPlugin extends Plugin { private void patchPaths(String dir) { try { - ProcessBuilder pb = new ProcessBuilder("find", dir, "-type", "f", - "(", "-name", "*.sh", "-o", "-name", "*.conf", "-o", "-name", "*.cfg", - "-o", "-name", "*.txt", "-o", "-name", "*.env", "-o", "-name", "properties.sh", - "-o", "-name", "profile", "-o", "-name", "bashrc", "-o", "-name", "*.profile", ")"); - pb.redirectErrorStream(true); - Process p = pb.start(); + String ourPrefix = filesDir + "/usr"; + String sedExpr = "s|/data/data/com.termux/files/usr|" + ourPrefix + "|g;" + + "s|/data/data/com.termux/files/home|" + homeDir + "|g;" + + "s|/data/data/com.termux|" + filesDir + "|g"; + + ProcessBuilder findPb = new ProcessBuilder("find", dir, "-type", "f", + "-not", "-name", "*.so", "-not", "-name", "*.elf", + "-not", "-name", "*.pyc", "-not", "-name", "*.a", "-size", "-512k"); + findPb.redirectErrorStream(true); + Process p = findPb.start(); BufferedReader reader = new BufferedReader(new InputStreamReader(p.getInputStream())); List files = new ArrayList<>(); String line; @@ -288,20 +295,20 @@ public class BootstrapPlugin extends Plugin { } p.waitFor(); - String ourPrefix = filesDir + "/usr"; for (String filePath : files) { try { - ProcessBuilder sedPb = new ProcessBuilder("sed", "-i", - "s|/data/data/com.termux/files/usr|" + ourPrefix + "|g;" + - "s|/data/data/com.termux/files/home|" + homeDir + "|g;" + - "s|/data/data/com.termux|" + filesDir + "|g", - filePath); + ProcessBuilder sedPb = new ProcessBuilder("sed", "-i", sedExpr, filePath); sedPb.start().waitFor(); } catch (Exception e) { Log.w(TAG, "Patch failed for " + filePath + ": " + e.getMessage()); } } + patchShebangs(dir + "/bin"); + if (new File(dir + "/libexec").exists()) { + patchShebangs(dir + "/libexec"); + } + ProcessBuilder ldSo = new ProcessBuilder("sed", "-i", "s|/data/data/com.termux/files/usr|" + ourPrefix + "|g", dir + "/etc/ld.so.conf"); @@ -311,6 +318,34 @@ public class BootstrapPlugin extends Plugin { } } + private void patchShebangs(String dirPath) { + File dir = new File(dirPath); + if (!dir.exists() || !dir.isDirectory()) return; + File[] files = dir.listFiles(); + if (files == null) return; + for (File f : files) { + if (f.isDirectory()) continue; + try { + byte[] header = new byte[256]; + java.io.FileInputStream fis = new java.io.FileInputStream(f); + int read = fis.read(header); + fis.close(); + if (read < 2 || header[0] != '#' || header[1] != '!') continue; + + String shebangLine = new String(header, 0, Math.min(read, 256)).split("\n")[0]; + String newShebang = shebangLine.replaceAll("/data/data/com\\.termux/files/usr/bin/(sh|bash)", "/system/bin/sh") + .replaceAll("/data/user/0/ai\\.z\\.chat/files/usr/bin/(sh|bash)", "/system/bin/sh"); + if (!newShebang.equals(shebangLine)) { + ProcessBuilder sedPb = new ProcessBuilder("sed", "-i", "1s|" + shebangLine + "|" + newShebang + "|", f.getAbsolutePath()); + sedPb.start().waitFor(); + Log.i(TAG, "Patched shebang: " + f.getName() + " → " + newShebang); + } + } catch (Exception e) { + Log.w(TAG, "Shebang patch failed for " + f.getName() + ": " + e.getMessage()); + } + } + } + private void chmodRecursive(File dir) { if (!dir.exists() || !dir.isDirectory()) return; File[] children = dir.listFiles(); @@ -337,12 +372,98 @@ public class BootstrapPlugin extends Plugin { chmodRecursive(new File(prefixDir + "/lib")); File etcDir = new File(prefixDir + "/etc"); if (etcDir.exists()) chmodRecursive(etcDir); + patchShebangs(prefixDir + "/bin"); call.resolve(new JSObject().put("fixed", true)); } catch (Exception e) { call.reject("Fix permissions failed: " + e.getMessage()); } } + @PluginMethod + public void isTermuxInstalled(PluginCall call) { + try { + getContext().getPackageManager().getPackageInfo("com.termux", 0); + call.resolve(new JSObject().put("installed", true).put("prefix", "/data/data/com.termux/files/usr")); + } catch (PackageManager.NameNotFoundException e) { + call.resolve(new JSObject().put("installed", false).put("prefix", "")); + } catch (Exception e) { + call.resolve(new JSObject().put("installed", false).put("error", e.getMessage())); + } + } + + @PluginMethod + public void openTermuxPage(PluginCall call) { + try { + Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse("https://f-droid.org/en/packages/com.termux/")); + getContext().startActivity(intent); + call.resolve(new JSObject().put("opened", true)); + } catch (Exception e) { + call.reject("Failed to open: " + e.getMessage()); + } + } + + @PluginMethod + public void runInTermux(PluginCall call) { + String command = call.getString("command", ""); + if (command.isEmpty()) { + call.reject("No command"); + return; + } + try { + String[] cmd = new String[]{ + "/system/bin/am", "broadcast", + "--user", "0", + "-n", "com.termux/com.termux.app.TermuxOpenReceiver", + "-a", "com.termux.RUN_COMMAND", + "--es", "com.termux.RUN_COMMAND_PATH", "/data/data/com.termux/files/usr/bin/bash", + "--es", "com.termux.RUN_COMMAND_ARGUMENTS", "-c " + command, + "--ez", "com.termux.RUN_COMMAND_BACKGROUND", "false", + "--es", "com.termux.RUN_COMMAND_WORKDIR", "/data/data/com.termux/files/home" + }; + ProcessBuilder pb = new ProcessBuilder(cmd); + pb.redirectErrorStream(true); + Process p = pb.start(); + BufferedReader reader = new BufferedReader(new InputStreamReader(p.getInputStream())); + StringBuilder output = new StringBuilder(); + String line; + while ((line = reader.readLine()) != null) output.append(line).append("\n"); + p.waitFor(10, java.util.concurrent.TimeUnit.SECONDS); + call.resolve(new JSObject().put("sent", true).put("output", output.toString())); + } catch (Exception e) { + call.reject("Termux command failed: " + e.getMessage()); + } + } + + @PluginMethod + public void downloadTermuxApk(PluginCall call) { + call.setKeepAlive(true); + new Thread(() -> { + try { + String packagesContent = downloadToString("https://f-droid.org/repo/index-v1.jar"); + String apkUrl = null; + String[] lines = packagesContent.split("\n"); + for (String l : lines) { + if (l.contains("com.termux") && l.endsWith(".apk")) { + apkUrl = l.trim(); + break; + } + } + + if (apkUrl == null) { + apkUrl = "https://f-droid.org/repo/com.termux_1020.apk"; + } + + File apkFile = new File(getContext().getCacheDir(), "termux.apk"); + downloadFile(apkUrl, apkFile, null); + + call.resolve(new JSObject().put("path", apkFile.getAbsolutePath()) + .put("size", apkFile.length())); + } catch (Exception e) { + try { call.reject("Download failed: " + e.getMessage()); } catch (Exception ignored) {} + } + }).start(); + } + private void writeEnvFile() { try { File envFile = new File(prefixDir + "/etc/termux.env"); diff --git a/android/app/src/main/java/ai/z/chat/ShellPlugin.java b/android/app/src/main/java/ai/z/chat/ShellPlugin.java index 34c6ffc..6e2e1f6 100644 --- a/android/app/src/main/java/ai/z/chat/ShellPlugin.java +++ b/android/app/src/main/java/ai/z/chat/ShellPlugin.java @@ -54,36 +54,23 @@ public class ShellPlugin extends Plugin { private void refreshShell() { String nativeLibDir = getNativeLibDir(); - File prootInPrefix = new File(prefixDir + "/bin/proot"); - if (!prootInPrefix.exists() && nativeLibDir != null) { + if (nativeLibDir != null) { File bundledProot = new File(nativeLibDir, "libproot.so"); File bundledLoader = new File(nativeLibDir, "libproot-loader.so"); if (bundledProot.exists()) { - try { - new File(prefixDir + "/bin").mkdirs(); - new File(prefixDir + "/libexec").mkdirs(); - new File(prefixDir + "/libexec/proot").mkdirs(); - copyFile(bundledProot, new File(prefixDir + "/bin/proot")); - Os.chmod(prefixDir + "/bin/proot", 0755); - if (bundledLoader.exists()) { - copyFile(bundledLoader, new File(prefixDir + "/libexec/proot/loader")); - Os.chmod(prefixDir + "/libexec/proot/loader", 0755); - File loader32 = new File(nativeLibDir, "libproot-loader32.so"); - if (loader32.exists()) { - copyFile(loader32, new File(prefixDir + "/libexec/proot/loader32")); - Os.chmod(prefixDir + "/libexec/proot/loader32", 0755); - } - } - prootPath = prefixDir + "/bin/proot"; - prootLoaderPath = prefixDir + "/libexec/proot/loader"; - Log.i(TAG, "Installed bundled proot from APK: " + prootPath); - } catch (Exception e) { - Log.w(TAG, "Failed to install bundled proot: " + e.getMessage()); + prootPath = bundledProot.getAbsolutePath(); + if (bundledLoader.exists()) { + prootLoaderPath = bundledLoader.getAbsolutePath(); } + Log.i(TAG, "Using bundled proot from nativeLib: " + prootPath); + } + } + + if (prootPath == null) { + File prootInPrefix = new File(prefixDir + "/bin/proot"); + if (prootInPrefix.exists()) { + prootPath = prootInPrefix.getAbsolutePath(); } - } else if (prootInPrefix.exists()) { - try { Os.chmod(prootInPrefix.getAbsolutePath(), 0755); } catch (Exception e) {} - prootPath = prootInPrefix.getAbsolutePath(); } File bash = new File(prefixDir + "/bin/bash"); @@ -98,16 +85,6 @@ public class ShellPlugin extends Plugin { shellPath = "/system/bin/sh"; } - private void copyFile(File src, File dst) throws Exception { - java.io.FileInputStream fis = new java.io.FileInputStream(src); - java.io.FileOutputStream fos = new java.io.FileOutputStream(dst); - byte[] buf = new byte[8192]; - int r; - while ((r = fis.read(buf)) > 0) fos.write(buf, 0, r); - fos.close(); - fis.close(); - } - private String getNativeLibDir() { try { return getContext().getApplicationInfo().nativeLibraryDir; diff --git a/package.json b/package.json index f572991..bc1c923 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "zai-chat", - "version": "2.2.4", + "version": "2.2.5", "description": "Z.AI Chat - Full stack AI chat powered by GLM Coding Plan", "main": "index.js", "scripts": { diff --git a/www/index.html b/www/index.html index 56d5ecd..3752253 100644 --- a/www/index.html +++ b/www/index.html @@ -327,13 +327,24 @@

About

-

Z.AI Chat v2.2.4

+

Z.AI Chat v2.2.5

Built with Z.AI SDK & GLM-5.1

Compatible with Android 15/16

Changelog

    +
  • + v2.2.5 + 2026-05-19 +
      +
    • Shebang Patching — all bin/ scripts patched to use #!/system/bin/sh (bypasses interpreter SELinux)
    • +
    • Proot from APK nativeLib — uses proot directly from APK (apk_data_file SELinux label, always executable)
    • +
    • Termux Integration — detects installed Termux, sends RUN_COMMAND to install tools
    • +
    • F-Droid Fallback — opens Termux F-Droid page if not installed
    • +
    • 3-Strategy Install — direct → proot → Termux RUN_COMMAND → manual instructions
    • +
    +
  • v2.2.4 2026-05-19 diff --git a/www/js/app.js b/www/js/app.js index 435324d..99c4d69 100644 --- a/www/js/app.js +++ b/www/js/app.js @@ -1949,87 +1949,101 @@ termPrint('[*] This may take a few minutes...', 'info'); showStatusToast('Installing build tools...', 'info'); - var methods = []; + var installCmd = 'export PREFIX="' + prefixUsr + '" LD_LIBRARY_PATH="' + prefixUsr + '/lib" && export PATH="' + prefixUsr + '/bin:/system/bin:$PATH" && '; if (pkgTest.exitCode === 0) { - methods.push({cmd: 'export PREFIX="' + prefixUsr + '" PATH="' + prefixUsr + '/bin:$PATH" LD_LIBRARY_PATH="' + prefixUsr + '/lib" && sh "' + pkgBin + '" update -y 2>&1 && sh "' + pkgBin + '" install -y aapt2 ecj dx apksigner 2>&1', label: 'pkg'}); - } - if (aptTest.exitCode === 0) { - methods.push({cmd: 'export PREFIX="' + prefixUsr + '" PATH="' + prefixUsr + '/bin:$PATH" LD_LIBRARY_PATH="' + prefixUsr + '/lib" && sh "' + aptBin + '" update -y 2>&1 && sh "' + aptBin + '" install -y aapt2 ecj dx apksigner 2>&1', label: 'apt'}); + installCmd += 'sh "' + pkgBin + '" update -y 2>&1 && sh "' + pkgBin + '" install -y aapt2 ecj dx apksigner 2>&1'; + } else { + installCmd += 'sh "' + aptBin + '" update -y 2>&1 && sh "' + aptBin + '" install -y aapt2 ecj dx apksigner 2>&1'; } - for (var m = 0; m < methods.length; m++) { - termPrint('[*] Attempt ' + (m + 1) + ' (' + methods[m].label + ')...', 'info'); - var installResult = await shellExec(methods[m].cmd, termState.homeDir, false); - if (installResult.output) { - var out = installResult.output; - if (out.length > 2000) out = out.substring(0, 1000) + '\n... truncated ...\n' + out.substring(out.length - 800); - termPrint(out.replace(/\n$/, ''), ''); - } + termPrint('[*] Strategy 1: Direct execution (patched shebangs)...', 'info'); + var result = await shellExec(installCmd, termState.homeDir, false); + if (result.output) { + var out = result.output; + if (out.length > 2000) out = out.substring(0, 1000) + '\n... truncated ...\n' + out.substring(out.length - 800); + termPrint(out.replace(/\n$/, ''), ''); + } + if (await toolsReady()) { termPrint('[OK] Build tools installed!', 'success'); return true; } - var recheck = await shellExec('command -v aapt2 >/dev/null 2>&1 && command -v ecj >/dev/null 2>&1', termState.homeDir, false); - if (recheck.exitCode === 0) { - termPrint('[OK] Build tools installed successfully!', 'success'); - showStatusToast('Build tools installed!', 'success'); - termState.devToolsInstalled = true; - return true; - } + termPrint('[*] Strategy 2: Bundled PRoot...', 'info'); + if (termState.prootPath) { + termPrint('[OK] PRoot from APK: ' + termState.prootPath, 'success'); + var prootOk = await tryProotExec(termState.prootPath, prefixUsr, pkgBin, aptBin); + if (prootOk) return true; + } else { + termPrint('[!] No bundled PRoot found', 'warning'); } - termPrint('[*] Direct execution failed. Trying PRoot workaround...', 'info'); - var prootOk = await tryProotInstall(prefixUsr, pkgBin, aptBin); - if (prootOk) return true; + termPrint('[*] Strategy 3: Termux RUN_COMMAND...', 'info'); + var termuxOk = await tryTermuxInstall(); + if (termuxOk) return true; termPrint('', ''); - termPrint('[!] Auto-install failed. Android SELinux may be blocking binary execution.', 'err'); - termPrint('[*] Fallback options:', 'warning'); - termPrint(' 1. Install Termux from F-Droid, then run:', 'warning'); - termPrint(' pkg install aapt2 ecj dx apksigner', 'warning'); - termPrint(' 2. Z.AI Chat will auto-detect Termux tools.', 'warning'); + termPrint('[!] All auto-install strategies failed.', 'err'); + termPrint('[*] Manual fix: Install Termux from F-Droid:', 'warning'); + termPrint(' 1. Open: https://f-droid.org/en/packages/com.termux/', 'warning'); + termPrint(' 2. Install Termux, open it, run:', 'warning'); + termPrint(' pkg update && pkg install aapt2 ecj dx apksigner', 'warning'); + termPrint(' 3. Restart Z.AI Chat — tools will be detected.', 'warning'); termState.devToolsInstalled = false; return false; } - async function tryProotInstall(prefixUsr, pkgBin, aptBin) { - var prootCmd = termState.prootPath; - if (!prootCmd) { - try { - var prootResult = await Bootstrap.installProot(); - if (prootResult && prootResult.path) { - prootCmd = prootResult.path; - termState.prootPath = prootCmd; - termState.hasProot = true; - } - } catch(e) { - termPrint('[!] PRoot download failed: ' + e.message, 'err'); - } + async function toolsReady() { + var recheck = await shellExec('command -v aapt2 >/dev/null 2>&1 && command -v ecj >/dev/null 2>&1', termState.homeDir, false); + if (recheck.exitCode === 0) { + showStatusToast('Build tools installed!', 'success'); + termState.devToolsInstalled = true; + return true; } + return false; + } - if (!prootCmd) { - termPrint('[!] No PRoot available', 'err'); - return false; - } - - termPrint('[OK] PRoot available: ' + prootCmd, 'success'); - var pkgCmd = 'sh "' + pkgBin + '" update -y 2>&1 && sh "' + pkgBin + '" install -y aapt2 ecj dx apksigner 2>&1'; + async function tryProotExec(prootCmd, prefixUsr, pkgBin, aptBin) { + var pkgCmd = 'sh /usr/bin/pkg update -y 2>&1 && sh /usr/bin/pkg install -y aapt2 ecj dx apksigner 2>&1'; + if (!new File(pkgBin).exists) pkgCmd = 'sh /usr/bin/apt update -y 2>&1 && sh /usr/bin/apt install -y aapt2 ecj dx apksigner 2>&1'; var wrappedCmd = prootCmd + ' -0 -b /dev -b /proc -b /sys -r ' + prefixUsr + ' /bin/sh -c \'' + pkgCmd.replace(/'/g, "'\\''") + '\''; - termPrint('[*] Installing via PRoot...', 'info'); + termPrint('[*] Running pkg via PRoot...', 'info'); var result = await shellExec(wrappedCmd, termState.homeDir, false); if (result.output) { var out = result.output; if (out.length > 2000) out = out.substring(0, 1000) + '\n... truncated ...\n' + out.substring(out.length - 800); termPrint(out.replace(/\n$/, ''), ''); } + if (result.exitCode === 0 || result.output.indexOf('Setting up') !== -1) { + if (await toolsReady()) return true; + } + termPrint('[!] PRoot strategy failed (exit ' + result.exitCode + ')', 'err'); + return false; + } - var recheck = await shellExec('command -v aapt2 >/dev/null 2>&1 && command -v ecj >/dev/null 2>&1', termState.homeDir, false); - if (recheck.exitCode === 0) { - termPrint('[OK] Build tools installed via PRoot!', 'success'); - showStatusToast('Build tools installed!', 'success'); - termState.devToolsInstalled = true; - return true; + async function tryTermuxInstall() { + if (!Bootstrap) return false; + var termuxInfo; + try { termuxInfo = await Bootstrap.isTermuxInstalled(); } catch(e) { return false; } + + if (!termuxInfo.installed) { + termPrint('[!] Termux not installed. Opening F-Droid...', 'warning'); + try { await Bootstrap.openTermuxPage(); } catch(e) {} + termPrint('[*] After installing Termux:', 'info'); + termPrint(' 1. Open Termux app', 'info'); + termPrint(' 2. Run: pkg update && pkg install aapt2 ecj dx apksigner', 'info'); + termPrint(' 3. Come back to Z.AI Chat', 'info'); + return false; } - termPrint('[!] PRoot execution result: exit code ' + result.exitCode, 'err'); + termPrint('[OK] Termux detected! Sending install command...', 'success'); + try { + var runResult = await Bootstrap.runInTermux({command: 'pkg update -y && pkg install -y aapt2 ecj dx apksigner'}); + termPrint('[*] Command sent to Termux. Waiting...', 'info'); + await new Promise(function(r) { setTimeout(r, 15000); }); + if (await toolsReady()) return true; + await new Promise(function(r) { setTimeout(r, 30000); }); + if (await toolsReady()) return true; + } catch(e) { + termPrint('[!] Termux RUN_COMMAND failed: ' + e.message, 'err'); + } return false; }