diff --git a/package-lock.json b/package-lock.json
index c80e0ef00..76c306d95 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -128,7 +128,6 @@
"integrity": "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"@ampproject/remapping": "^2.2.0",
"@babel/code-frame": "^7.27.1",
@@ -2959,7 +2958,8 @@
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz",
"integrity": "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==",
- "license": "MIT"
+ "license": "MIT",
+ "peer": true
},
"node_modules/@types/markdown-it": {
"version": "14.1.2",
@@ -2976,7 +2976,8 @@
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz",
"integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==",
- "license": "MIT"
+ "license": "MIT",
+ "peer": true
},
"node_modules/@types/node": {
"version": "24.2.1",
@@ -3289,8 +3290,7 @@
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.5.0.tgz",
"integrity": "sha512-hqJHYaQb5OptNunnyAnkHyM8aCjZ1MEIDTQu1iIbbTD/xops91NB5yq1ZK/dC2JDbVWtF23zUtl9JE2NqwT87A==",
- "license": "MIT",
- "peer": true
+ "license": "MIT"
},
"node_modules/@xtuc/ieee754": {
"version": "1.2.0",
@@ -3321,7 +3321,6 @@
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"dev": true,
"license": "MIT",
- "peer": true,
"bin": {
"acorn": "bin/acorn"
},
@@ -3355,7 +3354,6 @@
"version": "8.12.0",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"fast-deep-equal": "^3.1.1",
"json-schema-traverse": "^1.0.0",
@@ -3736,7 +3734,6 @@
}
],
"license": "MIT",
- "peer": true,
"dependencies": {
"caniuse-lite": "^1.0.30001733",
"electron-to-chromium": "^1.5.199",
@@ -3839,7 +3836,6 @@
"integrity": "sha512-ci2iJH6LeIkvP9eJW6gpueU8cnZhv85ELY8w8WiFtNjMHA5ad6pQLaJo9mEly/9qUyCpvqX8/POVUTf18/HFdw==",
"dev": true,
"license": "Apache-2.0",
- "peer": true,
"dependencies": {
"@chevrotain/cst-dts-gen": "11.0.3",
"@chevrotain/gast": "11.0.3",
@@ -6020,7 +6016,6 @@
"resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.0.tgz",
"integrity": "sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==",
"license": "MIT",
- "peer": true,
"dependencies": {
"argparse": "^2.0.1",
"entities": "^4.4.0",
@@ -6906,7 +6901,6 @@
"url": "https://github.com/sponsors/ai"
}
],
- "peer": true,
"dependencies": {
"nanoid": "^3.3.7",
"picocolors": "^1.0.0",
@@ -7061,7 +7055,6 @@
"integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==",
"dev": true,
"license": "MIT",
- "peer": true,
"bin": {
"prettier": "bin/prettier.cjs"
},
@@ -7228,7 +7221,6 @@
"version": "6.12.6",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"fast-deep-equal": "^3.1.1",
"fast-json-stable-stringify": "^2.0.0",
@@ -8061,7 +8053,6 @@
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"license": "MIT",
- "peer": true,
"engines": {
"node": ">=12"
},
@@ -8333,7 +8324,6 @@
"integrity": "sha512-B4t+nJqytPeuZlHuIKTbalhljIFXeNRqrUGAQgTGlfOl2lXXKXw+yZu6bicycP+PUlM44CxBjCFD6aciKFT3LQ==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"@types/eslint-scope": "^3.7.7",
"@types/estree": "^1.0.8",
@@ -8383,7 +8373,6 @@
"integrity": "sha512-MfwFQ6SfwinsUVi0rNJm7rHZ31GyTcpVE5pgVA3hwFRb7COD4TzjUUwhGWKfO50+xdc2MQPuEBBJoqIMGt3JDw==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"@discoveryjs/json-ext": "^0.6.1",
"@webpack-cli/configtest": "^3.0.1",
diff --git a/package.json b/package.json
index 1de0509ce..f29c7648b 100644
--- a/package.json
+++ b/package.json
@@ -37,8 +37,8 @@
"cordova-plugin-browser": {},
"cordova-plugin-sftp": {},
"cordova-plugin-system": {},
- "com.foxdebug.acode.rk.exec.terminal": {},
- "com.foxdebug.acode.rk.exec.proot": {}
+ "com.foxdebug.acode.rk.exec.proot": {},
+ "com.foxdebug.acode.rk.exec.terminal": {}
},
"platforms": [
"android"
@@ -129,4 +129,4 @@
"yargs": "^18.0.0"
},
"browserslist": "cover 100%,not android < 5"
-}
\ No newline at end of file
+}
diff --git a/src/plugins/terminal/plugin.xml b/src/plugins/terminal/plugin.xml
index fbde6010f..72273257f 100644
--- a/src/plugins/terminal/plugin.xml
+++ b/src/plugins/terminal/plugin.xml
@@ -16,10 +16,19 @@
+
+
+
+
+
+
+
+
+
diff --git a/src/plugins/terminal/src/android/AlpineDocumentProvider.java b/src/plugins/terminal/src/android/AlpineDocumentProvider.java
index 7cdcaec90..97a4dd50a 100644
--- a/src/plugins/terminal/src/android/AlpineDocumentProvider.java
+++ b/src/plugins/terminal/src/android/AlpineDocumentProvider.java
@@ -20,6 +20,7 @@
import java.util.LinkedList;
import java.util.Locale;
import com.foxdebug.acode.R;
+import com.foxdebug.acode.rk.exec.terminal.*;
public class AlpineDocumentProvider extends DocumentsProvider {
diff --git a/src/plugins/terminal/src/android/BackgroundExecutor.java b/src/plugins/terminal/src/android/BackgroundExecutor.java
new file mode 100644
index 000000000..79e3f33ad
--- /dev/null
+++ b/src/plugins/terminal/src/android/BackgroundExecutor.java
@@ -0,0 +1,164 @@
+package com.foxdebug.acode.rk.exec.terminal;
+
+import org.apache.cordova.*;
+import org.json.*;
+import java.io.*;
+import java.util.*;
+import java.util.concurrent.*;
+import com.foxdebug.acode.rk.exec.terminal.*;
+
+public class BackgroundExecutor extends CordovaPlugin {
+
+ private final Map processes = new ConcurrentHashMap<>();
+ private final Map processInputs = new ConcurrentHashMap<>();
+ private final Map processCallbacks = new ConcurrentHashMap<>();
+ private ProcessManager processManager;
+
+ @Override
+ public void initialize(CordovaInterface cordova, CordovaWebView webView) {
+ super.initialize(cordova, webView);
+ this.processManager = new ProcessManager(cordova.getContext());
+ }
+
+ @Override
+ public boolean execute(String action, JSONArray args, CallbackContext callbackContext) throws JSONException {
+ switch (action) {
+ case "start":
+ String pid = UUID.randomUUID().toString();
+ startProcess(pid, args.getString(0), args.getString(1).equals("true"), callbackContext);
+ return true;
+ case "write":
+ writeToProcess(args.getString(0), args.getString(1), callbackContext);
+ return true;
+ case "stop":
+ stopProcess(args.getString(0), callbackContext);
+ return true;
+ case "exec":
+ exec(args.getString(0), args.getString(1).equals("true"), callbackContext);
+ return true;
+ case "isRunning":
+ isProcessRunning(args.getString(0), callbackContext);
+ return true;
+ case "loadLibrary":
+ loadLibrary(args.getString(0), callbackContext);
+ return true;
+ default:
+ callbackContext.error("Unknown action: " + action);
+ return false;
+ }
+ }
+
+ private void exec(String cmd, boolean useAlpine, CallbackContext callbackContext) {
+ cordova.getThreadPool().execute(() -> {
+ try {
+ ProcessManager.ExecResult result = processManager.executeCommand(cmd, useAlpine);
+
+ if (result.isSuccess()) {
+ callbackContext.success(result.stdout);
+ } else {
+ callbackContext.error(result.getErrorMessage());
+ }
+ } catch (Exception e) {
+ callbackContext.error("Exception: " + e.getMessage());
+ }
+ });
+ }
+
+ private void startProcess(String pid, String cmd, boolean useAlpine, CallbackContext callbackContext) {
+ cordova.getThreadPool().execute(() -> {
+ try {
+ ProcessBuilder builder = processManager.createProcessBuilder(cmd, useAlpine);
+ Process process = builder.start();
+
+ processes.put(pid, process);
+ processInputs.put(pid, process.getOutputStream());
+ processCallbacks.put(pid, callbackContext);
+
+ sendPluginResult(callbackContext, pid, true);
+
+ // Stream stdout
+ new Thread(() -> StreamHandler.streamOutput(
+ process.getInputStream(),
+ line -> sendPluginMessage(pid, "stdout:" + line)
+ )).start();
+
+ // Stream stderr
+ new Thread(() -> StreamHandler.streamOutput(
+ process.getErrorStream(),
+ line -> sendPluginMessage(pid, "stderr:" + line)
+ )).start();
+
+ int exitCode = process.waitFor();
+ sendPluginMessage(pid, "exit:" + exitCode);
+ cleanup(pid);
+ } catch (Exception e) {
+ callbackContext.error("Failed to start process: " + e.getMessage());
+ }
+ });
+ }
+
+ private void writeToProcess(String pid, String input, CallbackContext callbackContext) {
+ try {
+ OutputStream os = processInputs.get(pid);
+ if (os != null) {
+ StreamHandler.writeToStream(os, input);
+ callbackContext.success("Written to process");
+ } else {
+ callbackContext.error("Process not found or closed");
+ }
+ } catch (IOException e) {
+ callbackContext.error("Write error: " + e.getMessage());
+ }
+ }
+
+ private void stopProcess(String pid, CallbackContext callbackContext) {
+ Process process = processes.get(pid);
+ if (process != null) {
+ ProcessUtils.killProcessTree(process);
+ cleanup(pid);
+ callbackContext.success("Process terminated");
+ } else {
+ callbackContext.error("No such process");
+ }
+ }
+
+ private void isProcessRunning(String pid, CallbackContext callbackContext) {
+ Process process = processes.get(pid);
+
+ if (process != null) {
+ String status = ProcessUtils.isAlive(process) ? "running" : "exited";
+ if (status.equals("exited")) cleanup(pid);
+ callbackContext.success(status);
+ } else {
+ callbackContext.success("not_found");
+ }
+ }
+
+ private void loadLibrary(String path, CallbackContext callbackContext) {
+ try {
+ System.load(path);
+ callbackContext.success("Library loaded successfully.");
+ } catch (Exception e) {
+ callbackContext.error("Failed to load library: " + e.getMessage());
+ }
+ }
+
+ private void sendPluginResult(CallbackContext ctx, String message, boolean keepCallback) {
+ PluginResult result = new PluginResult(PluginResult.Status.OK, message);
+ result.setKeepCallback(keepCallback);
+ ctx.sendPluginResult(result);
+ }
+
+ private void sendPluginMessage(String pid, String message) {
+ CallbackContext ctx = processCallbacks.get(pid);
+ if (ctx != null) {
+ sendPluginResult(ctx, message, true);
+ }
+ }
+
+ private void cleanup(String pid) {
+ processes.remove(pid);
+ processInputs.remove(pid);
+ processCallbacks.remove(pid);
+ }
+}
\ No newline at end of file
diff --git a/src/plugins/terminal/src/android/Executor.java b/src/plugins/terminal/src/android/Executor.java
index 3e0b4e83d..5d0e90777 100644
--- a/src/plugins/terminal/src/android/Executor.java
+++ b/src/plugins/terminal/src/android/Executor.java
@@ -27,6 +27,7 @@
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import android.app.Activity;
+import com.foxdebug.acode.rk.exec.terminal.*;
public class Executor extends CordovaPlugin {
@@ -235,6 +236,22 @@ public boolean execute(String action, JSONArray args, CallbackContext callbackCo
return true;
}
+ if (action.equals("moveToBackground")) {
+ Intent intent = new Intent(context, TerminalService.class);
+ intent.setAction(TerminalService.MOVE_TO_BACKGROUND);
+ context.startService(intent);
+ callbackContext.success("Service moved to background mode");
+ return true;
+ }
+
+ if (action.equals("moveToForeground")) {
+ Intent intent = new Intent(context, TerminalService.class);
+ intent.setAction(TerminalService.MOVE_TO_FOREGROUND);
+ context.startService(intent);
+ callbackContext.success("Service moved to foreground mode");
+ return true;
+ }
+
// For all other actions, ensure service is bound first
if (!ensureServiceBound(callbackContext)) {
// Error already sent by ensureServiceBound
diff --git a/src/plugins/terminal/src/android/ProcessManager.java b/src/plugins/terminal/src/android/ProcessManager.java
new file mode 100644
index 000000000..fe7be24ba
--- /dev/null
+++ b/src/plugins/terminal/src/android/ProcessManager.java
@@ -0,0 +1,100 @@
+package com.foxdebug.acode.rk.exec.terminal;
+
+import android.content.Context;
+import android.content.pm.PackageManager;
+import java.io.*;
+import java.util.Map;
+import java.util.TimeZone;
+import com.foxdebug.acode.rk.exec.terminal.*;
+
+public class ProcessManager {
+
+ private final Context context;
+
+ public ProcessManager(Context context) {
+ this.context = context;
+ }
+
+ /**
+ * Creates a ProcessBuilder with common environment setup
+ */
+ public ProcessBuilder createProcessBuilder(String cmd, boolean useAlpine) {
+ String xcmd = useAlpine ? "source $PREFIX/init-sandbox.sh " + cmd : cmd;
+ ProcessBuilder builder = new ProcessBuilder("sh", "-c", xcmd);
+ setupEnvironment(builder.environment());
+ return builder;
+ }
+
+ /**
+ * Sets up common environment variables
+ */
+ private void setupEnvironment(Map env) {
+ env.put("PREFIX", context.getFilesDir().getAbsolutePath());
+ env.put("NATIVE_DIR", context.getApplicationInfo().nativeLibraryDir);
+
+ TimeZone tz = TimeZone.getDefault();
+ env.put("ANDROID_TZ", tz.getID());
+
+ try {
+ int target = context.getPackageManager()
+ .getPackageInfo(context.getPackageName(), 0)
+ .applicationInfo.targetSdkVersion;
+ env.put("FDROID", String.valueOf(target <= 28));
+ } catch (PackageManager.NameNotFoundException e) {
+ e.printStackTrace();
+ }
+ }
+
+ /**
+ * Reads all output from a stream
+ */
+ public static String readStream(InputStream stream) throws IOException {
+ BufferedReader reader = new BufferedReader(new InputStreamReader(stream));
+ StringBuilder output = new StringBuilder();
+ String line;
+ while ((line = reader.readLine()) != null) {
+ output.append(line).append("\n");
+ }
+ return output.toString();
+ }
+
+ /**
+ * Executes a command and returns the result
+ */
+ public ExecResult executeCommand(String cmd, boolean useAlpine) throws Exception {
+ ProcessBuilder builder = createProcessBuilder(cmd, useAlpine);
+ Process process = builder.start();
+
+ String stdout = readStream(process.getInputStream());
+ String stderr = readStream(process.getErrorStream());
+ int exitCode = process.waitFor();
+
+ return new ExecResult(exitCode, stdout.trim(), stderr.trim());
+ }
+
+ /**
+ * Result container for command execution
+ */
+ public static class ExecResult {
+ public final int exitCode;
+ public final String stdout;
+ public final String stderr;
+
+ public ExecResult(int exitCode, String stdout, String stderr) {
+ this.exitCode = exitCode;
+ this.stdout = stdout;
+ this.stderr = stderr;
+ }
+
+ public boolean isSuccess() {
+ return exitCode == 0;
+ }
+
+ public String getErrorMessage() {
+ if (!stderr.isEmpty()) {
+ return stderr;
+ }
+ return "Command exited with code: " + exitCode;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/plugins/terminal/src/android/ProcessUtils.java b/src/plugins/terminal/src/android/ProcessUtils.java
new file mode 100644
index 000000000..10ccc4034
--- /dev/null
+++ b/src/plugins/terminal/src/android/ProcessUtils.java
@@ -0,0 +1,45 @@
+package com.foxdebug.acode.rk.exec.terminal;
+
+import java.lang.reflect.Field;
+import com.foxdebug.acode.rk.exec.terminal.*;
+
+public class ProcessUtils {
+
+ /**
+ * Gets the PID of a process using reflection
+ */
+ public static long getPid(Process process) {
+ try {
+ Field f = process.getClass().getDeclaredField("pid");
+ f.setAccessible(true);
+ return f.getLong(process);
+ } catch (Exception e) {
+ return -1;
+ }
+ }
+
+ /**
+ * Checks if a process is still alive
+ */
+ public static boolean isAlive(Process process) {
+ try {
+ process.exitValue();
+ return false;
+ } catch(IllegalThreadStateException e) {
+ return true;
+ }
+ }
+
+ /**
+ * Forcefully kills a process and its children
+ */
+ public static void killProcessTree(Process process) {
+ try {
+ long pid = getPid(process);
+ if (pid > 0) {
+ Runtime.getRuntime().exec("kill -9 -" + pid);
+ }
+ } catch (Exception ignored) {}
+ process.destroy();
+ }
+}
\ No newline at end of file
diff --git a/src/plugins/terminal/src/android/StreamHandler.java b/src/plugins/terminal/src/android/StreamHandler.java
new file mode 100644
index 000000000..ab4c643c7
--- /dev/null
+++ b/src/plugins/terminal/src/android/StreamHandler.java
@@ -0,0 +1,31 @@
+package com.foxdebug.acode.rk.exec.terminal;
+
+import java.io.*;
+import com.foxdebug.acode.rk.exec.terminal.*;
+public class StreamHandler {
+
+ public interface OutputListener {
+ void onLine(String line);
+ }
+
+ /**
+ * Streams output from an InputStream to a listener
+ */
+ public static void streamOutput(InputStream inputStream, OutputListener listener) {
+ try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream))) {
+ String line;
+ while ((line = reader.readLine()) != null) {
+ listener.onLine(line);
+ }
+ } catch (IOException ignored) {
+ }
+ }
+
+ /**
+ * Writes input to an OutputStream
+ */
+ public static void writeToStream(OutputStream outputStream, String input) throws IOException {
+ outputStream.write((input + "\n").getBytes());
+ outputStream.flush();
+ }
+}
\ No newline at end of file
diff --git a/src/plugins/terminal/src/android/TerminalService.java b/src/plugins/terminal/src/android/TerminalService.java
index f8e26c67f..1d8281ee8 100644
--- a/src/plugins/terminal/src/android/TerminalService.java
+++ b/src/plugins/terminal/src/android/TerminalService.java
@@ -16,20 +16,12 @@
import android.os.PowerManager;
import android.os.RemoteException;
import androidx.core.app.NotificationCompat;
-import java.io.BufferedReader;
import java.io.IOException;
-import java.io.InputStream;
-import java.io.InputStreamReader;
import java.io.OutputStream;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executors;
-import java.lang.reflect.Field;
-
-import java.util.TimeZone;
-import java.util.Map;
-import java.util.HashMap;
-
+import com.foxdebug.acode.rk.exec.terminal.*;
public class TerminalService extends Service {
@@ -43,7 +35,10 @@ public class TerminalService extends Service {
public static final String CHANNEL_ID = "terminal_exec_channel";
public static final String ACTION_EXIT_SERVICE = "com.foxdebug.acode.ACTION_EXIT_SERVICE";
+ public static final String MOVE_TO_BACKGROUND = "com.foxdebug.acode.MOVE_TO_BACKGROUND";
+ public static final String MOVE_TO_FOREGROUND = "com.foxdebug.acode.MOVE_TO_FOREGROUND";
public static final String ACTION_TOGGLE_WAKE_LOCK = "com.foxdebug.acode.ACTION_TOGGLE_WAKE_LOCK";
+ public static boolean Default_Foreground = true;
private final Map processes = new ConcurrentHashMap<>();
private final Map processInputs = new ConcurrentHashMap<>();
@@ -54,6 +49,17 @@ public class TerminalService extends Service {
private PowerManager.WakeLock wakeLock;
private boolean isWakeLockHeld = false;
+ private ProcessManager processManager;
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ processManager = new ProcessManager(this);
+ if(Default_Foreground){
+ createNotificationChannel();
+ updateNotification();
+ }
+ }
@Override
public IBinder onBind(Intent intent) {
@@ -70,6 +76,13 @@ public int onStartCommand(Intent intent, int flags, int startId) {
return START_NOT_STICKY;
} else if (ACTION_TOGGLE_WAKE_LOCK.equals(action)) {
toggleWakeLock();
+ } else if(MOVE_TO_BACKGROUND.equals(action)){
+ Default_Foreground = false;
+ stopForeground(true);
+ } else if(MOVE_TO_FOREGROUND.equals(action)){
+ Default_Foreground = true;
+ createNotificationChannel();
+ updateNotification();
}
}
return START_STICKY;
@@ -87,7 +100,7 @@ public void handleMessage(Message msg) {
String cmd = bundle.getString("cmd");
String alpine = bundle.getString("alpine");
clientMessengers.put(id, clientMessenger);
- startProcess(id, cmd, alpine);
+ startProcess(id, cmd, "true".equals(alpine));
break;
case MSG_WRITE_TO_PROCESS:
String input = bundle.getString("input");
@@ -103,7 +116,7 @@ public void handleMessage(Message msg) {
String execCmd = bundle.getString("cmd");
String execAlpine = bundle.getString("alpine");
clientMessengers.put(id, clientMessenger);
- exec(id, execCmd, execAlpine);
+ exec(id, execCmd, "true".equals(execAlpine));
break;
}
}
@@ -137,31 +150,28 @@ private void releaseWakeLock() {
}
}
- private void startProcess(String pid, String cmd, String alpine) {
+ private void startProcess(String pid, String cmd, boolean useAlpine) {
threadPool.execute(() -> {
try {
- String xcmd = alpine.equals("true") ? "source $PREFIX/init-sandbox.sh " + cmd : cmd;
- ProcessBuilder builder = new ProcessBuilder("sh", "-c", xcmd);
-
- Map env = builder.environment();
- env.put("PREFIX", getFilesDir().getAbsolutePath());
- env.put("NATIVE_DIR", getApplicationInfo().nativeLibraryDir);
- TimeZone tz = TimeZone.getDefault();
- String timezoneId = tz.getID();
- env.put("ANDROID_TZ", timezoneId);
-
- try {
- int target = getPackageManager().getPackageInfo(getPackageName(), 0).applicationInfo.targetSdkVersion;
- env.put("FDROID", String.valueOf(target <= 28));
- } catch (Exception e) {
- e.printStackTrace();
- }
-
+ ProcessBuilder builder = processManager.createProcessBuilder(cmd, useAlpine);
Process process = builder.start();
+
processes.put(pid, process);
processInputs.put(pid, process.getOutputStream());
- threadPool.execute(() -> streamOutput(process.getInputStream(), pid, "stdout"));
- threadPool.execute(() -> streamOutput(process.getErrorStream(), pid, "stderr"));
+
+ // Stream stdout
+ threadPool.execute(() ->
+ StreamHandler.streamOutput(process.getInputStream(),
+ line -> sendMessageToClient(pid, "stdout", line))
+ );
+
+ // Stream stderr
+ threadPool.execute(() ->
+ StreamHandler.streamOutput(process.getErrorStream(),
+ line -> sendMessageToClient(pid, "stderr", line))
+ );
+
+ // Wait for process completion
threadPool.execute(() -> {
try {
int exitCode = process.waitFor();
@@ -180,53 +190,17 @@ private void startProcess(String pid, String cmd, String alpine) {
});
}
- private void exec(String execId, String cmd, String alpine) {
+ private void exec(String execId, String cmd, boolean useAlpine) {
threadPool.execute(() -> {
try {
- String xcmd = alpine.equals("true") ? "source $PREFIX/init-sandbox.sh " + cmd : cmd;
- ProcessBuilder builder = new ProcessBuilder("sh", "-c", xcmd);
- Map env = builder.environment();
- env.put("PREFIX", getFilesDir().getAbsolutePath());
- env.put("NATIVE_DIR", getApplicationInfo().nativeLibraryDir);
- TimeZone tz = TimeZone.getDefault();
- String timezoneId = tz.getID();
- env.put("ANDROID_TZ", timezoneId);
-
- try {
- int target = getPackageManager().getPackageInfo(getPackageName(), 0).applicationInfo.targetSdkVersion;
- env.put("FDROID", String.valueOf(target <= 28));
- } catch (Exception e) {
- e.printStackTrace();
- }
-
- Process process = builder.start();
- BufferedReader stdOutReader = new BufferedReader(
- new InputStreamReader(process.getInputStream()));
- StringBuilder stdOut = new StringBuilder();
- String line;
- while ((line = stdOutReader.readLine()) != null) {
- stdOut.append(line).append("\n");
- }
-
- BufferedReader stdErrReader = new BufferedReader(
- new InputStreamReader(process.getErrorStream()));
- StringBuilder stdErr = new StringBuilder();
- while ((line = stdErrReader.readLine()) != null) {
- stdErr.append(line).append("\n");
- }
-
- int exitCode = process.waitFor();
-
- if (exitCode == 0) {
- sendExecResultToClient(execId, true, stdOut.toString().trim());
+ ProcessManager.ExecResult result = processManager.executeCommand(cmd, useAlpine);
+
+ if (result.isSuccess()) {
+ sendExecResultToClient(execId, true, result.stdout);
} else {
- String errorOutput = stdErr.toString().trim();
- if (errorOutput.isEmpty()) {
- errorOutput = "Command exited with code: " + exitCode;
- }
- sendExecResultToClient(execId, false, errorOutput);
+ sendExecResultToClient(execId, false, result.getErrorMessage());
}
-
+
cleanup(execId);
} catch (Exception e) {
sendExecResultToClient(execId, false, "Exception: " + e.getMessage());
@@ -235,16 +209,6 @@ private void exec(String execId, String cmd, String alpine) {
});
}
- private void streamOutput(InputStream inputStream, String pid, String streamType) {
- try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream))) {
- String line;
- while ((line = reader.readLine()) != null) {
- sendMessageToClient(pid, streamType, line);
- }
- } catch (IOException ignored) {
- }
- }
-
private void sendMessageToClient(String id, String action, String data) {
Messenger clientMessenger = clientMessengers.get(id);
if (clientMessenger != null) {
@@ -284,64 +248,33 @@ private void writeToProcess(String pid, String input) {
try {
OutputStream os = processInputs.get(pid);
if (os != null) {
- os.write((input + "\n").getBytes());
- os.flush();
+ StreamHandler.writeToStream(os, input);
}
} catch (IOException e) {
e.printStackTrace();
}
}
- private long getPid(Process process) {
- try {
- Field f = process.getClass().getDeclaredField("pid");
- f.setAccessible(true);
- return f.getLong(process);
- } catch (Exception e) {
- return -1;
- }
-}
-
-
private void stopProcess(String pid) {
Process process = processes.get(pid);
if (process != null) {
- try {
- Runtime.getRuntime().exec("kill -9 -" + getPid(process));
- } catch (Exception ignored) {}
- process.destroy();
+ ProcessUtils.killProcessTree(process);
cleanup(pid);
}
}
private void isProcessRunning(String pid, Messenger clientMessenger) {
Process process = processes.get(pid);
- String status = process != null && isProcessAlive(process) ? "running" : "not_found";
+ String status = process != null && ProcessUtils.isAlive(process) ? "running" : "not_found";
sendMessageToClient(pid, "isRunning", status);
}
- private boolean isProcessAlive(Process process) {
- try {
- process.exitValue();
- return false;
- } catch(IllegalThreadStateException e) {
- return true;
- }
- }
-
private void cleanup(String id) {
processes.remove(id);
processInputs.remove(id);
clientMessengers.remove(id);
}
- @Override
- public void onCreate() {
- super.onCreate();
- createNotificationChannel();
- updateNotification();
- }
-
private void createNotificationChannel() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
NotificationChannel serviceChannel = new NotificationChannel(
@@ -391,10 +324,7 @@ public void onDestroy() {
releaseWakeLock();
for (Process process : processes.values()) {
- try {
- Runtime.getRuntime().exec("kill -9 -" + getPid(process));
- } catch (Exception ignored) {}
- process.destroyForcibly();
+ ProcessUtils.killProcessTree(process);
}
processes.clear();
@@ -410,4 +340,4 @@ private int resolveDrawableId(String... names) {
}
return android.R.drawable.sym_def_app_icon;
}
-}
+}
\ No newline at end of file
diff --git a/src/plugins/terminal/www/Executor.js b/src/plugins/terminal/www/Executor.js
index 78103fafc..419713e6d 100644
--- a/src/plugins/terminal/www/Executor.js
+++ b/src/plugins/terminal/www/Executor.js
@@ -1,14 +1,17 @@
/**
- * @module Executor
+ * @class Executor
* @description
- * This module provides an interface to run shell commands from a Cordova app.
+ * This class provides an interface to run shell commands from a Cordova app.
* It supports real-time process streaming, writing input to running processes,
* stopping them, and executing one-time commands.
*/
const exec = require('cordova/exec');
-const Executor = {
+class Executor {
+ constructor(BackgroundExecutor = false) {
+ this.ExecutorType = BackgroundExecutor ? "BackgroundExecutor" : "Executor";
+ }
/**
* Starts a shell process and enables real-time streaming of stdout, stderr, and exit status.
*
@@ -17,137 +20,182 @@ const Executor = {
* - `"stdout"`: Standard output line.
* - `"stderr"`: Standard error line.
* - `"exit"`: Exit code of the process.
- * @param {boolean} alpine - Whether to run the command inside the Alpine sandbox environment (`true`) or on Android directly (`false`).
+ * @param {boolean} [alpine=false] - Whether to run the command inside the Alpine sandbox environment (`true`) or on Android directly (`false`).
* @returns {Promise} Resolves with a unique process ID (UUID) used for future references like `write()` or `stop()`.
*
* @example
- * Executor.start('sh', (type, data) => {
- * console.log(`[${type}] ${data}`);
+ * const executor = new Executor();
+ * executor.start('sh', (type, data) => {
+ * //console.log(`[${type}] ${data}`);
* }).then(uuid => {
- * Executor.write(uuid, 'echo Hello World');
- * Executor.stop(uuid);
+ * executor.write(uuid, 'echo Hello World');
+ * executor.stop(uuid);
* });
*/
-
-
- start(command, onData) {
- this.start(command, onData, false);
- },
-
- start(command, onData, alpine) {
- console.log("start: " + command);
-
+ start(command, onData, alpine = false) {
return new Promise((resolve, reject) => {
let first = true;
- exec(async (message) => {
- console.log(message);
- if (first) {
- first = false;
- await new Promise(resolve => setTimeout(resolve, 100));
- // First message is always the process UUID
- resolve(message);
- } else {
- const match = message.match(/^([^:]+):(.*)$/);
- if (match) {
- const prefix = match[1]; // e.g. "stdout"
- const message = match[2].trim(); // output
- onData(prefix, message);
+ exec(
+ async (message) => {
+ //console.log(message);
+ if (first) {
+ first = false;
+ await new Promise(resolve => setTimeout(resolve, 100));
+ // First message is always the process UUID
+ resolve(message);
} else {
- onData("unknown", message);
+ const match = message.match(/^([^:]+):(.*)$/);
+ if (match) {
+ const prefix = match[1]; // e.g. "stdout"
+ const content = match[2].trim(); // output
+ onData(prefix, content);
+ } else {
+ onData("unknown", message);
+ }
}
- }
- },
+ },
reject,
- "Executor",
+ this.ExecutorType,
"start",
[command, String(alpine)]
);
});
- },
+ }
/**
* Sends input to a running process's stdin.
*
- * @param {string} uuid - The process ID returned by {@link Executor.start}.
+ * @param {string} uuid - The process ID returned by {@link Executor#start}.
* @param {string} input - Input string to send (e.g., shell commands).
* @returns {Promise} Resolves once the input is written.
*
* @example
- * Executor.write(uuid, 'ls /sdcard');
+ * executor.write(uuid, 'ls /sdcard');
*/
write(uuid, input) {
- console.log("write: " + input + " to " + uuid);
+ //console.log("write: " + input + " to " + uuid);
+ return new Promise((resolve, reject) => {
+ exec(resolve, reject, this.ExecutorType, "write", [uuid, input]);
+ });
+ }
+
+ /**
+ * Moves the executor service to the background (stops foreground notification).
+ *
+ * @returns {Promise} Resolves when the service is moved to background.
+ *
+ * @example
+ * executor.moveToBackground();
+ */
+ moveToBackground() {
return new Promise((resolve, reject) => {
- exec(resolve, reject, "Executor", "write", [uuid, input]);
+ exec(resolve, reject, this.ExecutorType, "moveToBackground", []);
});
- },
+ }
+
+ /**
+ * Moves the executor service to the foreground (shows notification).
+ *
+ * @returns {Promise} Resolves when the service is moved to foreground.
+ *
+ * @example
+ * executor.moveToForeground();
+ */
+ moveToForeground() {
+ return new Promise((resolve, reject) => {
+ exec(resolve, reject, this.ExecutorType, "moveToForeground", []);
+ });
+ }
/**
* Terminates a running process.
*
- * @param {string} uuid - The process ID returned by {@link Executor.start}.
+ * @param {string} uuid - The process ID returned by {@link Executor#start}.
* @returns {Promise} Resolves when the process has been stopped.
*
* @example
- * Executor.stop(uuid);
+ * executor.stop(uuid);
*/
stop(uuid) {
return new Promise((resolve, reject) => {
- exec(resolve, reject, "Executor", "stop", [uuid]);
+ exec(resolve, reject, this.ExecutorType, "stop", [uuid]);
});
- },
+ }
/**
* Checks if a process is still running.
*
- * @param {string} uuid - The process ID returned by {@link Executor.start}.
+ * @param {string} uuid - The process ID returned by {@link Executor#start}.
* @returns {Promise} Resolves `true` if the process is running, `false` otherwise.
*
* @example
- * const isAlive = await Executor.isRunning(uuid);
+ * const isAlive = await executor.isRunning(uuid);
*/
isRunning(uuid) {
return new Promise((resolve, reject) => {
- exec((result) => {
- resolve(result === "running");
- }, reject, "Executor", "isRunning", [uuid]);
+ exec(
+ (result) => {
+ resolve(result === "running");
+ },
+ reject,
+ this.ExecutorType,
+ "isRunning",
+ [uuid]
+ );
});
- },
+ }
+ /**
+ * Stops the executor service completely.
+ *
+ * @returns {Promise} Resolves when the service has been stopped.
+ *
+ * @example
+ * executor.stopService();
+ */
stopService() {
return new Promise((resolve, reject) => {
- exec(resolve, reject, "Executor", "stopService", []);
+ exec(resolve, reject, this.ExecutorType, "stopService", []);
});
- },
+ }
/**
* Executes a shell command once and waits for it to finish.
- * Unlike {@link Executor.start}, this does not stream output.
+ * Unlike {@link Executor#start}, this does not stream output.
*
* @param {string} command - The shell command to execute.
- * @param {boolean} alpine - Whether to run the command in the Alpine sandbox (`true`) or Android environment (`false`).
+ * @param {boolean} [alpine=false] - Whether to run the command in the Alpine sandbox (`true`) or Android environment (`false`).
* @returns {Promise} Resolves with standard output on success, rejects with an error or standard error on failure.
*
* @example
- * Executor.execute('ls -l')
- * .then(console.log)
+ * executor.execute('ls -l')
+ * .then(//console.log)
* .catch(console.error);
*/
- execute(command) {
- this.execute(command, false);
- }
- ,
- execute(command, alpine) {
+ execute(command, alpine = false) {
return new Promise((resolve, reject) => {
- exec(resolve, reject, "Executor", "exec", [command, String(alpine)]);
+ exec(resolve, reject, this.ExecutorType, "exec", [command, String(alpine)]);
});
- },
+ }
+ /**
+ * Loads a native library from the specified path.
+ *
+ * @param {string} path - The path to the native library to load.
+ * @returns {Promise} Resolves when the library has been loaded.
+ *
+ * @example
+ * executor.loadLibrary('/path/to/library.so');
+ */
loadLibrary(path) {
return new Promise((resolve, reject) => {
- exec(resolve, reject, "Executor", "loadLibrary", [path]);
+ exec(resolve, reject, this.ExecutorType, "loadLibrary", [path]);
});
}
-};
+}
+
+//backward compatibility
+const executorInstance = new Executor();
+executorInstance.BackgroundExecutor = new Executor(true);
-module.exports = Executor;
+module.exports = executorInstance;
\ No newline at end of file
diff --git a/src/plugins/terminal/www/Terminal.js b/src/plugins/terminal/www/Terminal.js
index 279cefbf6..d63c29c16 100644
--- a/src/plugins/terminal/www/Terminal.js
+++ b/src/plugins/terminal/www/Terminal.js
@@ -91,7 +91,7 @@ const Terminal = {
if (!pidExists) return false;
- const result = await Executor.execute(`kill -0 $(cat $PREFIX/pid) 2>/dev/null && echo "true" || echo "false"`);
+ const result = await Executor.BackgroundExecutor.execute(`kill -0 $(cat $PREFIX/pid) 2>/dev/null && echo "true" || echo "false"`);
return String(result).toLowerCase() === "true";
},