Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
6 changes: 3 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions src/components/terminal/terminalManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -564,6 +564,10 @@ class TerminalManager {
// Remove from map
this.terminals.delete(terminalId);

if (this.getAllTerminals().size <= 0) {
Executor.stopService();
}

console.log(`Terminal ${terminalId} closed`);
} catch (error) {
console.error(`Error closing terminal ${terminalId}:`, error);
Expand Down
1 change: 0 additions & 1 deletion src/plugins/system/android/com/foxdebug/system/System.java
Original file line number Diff line number Diff line change
Expand Up @@ -322,7 +322,6 @@ public void run() {
callbackContext.error("mkdirs failed");
}
return true;

default:
return false;
}
Expand Down
196 changes: 147 additions & 49 deletions src/plugins/terminal/src/android/Executor.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,9 @@
import java.util.UUID;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
//import com.foxdebug.acode.rk.exec.terminal.TerminalService;
import android.Manifest;
import android.content.pm.PackageManager;
import android.os.Build;
import android.os.Bundle;

import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
Expand All @@ -34,13 +32,15 @@ public class Executor extends CordovaPlugin {

private Messenger serviceMessenger;
private boolean isServiceBound;
private boolean isServiceBinding; // Track if binding is in progress
private Context context;
private Activity activity;
private final Messenger handlerMessenger = new Messenger(new IncomingHandler());
private CountDownLatch serviceConnectedLatch = new CountDownLatch(1);
private CountDownLatch serviceConnectedLatch;
private final java.util.Map<String, CallbackContext> callbackContextMap = new java.util.concurrent.ConcurrentHashMap<>();

private static final int REQUEST_POST_NOTIFICATIONS = 1001;

private void askNotificationPermission(Activity context) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
if (ContextCompat.checkSelfPermission(
Expand Down Expand Up @@ -69,27 +69,105 @@ public void initialize(CordovaInterface cordova, CordovaWebView webView) {
this.context = cordova.getContext();
this.activity = cordova.getActivity();
askNotificationPermission(activity);
bindService();

// Don't bind service immediately - wait until needed
Log.d("Executor", "Plugin initialized - service will be started when needed");
}

/**
* Ensure service is bound and ready for communication
* Returns true if service is ready, false if binding failed
*/
private boolean ensureServiceBound(CallbackContext callbackContext) {
// If already bound, return immediately
if (isServiceBound && serviceMessenger != null) {
return true;
}

// If binding is already in progress, wait for it
if (isServiceBinding) {
try {
if (serviceConnectedLatch != null &&
serviceConnectedLatch.await(10, TimeUnit.SECONDS)) {
return isServiceBound;
} else {
callbackContext.error("Service binding timeout");
return false;
}
} catch (InterruptedException e) {
callbackContext.error("Service binding interrupted: " + e.getMessage());
return false;
}
}

// Start binding process
Log.d("Executor", "Starting service binding...");
return bindServiceNow(callbackContext);
}

private void bindService() {
/**
* Immediately bind to service
*/
private boolean bindServiceNow(CallbackContext callbackContext) {
if (isServiceBinding) {
return false; // Already binding
}

isServiceBinding = true;
serviceConnectedLatch = new CountDownLatch(1);

Intent intent = new Intent(context, TerminalService.class);

// Start the service first
context.startService(intent);
context.bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE);

// Then bind to it
boolean bindResult = context.bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE);

if (!bindResult) {
Log.e("Executor", "Failed to bind to service");
isServiceBinding = false;
callbackContext.error("Failed to bind to service");
return false;
}

// Wait for connection
try {
if (serviceConnectedLatch.await(10, TimeUnit.SECONDS)) {
Log.d("Executor", "Service bound successfully");
return isServiceBound;
} else {
Log.e("Executor", "Service binding timeout");
callbackContext.error("Service binding timeout");
isServiceBinding = false;
return false;
}
} catch (InterruptedException e) {
Log.e("Executor", "Service binding interrupted: " + e.getMessage());
callbackContext.error("Service binding interrupted: " + e.getMessage());
isServiceBinding = false;
return false;
}
}

private final ServiceConnection serviceConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
Log.d("Executor", "Service connected");
serviceMessenger = new Messenger(service);
isServiceBound = true;
serviceConnectedLatch.countDown();
isServiceBinding = false;
if (serviceConnectedLatch != null) {
serviceConnectedLatch.countDown();
}
}

@Override
public void onServiceDisconnected(ComponentName name) {
Log.w("Executor", "Service disconnected");
serviceMessenger = null;
isServiceBound = false;
isServiceBinding = false;
serviceConnectedLatch = new CountDownLatch(1);
}
};
Expand Down Expand Up @@ -130,6 +208,7 @@ public void handleMessage(Message msg) {
break;
case "isRunning":
callbackContext.success(data);
cleanupCallback(pid);
break;
}
}
Expand All @@ -139,18 +218,26 @@ public void handleMessage(Message msg) {

@Override
public boolean execute(String action, JSONArray args, CallbackContext callbackContext) throws JSONException {
try {
if (!isServiceBound && !serviceConnectedLatch.await(5, TimeUnit.SECONDS)) {
callbackContext.error("Service not bound - timeout");
return false;
// For actions that don't need the service, handle them directly
if (action.equals("loadLibrary")) {
try {
System.load(args.getString(0));
callbackContext.success("Library loaded successfully.");
} catch (Exception e) {
callbackContext.error("Failed to load library: " + e.getMessage());
}
} catch (InterruptedException e) {
callbackContext.error("Service binding interrupted: " + e.getMessage());
return false;
return true;
}

if (!isServiceBound) {
callbackContext.error("Service not bound");
if (action.equals("stopService")) {
stopServiceNow();
callbackContext.success("Service stopped");
return true;
}

// For all other actions, ensure service is bound first
if (!ensureServiceBound(callbackContext)) {
// Error already sent by ensureServiceBound
return false;
}

Expand Down Expand Up @@ -180,45 +267,59 @@ public boolean execute(String action, JSONArray args, CallbackContext callbackCo
callbackContextMap.put(pidCheck, callbackContext);
isProcessRunning(pidCheck);
return true;
case "loadLibrary":
try {
System.load(args.getString(0));
callbackContext.success("Library loaded successfully.");
} catch (Exception e) {
callbackContext.error("Failed to load library: " + e.getMessage());
}
return true;
default:
callbackContext.error("Unknown action: " + action);
return false;
}
}

private void startProcess(String pid, String cmd, String alpine) {
CallbackContext callbackContext = getCallbackContext(pid);
if (callbackContext != null) {
PluginResult result = new PluginResult(PluginResult.Status.OK, pid);
result.setKeepCallback(true);
callbackContext.sendPluginResult(result);
private void stopServiceNow() {
if (isServiceBound) {
try {
context.unbindService(serviceConnection);
Log.d("Executor", "Service unbound");
} catch (IllegalArgumentException ignored) {
// already unbound
}
isServiceBound = false;
}
isServiceBinding = false;

Intent intent = new Intent(context, TerminalService.class);
boolean stopped = context.stopService(intent);
Log.d("Executor", "Service stop result: " + stopped);

serviceMessenger = null;
if (serviceConnectedLatch == null) {
serviceConnectedLatch = new CountDownLatch(1);
}
}

Message msg = Message.obtain(null, TerminalService.MSG_START_PROCESS);
msg.replyTo = handlerMessenger;
Bundle bundle = new Bundle();
bundle.putString("id", pid);
bundle.putString("cmd", cmd);
bundle.putString("alpine", alpine);
msg.setData(bundle);
try {
serviceMessenger.send(msg);
} catch (RemoteException e) {
CallbackContext errorContext = getCallbackContext(pid);
if (errorContext != null) {
errorContext.error("Failed to start process: " + e.getMessage());
cleanupCallback(pid);
private void startProcess(String pid, String cmd, String alpine) {
CallbackContext callbackContext = getCallbackContext(pid);
if (callbackContext != null) {
PluginResult result = new PluginResult(PluginResult.Status.OK, pid);
result.setKeepCallback(true);
callbackContext.sendPluginResult(result);
}

Message msg = Message.obtain(null, TerminalService.MSG_START_PROCESS);
msg.replyTo = handlerMessenger;
Bundle bundle = new Bundle();
bundle.putString("id", pid);
bundle.putString("cmd", cmd);
bundle.putString("alpine", alpine);
msg.setData(bundle);
try {
serviceMessenger.send(msg);
} catch (RemoteException e) {
CallbackContext errorContext = getCallbackContext(pid);
if (errorContext != null) {
errorContext.error("Failed to start process: " + e.getMessage());
cleanupCallback(pid);
}
}
}
}

private void exec(String execId, String cmd, String alpine) {
Message msg = Message.obtain(null, TerminalService.MSG_EXEC);
Expand Down Expand Up @@ -294,9 +395,6 @@ private void cleanupCallback(String id) {
@Override
public void onDestroy() {
super.onDestroy();
if (isServiceBound) {
context.unbindService(serviceConnection);
isServiceBound = false;
}
stopServiceNow();
}
}
1 change: 1 addition & 0 deletions src/plugins/terminal/src/android/TerminalService.java
Original file line number Diff line number Diff line change
Expand Up @@ -374,6 +374,7 @@ private void updateNotification() {

@Override
public void onDestroy() {
stopForeground(true);
super.onDestroy();
releaseWakeLock();

Expand Down
6 changes: 6 additions & 0 deletions src/plugins/terminal/www/Executor.js
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,12 @@ const Executor = {
});
},

stopService() {
return new Promise((resolve, reject) => {
exec(resolve, reject, "Executor", "stopService", []);
});
},

/**
* Executes a shell command once and waits for it to finish.
* Unlike {@link Executor.start}, this does not stream output.
Expand Down