Skip to content

Commit 41973b3

Browse files
feat: added backgroudExecutor
1 parent a2aa28d commit 41973b3

7 files changed

Lines changed: 488 additions & 197 deletions

File tree

src/plugins/terminal/plugin.xml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,10 @@
1919
</config-file>
2020
<config-file parent="/*" target="AndroidManifest.xml" />
2121

22+
<source-file src="src/android/BackgroundExecutor.java" target-dir="src/com/foxdebug/acode/rk/exec/terminal" />
23+
2224
<source-file src="src/android/Executor.java" target-dir="src/com/foxdebug/acode/rk/exec/terminal" />
25+
2326
<source-file src="src/android/TerminalService.java" target-dir="src/com/foxdebug/acode/rk/exec/terminal" />
2427

2528
<source-file src="src/android/AlpineDocumentProvider.java" target-dir="src/com/foxdebug/acode/rk/exec/terminal" />
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
package com.foxdebug.acode.rk.exec.terminal;
2+
3+
import org.apache.cordova.*;
4+
import org.json.*;
5+
import java.io.*;
6+
import java.util.*;
7+
import java.util.concurrent.*;
8+
9+
public class BackgroundExecutor extends CordovaPlugin {
10+
11+
private final Map<String, Process> processes = new ConcurrentHashMap<>();
12+
private final Map<String, OutputStream> processInputs = new ConcurrentHashMap<>();
13+
private final Map<String, CallbackContext> processCallbacks = new ConcurrentHashMap<>();
14+
private ProcessManager processManager;
15+
16+
@Override
17+
public void initialize(CordovaInterface cordova, CordovaWebView webView) {
18+
super.initialize(cordova, webView);
19+
this.processManager = new ProcessManager(cordova.getContext());
20+
}
21+
22+
@Override
23+
public boolean execute(String action, JSONArray args, CallbackContext callbackContext) throws JSONException {
24+
switch (action) {
25+
case "start":
26+
String pid = UUID.randomUUID().toString();
27+
startProcess(pid, args.getString(0), args.getString(1).equals("true"), callbackContext);
28+
return true;
29+
case "write":
30+
writeToProcess(args.getString(0), args.getString(1), callbackContext);
31+
return true;
32+
case "stop":
33+
stopProcess(args.getString(0), callbackContext);
34+
return true;
35+
case "exec":
36+
exec(args.getString(0), args.getString(1).equals("true"), callbackContext);
37+
return true;
38+
case "isRunning":
39+
isProcessRunning(args.getString(0), callbackContext);
40+
return true;
41+
case "loadLibrary":
42+
loadLibrary(args.getString(0), callbackContext);
43+
return true;
44+
default:
45+
callbackContext.error("Unknown action: " + action);
46+
return false;
47+
}
48+
}
49+
50+
private void exec(String cmd, boolean useAlpine, CallbackContext callbackContext) {
51+
cordova.getThreadPool().execute(() -> {
52+
try {
53+
ProcessManager.ExecResult result = processManager.executeCommand(cmd, useAlpine);
54+
55+
if (result.isSuccess()) {
56+
callbackContext.success(result.stdout);
57+
} else {
58+
callbackContext.error(result.getErrorMessage());
59+
}
60+
} catch (Exception e) {
61+
callbackContext.error("Exception: " + e.getMessage());
62+
}
63+
});
64+
}
65+
66+
private void startProcess(String pid, String cmd, boolean useAlpine, CallbackContext callbackContext) {
67+
cordova.getThreadPool().execute(() -> {
68+
try {
69+
ProcessBuilder builder = processManager.createProcessBuilder(cmd, useAlpine);
70+
Process process = builder.start();
71+
72+
processes.put(pid, process);
73+
processInputs.put(pid, process.getOutputStream());
74+
processCallbacks.put(pid, callbackContext);
75+
76+
sendPluginResult(callbackContext, pid, true);
77+
78+
// Stream stdout
79+
new Thread(() -> StreamHandler.streamOutput(
80+
process.getInputStream(),
81+
line -> sendPluginMessage(pid, "stdout:" + line)
82+
)).start();
83+
84+
// Stream stderr
85+
new Thread(() -> StreamHandler.streamOutput(
86+
process.getErrorStream(),
87+
line -> sendPluginMessage(pid, "stderr:" + line)
88+
)).start();
89+
90+
int exitCode = process.waitFor();
91+
sendPluginMessage(pid, "exit:" + exitCode);
92+
cleanup(pid);
93+
} catch (Exception e) {
94+
callbackContext.error("Failed to start process: " + e.getMessage());
95+
}
96+
});
97+
}
98+
99+
private void writeToProcess(String pid, String input, CallbackContext callbackContext) {
100+
try {
101+
OutputStream os = processInputs.get(pid);
102+
if (os != null) {
103+
StreamHandler.writeToStream(os, input);
104+
callbackContext.success("Written to process");
105+
} else {
106+
callbackContext.error("Process not found or closed");
107+
}
108+
} catch (IOException e) {
109+
callbackContext.error("Write error: " + e.getMessage());
110+
}
111+
}
112+
113+
private void stopProcess(String pid, CallbackContext callbackContext) {
114+
Process process = processes.get(pid);
115+
if (process != null) {
116+
ProcessUtils.killProcessTree(process);
117+
cleanup(pid);
118+
callbackContext.success("Process terminated");
119+
} else {
120+
callbackContext.error("No such process");
121+
}
122+
}
123+
124+
private void isProcessRunning(String pid, CallbackContext callbackContext) {
125+
Process process = processes.get(pid);
126+
127+
if (process != null) {
128+
String status = ProcessUtils.isAlive(process) ? "running" : "exited";
129+
if (status.equals("exited")) cleanup(pid);
130+
callbackContext.success(status);
131+
} else {
132+
callbackContext.success("not_found");
133+
}
134+
}
135+
136+
private void loadLibrary(String path, CallbackContext callbackContext) {
137+
try {
138+
System.load(path);
139+
callbackContext.success("Library loaded successfully.");
140+
} catch (Exception e) {
141+
callbackContext.error("Failed to load library: " + e.getMessage());
142+
}
143+
}
144+
145+
private void sendPluginResult(CallbackContext ctx, String message, boolean keepCallback) {
146+
PluginResult result = new PluginResult(PluginResult.Status.OK, message);
147+
result.setKeepCallback(keepCallback);
148+
ctx.sendPluginResult(result);
149+
}
150+
151+
private void sendPluginMessage(String pid, String message) {
152+
CallbackContext ctx = processCallbacks.get(pid);
153+
if (ctx != null) {
154+
sendPluginResult(ctx, message, true);
155+
}
156+
}
157+
158+
private void cleanup(String pid) {
159+
processes.remove(pid);
160+
processInputs.remove(pid);
161+
processCallbacks.remove(pid);
162+
}
163+
}
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
package com.foxdebug.acode.rk.exec.terminal;
2+
3+
import android.content.Context;
4+
import android.content.pm.PackageManager;
5+
import java.io.*;
6+
import java.util.Map;
7+
import java.util.TimeZone;
8+
9+
public class ProcessManager {
10+
11+
private final Context context;
12+
13+
public ProcessManager(Context context) {
14+
this.context = context;
15+
}
16+
17+
/**
18+
* Creates a ProcessBuilder with common environment setup
19+
*/
20+
public ProcessBuilder createProcessBuilder(String cmd, boolean useAlpine) {
21+
String xcmd = useAlpine ? "source $PREFIX/init-sandbox.sh " + cmd : cmd;
22+
ProcessBuilder builder = new ProcessBuilder("sh", "-c", xcmd);
23+
setupEnvironment(builder.environment());
24+
return builder;
25+
}
26+
27+
/**
28+
* Sets up common environment variables
29+
*/
30+
private void setupEnvironment(Map<String, String> env) {
31+
env.put("PREFIX", context.getFilesDir().getAbsolutePath());
32+
env.put("NATIVE_DIR", context.getApplicationInfo().nativeLibraryDir);
33+
34+
TimeZone tz = TimeZone.getDefault();
35+
env.put("ANDROID_TZ", tz.getID());
36+
37+
try {
38+
int target = context.getPackageManager()
39+
.getPackageInfo(context.getPackageName(), 0)
40+
.applicationInfo.targetSdkVersion;
41+
env.put("FDROID", String.valueOf(target <= 28));
42+
} catch (PackageManager.NameNotFoundException e) {
43+
e.printStackTrace();
44+
}
45+
}
46+
47+
/**
48+
* Reads all output from a stream
49+
*/
50+
public static String readStream(InputStream stream) throws IOException {
51+
BufferedReader reader = new BufferedReader(new InputStreamReader(stream));
52+
StringBuilder output = new StringBuilder();
53+
String line;
54+
while ((line = reader.readLine()) != null) {
55+
output.append(line).append("\n");
56+
}
57+
return output.toString();
58+
}
59+
60+
/**
61+
* Executes a command and returns the result
62+
*/
63+
public ExecResult executeCommand(String cmd, boolean useAlpine) throws Exception {
64+
ProcessBuilder builder = createProcessBuilder(cmd, useAlpine);
65+
Process process = builder.start();
66+
67+
String stdout = readStream(process.getInputStream());
68+
String stderr = readStream(process.getErrorStream());
69+
int exitCode = process.waitFor();
70+
71+
return new ExecResult(exitCode, stdout.trim(), stderr.trim());
72+
}
73+
74+
/**
75+
* Result container for command execution
76+
*/
77+
public static class ExecResult {
78+
public final int exitCode;
79+
public final String stdout;
80+
public final String stderr;
81+
82+
public ExecResult(int exitCode, String stdout, String stderr) {
83+
this.exitCode = exitCode;
84+
this.stdout = stdout;
85+
this.stderr = stderr;
86+
}
87+
88+
public boolean isSuccess() {
89+
return exitCode == 0;
90+
}
91+
92+
public String getErrorMessage() {
93+
if (!stderr.isEmpty()) {
94+
return stderr;
95+
}
96+
return "Command exited with code: " + exitCode;
97+
}
98+
}
99+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package com.foxdebug.acode.rk.exec.terminal;
2+
3+
import java.lang.reflect.Field;
4+
5+
public class ProcessUtils {
6+
7+
/**
8+
* Gets the PID of a process using reflection
9+
*/
10+
public static long getPid(Process process) {
11+
try {
12+
Field f = process.getClass().getDeclaredField("pid");
13+
f.setAccessible(true);
14+
return f.getLong(process);
15+
} catch (Exception e) {
16+
return -1;
17+
}
18+
}
19+
20+
/**
21+
* Checks if a process is still alive
22+
*/
23+
public static boolean isAlive(Process process) {
24+
try {
25+
process.exitValue();
26+
return false;
27+
} catch(IllegalThreadStateException e) {
28+
return true;
29+
}
30+
}
31+
32+
/**
33+
* Forcefully kills a process and its children
34+
*/
35+
public static void killProcessTree(Process process) {
36+
try {
37+
long pid = getPid(process);
38+
if (pid > 0) {
39+
Runtime.getRuntime().exec("kill -9 -" + pid);
40+
}
41+
} catch (Exception ignored) {}
42+
process.destroy();
43+
}
44+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package com.foxdebug.acode.rk.exec.terminal;
2+
3+
import java.io.*;
4+
5+
public class StreamHandler {
6+
7+
public interface OutputListener {
8+
void onLine(String line);
9+
}
10+
11+
/**
12+
* Streams output from an InputStream to a listener
13+
*/
14+
public static void streamOutput(InputStream inputStream, OutputListener listener) {
15+
try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream))) {
16+
String line;
17+
while ((line = reader.readLine()) != null) {
18+
listener.onLine(line);
19+
}
20+
} catch (IOException ignored) {
21+
}
22+
}
23+
24+
/**
25+
* Writes input to an OutputStream
26+
*/
27+
public static void writeToStream(OutputStream outputStream, String input) throws IOException {
28+
outputStream.write((input + "\n").getBytes());
29+
outputStream.flush();
30+
}
31+
}

0 commit comments

Comments
 (0)