Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 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
4 changes: 4 additions & 0 deletions src/plugins/terminal/plugin.xml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@
</js-module>

<platform name="android">

<framework src="org.java-websocket:Java-WebSocket:1.6.0" />

<config-file parent="/*" target="res/xml/config.xml">
<feature name="Executor">
<param name="android-package" value="com.foxdebug.acode.rk.exec.terminal.Executor" />
Expand All @@ -28,6 +31,7 @@
<source-file src="src/android/StreamHandler.java" target-dir="src/com/foxdebug/acode/rk/exec/terminal" />

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

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

Expand Down
33 changes: 33 additions & 0 deletions src/plugins/terminal/src/android/Executor.java
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@
import android.app.Activity;
import com.foxdebug.acode.rk.exec.terminal.*;

import java.net.ServerSocket;
import java.io.IOException;
Comment thread
RohitKushvaha01 marked this conversation as resolved.
Outdated


public class Executor extends CordovaPlugin {

private Messenger serviceMessenger;
Expand All @@ -41,6 +45,8 @@ public class Executor extends CordovaPlugin {
private final java.util.Map<String, CallbackContext> callbackContextMap = new java.util.concurrent.ConcurrentHashMap<>();

private static final int REQUEST_POST_NOTIFICATIONS = 1001;
private static final int REQUEST_POST_NOTIFICATIONS = 1001;
Comment thread
RohitKushvaha01 marked this conversation as resolved.
Outdated


private void askNotificationPermission(Activity context) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
Expand Down Expand Up @@ -252,6 +258,33 @@ public boolean execute(String action, JSONArray args, CallbackContext callbackCo
return true;
}

if (action.equals("spawn")) {
try {
JSONArray cmdArr = args.getJSONArray(0);
String[] cmd = new String[cmdArr.length()];
for (int i = 0; i < cmdArr.length(); i++) {
cmd[i] = cmdArr.getString(i);
}

int port;
try (ServerSocket socket = new ServerSocket(0)) {
socket.setReuseAddress(true);
port = socket.getLocalPort();
}
Comment thread
RohitKushvaha01 marked this conversation as resolved.

ProcessServer server = new ProcessServer(port, cmd);
server.startAndAwait(); // blocks until onStart() fires — server is listening before port is returned

callbackContext.success(port);
Comment thread
RohitKushvaha01 marked this conversation as resolved.
} catch (Exception e) {
e.printStackTrace();
Comment thread
RohitKushvaha01 marked this conversation as resolved.
callbackContext.error("Failed to spawn process: " + e.getMessage());
}

return true;
}


// For all other actions, ensure service is bound first
if (!ensureServiceBound(callbackContext)) {
// Error already sent by ensureServiceBound
Expand Down
105 changes: 105 additions & 0 deletions src/plugins/terminal/src/android/ProcessServer.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
package com.foxdebug.acode.rk.exec.terminal;

import org.java_websocket.WebSocket;
import org.java_websocket.handshake.ClientHandshake;
import org.java_websocket.server.WebSocketServer;

import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.util.concurrent.CountDownLatch;

class ProcessServer extends WebSocketServer {

private final String[] cmd;
private final CountDownLatch readyLatch = new CountDownLatch(1);

private static final class ConnState {
final Process process;
final OutputStream stdin;

ConnState(Process process, OutputStream stdin) {
this.process = process;
this.stdin = stdin;
}
}

ProcessServer(int port, String[] cmd) {
super(new InetSocketAddress("127.0.0.1", port)); // loopback only
this.cmd = cmd;
}

/** Blocks the calling thread until onStart() fires. */
void startAndAwait() throws InterruptedException {
Comment thread
RohitKushvaha01 marked this conversation as resolved.
Outdated
start();
readyLatch.await(); // returns as soon as the server socket is open
}

@Override
public void onStart() {
readyLatch.countDown(); // unblocks startAndAwait()
}

@Override
public void onOpen(WebSocket conn, ClientHandshake handshake) {
try {
Process process = new ProcessBuilder(cmd).redirectErrorStream(true).start();
InputStream stdout = process.getInputStream();
OutputStream stdin = process.getOutputStream();

conn.setAttachment(new ConnState(process, stdin));

new Thread(() -> {
try {
byte[] buf = new byte[8192];
int len;
while ((len = stdout.read(buf)) != -1) {
conn.send(ByteBuffer.wrap(buf, 0, len));
}
} catch (Exception ignored) {}
}).start();

} catch (Exception ignored) {}
}

@Override
public void onMessage(WebSocket conn, ByteBuffer msg) {
try {
ConnState state = conn.getAttachment();
state.stdin.write(msg.array(), msg.position(), msg.remaining());
state.stdin.flush();
} catch (Exception ignored) {}
Comment thread
RohitKushvaha01 marked this conversation as resolved.
}
Comment thread
RohitKushvaha01 marked this conversation as resolved.

@Override
public void onMessage(WebSocket conn, String message) {
try {
ConnState state = conn.getAttachment();
Comment thread
RohitKushvaha01 marked this conversation as resolved.
state.stdin.write(message.getBytes());
state.stdin.flush();
} catch (Exception ignored) {}
}

@Override
public void onClose(WebSocket conn, int code, String reason, boolean remote) {
stopProcess(conn);
}

@Override
public void onError(WebSocket conn, Exception ex) {
if (conn != null) stopProcess(conn);
}

private void stopProcess(WebSocket conn) {
try {
ConnState state = conn.getAttachment();
if (state != null) state.process.destroy();
stop();
} catch (Exception ignored) {}
Comment thread
RohitKushvaha01 marked this conversation as resolved.
Outdated
Comment thread
RohitKushvaha01 marked this conversation as resolved.
}
}
25 changes: 25 additions & 0 deletions src/plugins/terminal/www/Executor.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,29 @@ class Executor {
constructor(BackgroundExecutor = false) {
this.ExecutorType = BackgroundExecutor ? "BackgroundExecutor" : "Executor";
}

/**
* Spawns a process and exposes it as a raw WebSocket stream.
*
* @param {string[]} cmd - Command and arguments to execute (e.g. `["sh", "-c", "echo hi"]`).
* @param {(ws: WebSocket) => void} callback - Called with the connected WebSocket once the
* process is ready. Use `ws.send()` to write to stdin and `ws.onmessage` to read stdout.
*/
spawnStream(cmd, callback){
Comment thread
RohitKushvaha01 marked this conversation as resolved.
Outdated

exec((port)=>{
const ws = new WebSocket(`ws://127.0.0.1:${port}`);
Comment thread
RohitKushvaha01 marked this conversation as resolved.
ws.binaryType = "arraybuffer";

Comment thread
RohitKushvaha01 marked this conversation as resolved.
ws.onopen = ()=>{
callback(ws);
};

}, null, "Executor", "spawn", [cmd]);

}
Comment thread
RohitKushvaha01 marked this conversation as resolved.
Outdated


/**
* Starts a shell process and enables real-time streaming of stdout, stderr, and exit status.
*
Expand Down Expand Up @@ -150,6 +173,8 @@ class Executor {
*
* @returns {Promise<string>} Resolves when the service has been stopped.
*
* Note: This does not gurantee that all running processes have been killed, but the service will no longer be active. Use with caution.
*
* @example
* executor.stopService();
*/
Expand Down