diff --git a/app/build.gradle b/app/build.gradle index 5811c7ed..85343569 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -25,10 +25,6 @@ android { implementation project(":terminal-view") implementation project(":termux-shared") - - - implementation 'com.github.termux:termux-am-library:1.0' - } defaultConfig { diff --git a/app/src/main/java/com/termux/app/TermuxApplication.java b/app/src/main/java/com/termux/app/TermuxApplication.java index d844c652..78a4bf52 100644 --- a/app/src/main/java/com/termux/app/TermuxApplication.java +++ b/app/src/main/java/com/termux/app/TermuxApplication.java @@ -3,15 +3,14 @@ package com.termux.app; import android.app.Application; import android.content.Context; -import com.termux.am.Am; import com.termux.shared.errors.Error; import com.termux.shared.logger.Logger; -import com.termux.shared.shell.LocalSocketListener; import com.termux.shared.termux.TermuxConstants; import com.termux.shared.termux.crash.TermuxCrashUtils; import com.termux.shared.termux.file.TermuxFileUtils; import com.termux.shared.termux.settings.preferences.TermuxAppSharedPreferences; import com.termux.shared.termux.settings.properties.TermuxAppSharedProperties; +import com.termux.shared.termux.shell.am.TermuxAmSocketServer; import com.termux.shared.termux.theme.TermuxThemeUtils; public class TermuxApplication extends Application { @@ -50,17 +49,8 @@ public class TermuxApplication extends Application { Logger.logErrorExtended(LOG_TAG, "Create apps/termux-app directory failed\n" + error); return; } - } - if (LocalSocketListener.tryEstablishLocalSocketListener(this, (args, out, err) -> { - try { - new Am(out, err, this).run(args); - return 0; - } catch (Exception e) { - return 1; - } - }, TermuxConstants.TERMUX_FILES_DIR_PATH+"/api/am-socket", 100, 1000) == null) { - Logger.logWarn("TermuxApplication", "am socket cannot be created"); + TermuxAmSocketServer.setupTermuxAmSocketServer(context); } } @@ -74,4 +64,3 @@ public class TermuxApplication extends Application { } } - diff --git a/termux-shared/build.gradle b/termux-shared/build.gradle index 97f60d49..0f3f6cf9 100644 --- a/termux-shared/build.gradle +++ b/termux-shared/build.gradle @@ -26,6 +26,8 @@ android { implementation "commons-io:commons-io:2.5" implementation project(":terminal-view") + + implementation 'com.github.termux:termux-am-library:1.0' } defaultConfig { diff --git a/termux-shared/src/main/java/com/termux/shared/shell/am/AmSocketServer.java b/termux-shared/src/main/java/com/termux/shared/shell/am/AmSocketServer.java new file mode 100644 index 00000000..f94e3453 --- /dev/null +++ b/termux-shared/src/main/java/com/termux/shared/shell/am/AmSocketServer.java @@ -0,0 +1,239 @@ +package com.termux.shared.shell.am; + +import android.app.Application; +import android.content.Context; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.termux.am.Am; +import com.termux.shared.errors.Error; +import com.termux.shared.logger.Logger; +import com.termux.shared.net.socket.local.ILocalSocketManager; +import com.termux.shared.net.socket.local.LocalClientSocket; +import com.termux.shared.net.socket.local.LocalServerSocket; +import com.termux.shared.net.socket.local.LocalSocketManager; +import com.termux.shared.net.socket.local.LocalSocketManagerClientBase; +import com.termux.shared.net.socket.local.LocalSocketRunConfig; +import com.termux.shared.shell.ArgumentTokenizer; +import com.termux.shared.shell.command.ExecutionCommand; + +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * A AF_UNIX/SOCK_STREAM local server managed with {@link LocalSocketManager} whose + * {@link LocalServerSocket} receives android activity manager (am) commands from {@link LocalClientSocket} + * and runs them with termux-am-library. It would normally only allow processes belonging to the + * server app's user and root user to connect to it. + * + * The client must send the am command as a string without the initial "am" arg on its output stream + * and then wait for the result on its input stream. The result of the execution or error is sent + * back in the format `exit_code\0stdout\0stderr\0` where `\0` represents a null character. + * Check termux/termux-am-socket for implementation of a native c client. + * + * Usage: + * 1. Optionally extend {@link AmSocketServerClient}, the implementation for + * {@link ILocalSocketManager} that will receive call backs from the server including + * when client connects via {@link ILocalSocketManager#onClientAccepted(LocalSocketManager, LocalClientSocket)}. + * 2. Create a {@link LocalSocketRunConfig} instance with the run config of the am server. It would + * be better to use a filesystem socket instead of abstract namespace socket for security reasons. + * 3. Call {@link #start(Context, LocalSocketRunConfig)} to start the server and store the {@link LocalSocketManager} + * instance returned. + * 4. Stop server if needed with a call to {@link LocalSocketManager#stop()} on the + * {@link LocalSocketManager} instance returned by start call. + * + * https://github.com/termux/termux-am-library/blob/main/termux-am-library/src/main/java/com/termux/am/Am.java + * https://github.com/termux/termux-am-socket + * https://developer.android.com/studio/command-line/adb#am + * https://cs.android.com/android/platform/superproject/+/android-12.0.0_r32:frameworks/base/services/core/java/com/android/server/am/ActivityManagerShellCommand.java + */ +public class AmSocketServer { + + public static final String LOG_TAG = "AmSocketServer"; + + /** + * Create the {@link AmSocketServer} {@link LocalServerSocket} and start listening for new {@link LocalClientSocket}. + * + * @param context The {@link Context} for {@link LocalSocketManager}. + * @param localSocketRunConfig The {@link LocalSocketRunConfig} for {@link LocalSocketManager}. + */ + public static synchronized LocalSocketManager start(@NonNull Context context, + @NonNull LocalSocketRunConfig localSocketRunConfig) { + LocalSocketManager localSocketManager = new LocalSocketManager(context, localSocketRunConfig); + Error error = localSocketManager.start(); + if (error != null) { + localSocketManager.onError(error); + return null; + } + + return localSocketManager; + } + + public static void processAmClient(@NonNull LocalSocketManager localSocketManager, + @NonNull LocalClientSocket clientSocket) { + Error error; + + // Read amCommandString client sent and close input stream + StringBuilder data = new StringBuilder(); + error = clientSocket.readDataOnInputStream(data, true); + if (error != null) { + sendResultToClient(localSocketManager, clientSocket, 1, null, error.toString()); + return; + } + + String amCommandString = data.toString(); + + Logger.logVerbose(LOG_TAG, "am command received from peer " + clientSocket.getPeerCred().getMinimalString() + + "\nam command: `" + amCommandString + "`"); + + // Parse am command string and convert it to a list of arguments + List amCommandList = new ArrayList<>(); + error = parseAmCommand(amCommandString, amCommandList); + if (error != null) { + sendResultToClient(localSocketManager, clientSocket, 1, null, error.toString()); + return; + } + + String[] amCommandArray = amCommandList.toArray(new String[0]); + + Logger.logDebug(LOG_TAG, "am command received from peer " + clientSocket.getPeerCred().getMinimalString() + + "\n" + ExecutionCommand.getArgumentsLogString("am command", amCommandArray)); + + // Run am command and send its result to the client + StringBuilder stdout = new StringBuilder(); + StringBuilder stderr = new StringBuilder(); + error = runAmCommand(localSocketManager.getContext(), amCommandArray, stdout, stderr); + if (error != null) { + sendResultToClient(localSocketManager, clientSocket, 1, stdout.toString(), + !stderr.toString().isEmpty() ? stderr + "\n\n" + error : error.toString()); + } + + sendResultToClient(localSocketManager, clientSocket, 0, stdout.toString(), stderr.toString()); + } + + /** + * Send result to {@link LocalClientSocket} that requested the am command to be run. + * + * @param localSocketManager The {@link LocalSocketManager} instance for the local socket. + * @param clientSocket The {@link LocalClientSocket} to which the result is to be sent. + * @param exitCode The exit code value to send. + * @param stdout The stdout value to send. + * @param stderr The stderr value to send. + */ + public static void sendResultToClient(@NonNull LocalSocketManager localSocketManager, + @NonNull LocalClientSocket clientSocket, + int exitCode, + @Nullable String stdout, @Nullable String stderr) { + StringBuilder result = new StringBuilder(); + result.append(sanitizeExitCode(clientSocket, exitCode)); + result.append('\0'); + result.append(stdout != null ? stdout : ""); + result.append('\0'); + result.append(stderr != null ? stderr : ""); + + // Send result to client and close output stream + Error error = clientSocket.sendDataToOutputStream(result.toString(), true); + if (error != null) { + localSocketManager.onError(clientSocket, error); + } + } + + /** + * Sanitize exitCode to between 0-255, otherwise it may be considered invalid. + * Out of bound exit codes would return with exit code `44` `Channel number out of range` in shell. + * + * @param clientSocket The {@link LocalClientSocket} to which the exit code will be sent. + * @param exitCode The current exit code. + * @return Returns the sanitized exit code. + */ + public static int sanitizeExitCode(@NonNull LocalClientSocket clientSocket, int exitCode) { + if (exitCode < 0 || exitCode > 255) { + Logger.logWarn(LOG_TAG, "Ignoring invalid peer " + clientSocket.getPeerCred().getMinimalString() + " result value \"" + exitCode + "\" and force setting it to \"" + 1 + "\""); + exitCode = 1; + } + + return exitCode; + } + + + /** + * Parse amCommandString into a list of arguments like normally done on shells like bourne shell. + * Arguments are split on whitespaces unless quoted with single or double quotes. + * Double quotes and backslashes can be escaped with backslashes in arguments surrounded. + * Double quotes and backslashes can be escaped with backslashes in arguments surrounded with + * double quotes. + * + * @param amCommandString The am command {@link String}. + * @param amCommandList The {@link List} to set list of arguments in. + * @return Returns the {@code error} if parsing am command failed, otherwise {@code null}. + */ + public static Error parseAmCommand(String amCommandString, List amCommandList) { + + if (amCommandString == null || amCommandString.isEmpty()) { + return null; + } + + try { + amCommandList.addAll(ArgumentTokenizer.tokenize(amCommandString)); + } catch (Exception e) { + return AmSocketServerErrno.ERRNO_PARSE_AM_COMMAND_FAILED_WITH_EXCEPTION.getError(e, amCommandString, e.getMessage()); + } + + return null; + } + + /** + * Call termux-am-library to run the am command. + * + * @param context The {@link Context} to run am command with. + * @param amCommandArray The am command array. + * @param stdout The {@link StringBuilder} to set stdout in that is returned by the am command. + * @param stderr The {@link StringBuilder} to set stderr in that is returned by the am command. + * @return Returns the {@code error} if am command failed, otherwise {@code null}. + */ + public static Error runAmCommand(@NonNull Context context, + String[] amCommandArray, + @NonNull StringBuilder stdout, @NonNull StringBuilder stderr) { + try (ByteArrayOutputStream stdoutByteStream = new ByteArrayOutputStream(); + PrintStream stdoutPrintStream = new PrintStream(stdoutByteStream); + ByteArrayOutputStream stderrByteStream = new ByteArrayOutputStream(); + PrintStream stderrPrintStream = new PrintStream(stderrByteStream)) { + + new Am(stdoutPrintStream, stderrPrintStream, (Application) context.getApplicationContext()).run(amCommandArray); + + // Set stdout to value set by am command in stdoutPrintStream + stdoutPrintStream.flush(); + stdout.append(stdoutByteStream.toString(StandardCharsets.UTF_8.name())); + + // Set stderr to value set by am command in stderrPrintStream + stderrPrintStream.flush(); + stderr.append(stderrByteStream.toString(StandardCharsets.UTF_8.name())); + } catch (Exception e) { + return AmSocketServerErrno.ERRNO_RUN_AM_COMMAND_FAILED_WITH_EXCEPTION.getError(e, Arrays.toString(amCommandArray), e.getMessage()); + } + + return null; + } + + + + + + /** Implementation for {@link ILocalSocketManager} for {@link AmSocketServer}. */ + public abstract static class AmSocketServerClient extends LocalSocketManagerClientBase { + + @Override + public void onClientAccepted(@NonNull LocalSocketManager localSocketManager, + @NonNull LocalClientSocket clientSocket) { + AmSocketServer.processAmClient(localSocketManager, clientSocket); + super.onClientAccepted(localSocketManager, clientSocket); + } + + } + +} diff --git a/termux-shared/src/main/java/com/termux/shared/shell/am/AmSocketServerErrno.java b/termux-shared/src/main/java/com/termux/shared/shell/am/AmSocketServerErrno.java new file mode 100644 index 00000000..74b26528 --- /dev/null +++ b/termux-shared/src/main/java/com/termux/shared/shell/am/AmSocketServerErrno.java @@ -0,0 +1,18 @@ +package com.termux.shared.shell.am; + +import com.termux.shared.errors.Errno; + +public class AmSocketServerErrno extends Errno { + + public static final String TYPE = "AmSocketServer Error"; + + + /** Errors for {@link AmSocketServer} (100-150) */ + public static final Errno ERRNO_PARSE_AM_COMMAND_FAILED_WITH_EXCEPTION = new Errno(TYPE, 100, "Parse am command `%1$s` failed.\nException: %2$s"); + public static final Errno ERRNO_RUN_AM_COMMAND_FAILED_WITH_EXCEPTION = new Errno(TYPE, 101, "Run am command `%1$s` failed.\nException: %2$s"); + + AmSocketServerErrno(final String type, final int code, final String message) { + super(type, code, message); + } + +} diff --git a/termux-shared/src/main/java/com/termux/shared/termux/TermuxConstants.java b/termux-shared/src/main/java/com/termux/shared/termux/TermuxConstants.java index d400e693..3a33eb52 100644 --- a/termux-shared/src/main/java/com/termux/shared/termux/TermuxConstants.java +++ b/termux-shared/src/main/java/com/termux/shared/termux/TermuxConstants.java @@ -11,7 +11,7 @@ import java.util.Formatter; import java.util.List; /* - * Version: v0.40.0 + * Version: v0.41.0 * SPDX-License-Identifier: MIT * * Changelog @@ -236,6 +236,9 @@ import java.util.List; * * - 0.40.0 (2022-04-17) * - Added `TERMUX_APPS_DIR_PATH` and `TERMUX_APP.APPS_DIR_PATH`. + * + * - 0.41.0 (2022-04-17) + * - Added `TERMUX_APP.TERMUX_AM_SOCKET_FILE_PATH`. */ /** @@ -883,6 +886,9 @@ public final class TermuxConstants { /** Termux apps directory path */ public static final String APPS_DIR_PATH = TERMUX_APPS_DIR_PATH + "/termux-app"; // Default: "/data/data/com.termux/files/apps/termux-app" + /** termux-am socket file path */ + public static final String TERMUX_AM_SOCKET_FILE_PATH = APPS_DIR_PATH + "/termux-am/am.sock"; // Default: "/data/data/com.termux/files/apps/termux-app/termux-am/am.sock" + /** Termux app core activity name. */ public static final String TERMUX_ACTIVITY_NAME = TERMUX_PACKAGE_NAME + ".app.TermuxActivity"; // Default: "com.termux.app.TermuxActivity" diff --git a/termux-shared/src/main/java/com/termux/shared/termux/shell/TermuxShellUtils.java b/termux-shared/src/main/java/com/termux/shared/termux/shell/TermuxShellUtils.java index f3771f43..8ac6d258 100644 --- a/termux-shared/src/main/java/com/termux/shared/termux/shell/TermuxShellUtils.java +++ b/termux-shared/src/main/java/com/termux/shared/termux/shell/TermuxShellUtils.java @@ -28,9 +28,11 @@ public class TermuxShellUtils { public static String TERMUX_IS_DEBUGGABLE_BUILD; public static String TERMUX_APP_PID; public static String TERMUX_APK_RELEASE; + public static Boolean TERMUX_APP_AM_SOCKET_SERVER_ENABLED; public static String TERMUX_API_VERSION_NAME; + private static final String LOG_TAG = "TermuxShellUtils"; public static String getDefaultWorkingDirectoryPath() { @@ -59,6 +61,8 @@ public class TermuxShellUtils { environment.add("TERMUX_APP_PID=" + TERMUX_APP_PID); if (TERMUX_APK_RELEASE != null) environment.add("TERMUX_APK_RELEASE=" + TERMUX_APK_RELEASE); + if (TERMUX_APP_AM_SOCKET_SERVER_ENABLED != null) + environment.add("TERMUX_APP_AM_SOCKET_SERVER_ENABLED=" + TERMUX_APP_AM_SOCKET_SERVER_ENABLED); if (TERMUX_API_VERSION_NAME != null) environment.add("TERMUX_API_VERSION=" + TERMUX_API_VERSION_NAME); diff --git a/termux-shared/src/main/java/com/termux/shared/termux/shell/am/TermuxAmSocketServer.java b/termux-shared/src/main/java/com/termux/shared/termux/shell/am/TermuxAmSocketServer.java new file mode 100644 index 00000000..32116b30 --- /dev/null +++ b/termux-shared/src/main/java/com/termux/shared/termux/shell/am/TermuxAmSocketServer.java @@ -0,0 +1,210 @@ +package com.termux.shared.termux.shell.am; + +import android.content.Context; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.termux.shared.errors.Error; +import com.termux.shared.logger.Logger; +import com.termux.shared.net.socket.local.LocalClientSocket; +import com.termux.shared.net.socket.local.LocalServerSocket; +import com.termux.shared.net.socket.local.LocalSocketManager; +import com.termux.shared.net.socket.local.LocalSocketManagerClientBase; +import com.termux.shared.net.socket.local.LocalSocketRunConfig; +import com.termux.shared.shell.am.AmSocketServer; +import com.termux.shared.termux.TermuxConstants; +import com.termux.shared.termux.crash.TermuxCrashUtils; +import com.termux.shared.termux.plugins.TermuxPluginUtils; +import com.termux.shared.termux.settings.properties.TermuxAppSharedProperties; +import com.termux.shared.termux.settings.properties.TermuxPropertyConstants; +import com.termux.shared.termux.shell.TermuxShellUtils; + +/** + * A wrapper for {@link AmSocketServer} for termux-app usage. + * + * The static {@link #termuxAmSocketServer} variable stores the {@link LocalSocketManager} for the + * {@link AmSocketServer}. + * + * The {@link TermuxAmSocketServerClient} extends the {@link AmSocketServer.AmSocketServerClient} + * class to also show plugin error notifications for errors and disallowed client connections in + * addition to logging the messages to logcat, which are only logged by {@link LocalSocketManagerClientBase} + * if log level is debug or higher for privacy issues. + * + * It uses a filesystem socket server with the socket file at + * {@link TermuxConstants.TERMUX_APP#TERMUX_AM_SOCKET_FILE_PATH}. It would normally only allow + * processes belonging to the termux user and root user to connect to it. If commands are sent by the + * root user, then the am commands executed will be run as the termux user and its permissions, + * capabilities and selinux context instead of root. + * + * The `$PREFIX/bin/termux-am` client connects to the server via `$PREFIX/bin/termux-am-socket` to + * run the am commands. It provides similar functionality to "$PREFIX/bin/am" + * (and "/system/bin/am"), but should be faster since it does not require starting a dalvik vm for + * every command as done by "am" via termux/TermuxAm. + * + * The server is started by termux-app Application class but is not started if + * {@link TermuxPropertyConstants#KEY_RUN_TERMUX_AM_SOCKET_SERVER} is `false` which can be done by + * adding the prop with value "false" to the "~/.termux/termux.properties" file. Changes + * require termux-app to be force stopped and restarted. + * + * The current state of the server can be checked with the + * {@link TermuxShellUtils#TERMUX_APP_AM_SOCKET_SERVER_ENABLED} env variable, which is exported + * for all shell sessions and tasks. + * + * https://github.com/termux/termux-am-socket + * https://github.com/termux/TermuxAm + */ +public class TermuxAmSocketServer { + + public static final String LOG_TAG = "TermuxAmSocketServer"; + + public static final String TITLE = "TermuxAm"; + + /** The static instance for the {@link TermuxAmSocketServer} {@link LocalSocketManager}. */ + private static LocalSocketManager termuxAmSocketServer; + + /** + * Setup the {@link AmSocketServer} {@link LocalServerSocket} and start listening for + * new {@link LocalClientSocket} if enabled. + * + * @param context The {@link Context} for {@link LocalSocketManager}. + */ + public static void setupTermuxAmSocketServer(@NonNull Context context) { + // Start termux-am-socket server if enabled by user + boolean enabled = false; + if (TermuxAppSharedProperties.getProperties().shouldRunTermuxAmSocketServer()) { + Logger.logDebug(LOG_TAG, "Starting " + TITLE + " socket server since its enabled"); + start(context); + if (termuxAmSocketServer != null && termuxAmSocketServer.isRunning()) { + enabled = true; + Logger.logDebug(LOG_TAG, TITLE + " socket server successfully started"); + } + } else { + Logger.logDebug(LOG_TAG, "Not starting " + TITLE + " socket server since its not enabled"); + } + + // Once termux-app has started, the server state must not be changed since the variable is + // exported in shell sessions and tasks and if state is changed, then env of older shells will + // retain invalid value. User should force stop the app to update state after changing prop. + TermuxShellUtils.TERMUX_APP_AM_SOCKET_SERVER_ENABLED = enabled; + } + + /** + * Create the {@link AmSocketServer} {@link LocalServerSocket} and start listening for new {@link LocalClientSocket}. + */ + public static synchronized void start(@NonNull Context context) { + stop(); + + LocalSocketRunConfig localSocketRunConfig = new LocalSocketRunConfig(TITLE, + TermuxConstants.TERMUX_APP.TERMUX_AM_SOCKET_FILE_PATH, new TermuxAmSocketServerClient()); + + termuxAmSocketServer = AmSocketServer.start(context, localSocketRunConfig); + } + + /** + * Stop the {@link AmSocketServer} {@link LocalServerSocket} and stop listening for new {@link LocalClientSocket}. + */ + public static synchronized void stop() { + if (termuxAmSocketServer != null) { + Error error = termuxAmSocketServer.stop(); + if (error != null) { + termuxAmSocketServer.onError(error); + } + termuxAmSocketServer = null; + } + } + + /** + * Update the state of the {@link AmSocketServer} {@link LocalServerSocket} depending on current + * value of {@link TermuxPropertyConstants#KEY_RUN_TERMUX_AM_SOCKET_SERVER}. + */ + public static synchronized void updateState(@NonNull Context context) { + TermuxAppSharedProperties properties = TermuxAppSharedProperties.getProperties(); + if (properties.shouldRunTermuxAmSocketServer()) { + if (termuxAmSocketServer == null) { + Logger.logDebug(LOG_TAG, "updateState: Starting " + TITLE + " socket server"); + start(context); + } + } else { + if (termuxAmSocketServer != null) { + Logger.logDebug(LOG_TAG, "updateState: Disabling " + TITLE + " socket server"); + stop(); + } + } + } + + /** + * Get {@link #termuxAmSocketServer}. + */ + public static synchronized LocalSocketManager getTermuxAmSocketServer() { + return termuxAmSocketServer; + } + + /** + * Show an error notification on the {@link TermuxConstants#TERMUX_PLUGIN_COMMAND_ERRORS_NOTIFICATION_CHANNEL_ID} + * {@link TermuxConstants#TERMUX_PLUGIN_COMMAND_ERRORS_NOTIFICATION_CHANNEL_NAME} with a call + * to {@link TermuxPluginUtils#sendPluginCommandErrorNotification(Context, String, CharSequence, String, String)}. + * + * @param context The {@link Context} to send the notification with. + * @param error The {@link Error} generated. + * @param localSocketRunConfig The {@link LocalSocketRunConfig} for {@link LocalSocketManager}. + * @param clientSocket The optional {@link LocalClientSocket} for which the error was generated. + */ + public static synchronized void showErrorNotification(@NonNull Context context, @NonNull Error error, + @NonNull LocalSocketRunConfig localSocketRunConfig, + @Nullable LocalClientSocket clientSocket) { + TermuxPluginUtils.sendPluginCommandErrorNotification(context, LOG_TAG, + localSocketRunConfig.getTitle() + " Socket Server Error", error.getMinimalErrorString(), + LocalSocketManager.getErrorMarkdownString(error, localSocketRunConfig, clientSocket)); + } + + + + + + /** Enhanced implementation for {@link AmSocketServer.AmSocketServerClient} for {@link TermuxAmSocketServer}. */ + public static class TermuxAmSocketServerClient extends AmSocketServer.AmSocketServerClient { + + public static final String LOG_TAG = "TermuxAmSocketServerClient"; + + @Nullable + @Override + public Thread.UncaughtExceptionHandler getLocalSocketManagerClientThreadUEH( + @NonNull LocalSocketManager localSocketManager) { + // Use termux crash handler for socket listener thread just like used for main app process thread. + return TermuxCrashUtils.getCrashHandler(localSocketManager.getContext()); + } + + @Override + public void onError(@NonNull LocalSocketManager localSocketManager, + @Nullable LocalClientSocket clientSocket, @NonNull Error error) { + // Don't show notification if server is not running since errors may be triggered + // when server is stopped and server and client sockets are closed. + if (localSocketManager.isRunning()) { + TermuxAmSocketServer.showErrorNotification(localSocketManager.getContext(), error, + localSocketManager.getLocalSocketRunConfig(), clientSocket); + } + + // But log the exception + super.onError(localSocketManager, clientSocket, error); + } + + @Override + public void onDisallowedClientConnected(@NonNull LocalSocketManager localSocketManager, + @NonNull LocalClientSocket clientSocket, @NonNull Error error) { + // Always show notification and log error regardless of if server is running or not + TermuxAmSocketServer.showErrorNotification(localSocketManager.getContext(), error, + localSocketManager.getLocalSocketRunConfig(), clientSocket); + super.onDisallowedClientConnected(localSocketManager, clientSocket, error); + } + + + + @Override + protected String getLogTag() { + return LOG_TAG; + } + + } + +}