Skip to content

Commit a7785c5

Browse files
committed
Merge branch 'codemirror' of https://github.com/Acode-Foundation/Acode2 into codemirror
2 parents 350d319 + 3ef5d73 commit a7785c5

File tree

11 files changed

+537
-192
lines changed

11 files changed

+537
-192
lines changed

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,8 @@
3838
"cordova-plugin-browser": {},
3939
"cordova-plugin-sftp": {},
4040
"cordova-plugin-system": {},
41-
"com.foxdebug.acode.rk.exec.terminal": {},
42-
"com.foxdebug.acode.rk.exec.proot": {}
41+
"com.foxdebug.acode.rk.exec.proot": {},
42+
"com.foxdebug.acode.rk.exec.terminal": {}
4343
},
4444
"platforms": [
4545
"android"

src/plugins/terminal/plugin.xml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,19 @@
1616
<feature name="Executor">
1717
<param name="android-package" value="com.foxdebug.acode.rk.exec.terminal.Executor" />
1818
</feature>
19+
<feature name="BackgroundExecutor">
20+
<param name="android-package" value="com.foxdebug.acode.rk.exec.terminal.BackgroundExecutor" />
21+
</feature>
1922
</config-file>
2023
<config-file parent="/*" target="AndroidManifest.xml" />
2124

25+
<source-file src="src/android/BackgroundExecutor.java" target-dir="src/com/foxdebug/acode/rk/exec/terminal" />
26+
<source-file src="src/android/ProcessManager.java" target-dir="src/com/foxdebug/acode/rk/exec/terminal" />
27+
<source-file src="src/android/ProcessUtils.java" target-dir="src/com/foxdebug/acode/rk/exec/terminal" />
28+
<source-file src="src/android/StreamHandler.java" target-dir="src/com/foxdebug/acode/rk/exec/terminal" />
29+
2230
<source-file src="src/android/Executor.java" target-dir="src/com/foxdebug/acode/rk/exec/terminal" />
31+
2332
<source-file src="src/android/TerminalService.java" target-dir="src/com/foxdebug/acode/rk/exec/terminal" />
2433

2534
<source-file src="src/android/AlpineDocumentProvider.java" target-dir="src/com/foxdebug/acode/rk/exec/terminal" />

src/plugins/terminal/src/android/AlpineDocumentProvider.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import java.util.LinkedList;
2121
import java.util.Locale;
2222
import com.foxdebug.acode.R;
23+
import com.foxdebug.acode.rk.exec.terminal.*;
2324

2425
public class AlpineDocumentProvider extends DocumentsProvider {
2526

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
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+
import com.foxdebug.acode.rk.exec.terminal.*;
9+
10+
public class BackgroundExecutor extends CordovaPlugin {
11+
12+
private final Map<String, Process> processes = new ConcurrentHashMap<>();
13+
private final Map<String, OutputStream> processInputs = new ConcurrentHashMap<>();
14+
private final Map<String, CallbackContext> processCallbacks = new ConcurrentHashMap<>();
15+
private ProcessManager processManager;
16+
17+
@Override
18+
public void initialize(CordovaInterface cordova, CordovaWebView webView) {
19+
super.initialize(cordova, webView);
20+
this.processManager = new ProcessManager(cordova.getContext());
21+
}
22+
23+
@Override
24+
public boolean execute(String action, JSONArray args, CallbackContext callbackContext) throws JSONException {
25+
switch (action) {
26+
case "start":
27+
String pid = UUID.randomUUID().toString();
28+
startProcess(pid, args.getString(0), args.getString(1).equals("true"), callbackContext);
29+
return true;
30+
case "write":
31+
writeToProcess(args.getString(0), args.getString(1), callbackContext);
32+
return true;
33+
case "stop":
34+
stopProcess(args.getString(0), callbackContext);
35+
return true;
36+
case "exec":
37+
exec(args.getString(0), args.getString(1).equals("true"), callbackContext);
38+
return true;
39+
case "isRunning":
40+
isProcessRunning(args.getString(0), callbackContext);
41+
return true;
42+
case "loadLibrary":
43+
loadLibrary(args.getString(0), callbackContext);
44+
return true;
45+
default:
46+
callbackContext.error("Unknown action: " + action);
47+
return false;
48+
}
49+
}
50+
51+
private void exec(String cmd, boolean useAlpine, CallbackContext callbackContext) {
52+
cordova.getThreadPool().execute(() -> {
53+
try {
54+
ProcessManager.ExecResult result = processManager.executeCommand(cmd, useAlpine);
55+
56+
if (result.isSuccess()) {
57+
callbackContext.success(result.stdout);
58+
} else {
59+
callbackContext.error(result.getErrorMessage());
60+
}
61+
} catch (Exception e) {
62+
callbackContext.error("Exception: " + e.getMessage());
63+
}
64+
});
65+
}
66+
67+
private void startProcess(String pid, String cmd, boolean useAlpine, CallbackContext callbackContext) {
68+
cordova.getThreadPool().execute(() -> {
69+
try {
70+
ProcessBuilder builder = processManager.createProcessBuilder(cmd, useAlpine);
71+
Process process = builder.start();
72+
73+
processes.put(pid, process);
74+
processInputs.put(pid, process.getOutputStream());
75+
processCallbacks.put(pid, callbackContext);
76+
77+
sendPluginResult(callbackContext, pid, true);
78+
79+
// Stream stdout
80+
new Thread(() -> StreamHandler.streamOutput(
81+
process.getInputStream(),
82+
line -> sendPluginMessage(pid, "stdout:" + line)
83+
)).start();
84+
85+
// Stream stderr
86+
new Thread(() -> StreamHandler.streamOutput(
87+
process.getErrorStream(),
88+
line -> sendPluginMessage(pid, "stderr:" + line)
89+
)).start();
90+
91+
int exitCode = process.waitFor();
92+
sendPluginMessage(pid, "exit:" + exitCode);
93+
cleanup(pid);
94+
} catch (Exception e) {
95+
callbackContext.error("Failed to start process: " + e.getMessage());
96+
}
97+
});
98+
}
99+
100+
private void writeToProcess(String pid, String input, CallbackContext callbackContext) {
101+
try {
102+
OutputStream os = processInputs.get(pid);
103+
if (os != null) {
104+
StreamHandler.writeToStream(os, input);
105+
callbackContext.success("Written to process");
106+
} else {
107+
callbackContext.error("Process not found or closed");
108+
}
109+
} catch (IOException e) {
110+
callbackContext.error("Write error: " + e.getMessage());
111+
}
112+
}
113+
114+
private void stopProcess(String pid, CallbackContext callbackContext) {
115+
Process process = processes.get(pid);
116+
if (process != null) {
117+
ProcessUtils.killProcessTree(process);
118+
cleanup(pid);
119+
callbackContext.success("Process terminated");
120+
} else {
121+
callbackContext.error("No such process");
122+
}
123+
}
124+
125+
private void isProcessRunning(String pid, CallbackContext callbackContext) {
126+
Process process = processes.get(pid);
127+
128+
if (process != null) {
129+
String status = ProcessUtils.isAlive(process) ? "running" : "exited";
130+
if (status.equals("exited")) cleanup(pid);
131+
callbackContext.success(status);
132+
} else {
133+
callbackContext.success("not_found");
134+
}
135+
}
136+
137+
private void loadLibrary(String path, CallbackContext callbackContext) {
138+
try {
139+
System.load(path);
140+
callbackContext.success("Library loaded successfully.");
141+
} catch (Exception e) {
142+
callbackContext.error("Failed to load library: " + e.getMessage());
143+
}
144+
}
145+
146+
private void sendPluginResult(CallbackContext ctx, String message, boolean keepCallback) {
147+
PluginResult result = new PluginResult(PluginResult.Status.OK, message);
148+
result.setKeepCallback(keepCallback);
149+
ctx.sendPluginResult(result);
150+
}
151+
152+
private void sendPluginMessage(String pid, String message) {
153+
CallbackContext ctx = processCallbacks.get(pid);
154+
if (ctx != null) {
155+
sendPluginResult(ctx, message, true);
156+
}
157+
}
158+
159+
private void cleanup(String pid) {
160+
processes.remove(pid);
161+
processInputs.remove(pid);
162+
processCallbacks.remove(pid);
163+
}
164+
}

src/plugins/terminal/src/android/Executor.java

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import androidx.core.app.ActivityCompat;
2828
import androidx.core.content.ContextCompat;
2929
import android.app.Activity;
30+
import com.foxdebug.acode.rk.exec.terminal.*;
3031

3132
public class Executor extends CordovaPlugin {
3233

@@ -235,6 +236,22 @@ public boolean execute(String action, JSONArray args, CallbackContext callbackCo
235236
return true;
236237
}
237238

239+
if (action.equals("moveToBackground")) {
240+
Intent intent = new Intent(context, TerminalService.class);
241+
intent.setAction(TerminalService.MOVE_TO_BACKGROUND);
242+
context.startService(intent);
243+
callbackContext.success("Service moved to background mode");
244+
return true;
245+
}
246+
247+
if (action.equals("moveToForeground")) {
248+
Intent intent = new Intent(context, TerminalService.class);
249+
intent.setAction(TerminalService.MOVE_TO_FOREGROUND);
250+
context.startService(intent);
251+
callbackContext.success("Service moved to foreground mode");
252+
return true;
253+
}
254+
238255
// For all other actions, ensure service is bound first
239256
if (!ensureServiceBound(callbackContext)) {
240257
// Error already sent by ensureServiceBound
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
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+
import com.foxdebug.acode.rk.exec.terminal.*;
9+
10+
public class ProcessManager {
11+
12+
private final Context context;
13+
14+
public ProcessManager(Context context) {
15+
this.context = context;
16+
}
17+
18+
/**
19+
* Creates a ProcessBuilder with common environment setup
20+
*/
21+
public ProcessBuilder createProcessBuilder(String cmd, boolean useAlpine) {
22+
String xcmd = useAlpine ? "source $PREFIX/init-sandbox.sh " + cmd : cmd;
23+
ProcessBuilder builder = new ProcessBuilder("sh", "-c", xcmd);
24+
setupEnvironment(builder.environment());
25+
return builder;
26+
}
27+
28+
/**
29+
* Sets up common environment variables
30+
*/
31+
private void setupEnvironment(Map<String, String> env) {
32+
env.put("PREFIX", context.getFilesDir().getAbsolutePath());
33+
env.put("NATIVE_DIR", context.getApplicationInfo().nativeLibraryDir);
34+
35+
TimeZone tz = TimeZone.getDefault();
36+
env.put("ANDROID_TZ", tz.getID());
37+
38+
try {
39+
int target = context.getPackageManager()
40+
.getPackageInfo(context.getPackageName(), 0)
41+
.applicationInfo.targetSdkVersion;
42+
env.put("FDROID", String.valueOf(target <= 28));
43+
} catch (PackageManager.NameNotFoundException e) {
44+
e.printStackTrace();
45+
}
46+
}
47+
48+
/**
49+
* Reads all output from a stream
50+
*/
51+
public static String readStream(InputStream stream) throws IOException {
52+
BufferedReader reader = new BufferedReader(new InputStreamReader(stream));
53+
StringBuilder output = new StringBuilder();
54+
String line;
55+
while ((line = reader.readLine()) != null) {
56+
output.append(line).append("\n");
57+
}
58+
return output.toString();
59+
}
60+
61+
/**
62+
* Executes a command and returns the result
63+
*/
64+
public ExecResult executeCommand(String cmd, boolean useAlpine) throws Exception {
65+
ProcessBuilder builder = createProcessBuilder(cmd, useAlpine);
66+
Process process = builder.start();
67+
68+
String stdout = readStream(process.getInputStream());
69+
String stderr = readStream(process.getErrorStream());
70+
int exitCode = process.waitFor();
71+
72+
return new ExecResult(exitCode, stdout.trim(), stderr.trim());
73+
}
74+
75+
/**
76+
* Result container for command execution
77+
*/
78+
public static class ExecResult {
79+
public final int exitCode;
80+
public final String stdout;
81+
public final String stderr;
82+
83+
public ExecResult(int exitCode, String stdout, String stderr) {
84+
this.exitCode = exitCode;
85+
this.stdout = stdout;
86+
this.stderr = stderr;
87+
}
88+
89+
public boolean isSuccess() {
90+
return exitCode == 0;
91+
}
92+
93+
public String getErrorMessage() {
94+
if (!stderr.isEmpty()) {
95+
return stderr;
96+
}
97+
return "Command exited with code: " + exitCode;
98+
}
99+
}
100+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package com.foxdebug.acode.rk.exec.terminal;
2+
3+
import java.lang.reflect.Field;
4+
import com.foxdebug.acode.rk.exec.terminal.*;
5+
6+
public class ProcessUtils {
7+
8+
/**
9+
* Gets the PID of a process using reflection
10+
*/
11+
public static long getPid(Process process) {
12+
try {
13+
Field f = process.getClass().getDeclaredField("pid");
14+
f.setAccessible(true);
15+
return f.getLong(process);
16+
} catch (Exception e) {
17+
return -1;
18+
}
19+
}
20+
21+
/**
22+
* Checks if a process is still alive
23+
*/
24+
public static boolean isAlive(Process process) {
25+
try {
26+
process.exitValue();
27+
return false;
28+
} catch(IllegalThreadStateException e) {
29+
return true;
30+
}
31+
}
32+
33+
/**
34+
* Forcefully kills a process and its children
35+
*/
36+
public static void killProcessTree(Process process) {
37+
try {
38+
long pid = getPid(process);
39+
if (pid > 0) {
40+
Runtime.getRuntime().exec("kill -9 -" + pid);
41+
}
42+
} catch (Exception ignored) {}
43+
process.destroy();
44+
}
45+
}

0 commit comments

Comments
 (0)