diff --git a/app/src/main/java/com/termux/app/RunCommandService.java b/app/src/main/java/com/termux/app/RunCommandService.java
index 4862a476a8..261729155c 100644
--- a/app/src/main/java/com/termux/app/RunCommandService.java
+++ b/app/src/main/java/com/termux/app/RunCommandService.java
@@ -12,6 +12,7 @@
import com.termux.R;
import com.termux.shared.data.DataUtils;
import com.termux.shared.data.IntentUtils;
+import com.termux.shared.termux.plugins.TermuxPluginUtils;
import com.termux.shared.termux.file.TermuxFileUtils;
import com.termux.shared.file.filesystem.FileType;
import com.termux.shared.errors.Errno;
@@ -22,7 +23,6 @@
import com.termux.shared.file.FileUtils;
import com.termux.shared.logger.Logger;
import com.termux.shared.notification.NotificationUtils;
-import com.termux.app.utils.PluginUtils;
import com.termux.shared.shell.command.ExecutionCommand;
import com.termux.shared.shell.command.ExecutionCommand.Runner;
@@ -75,7 +75,7 @@ public int onStartCommand(Intent intent, int flags, int startId) {
if (!RUN_COMMAND_SERVICE.ACTION_RUN_COMMAND.equals(intent.getAction())) {
errmsg = this.getString(R.string.error_run_command_service_invalid_intent_action, intent.getAction());
executionCommand.setStateFailed(Errno.ERRNO_FAILED.getCode(), errmsg);
- PluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand, false);
+ TermuxPluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand, false);
return stopService();
}
@@ -110,7 +110,7 @@ public int onStartCommand(Intent intent, int flags, int startId) {
if (Runner.runnerOf(executionCommand.runner) == null) {
errmsg = this.getString(R.string.error_run_command_service_invalid_execution_command_runner, executionCommand.runner);
executionCommand.setStateFailed(Errno.ERRNO_FAILED.getCode(), errmsg);
- PluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand, false);
+ TermuxPluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand, false);
return stopService();
}
@@ -137,10 +137,10 @@ public int onStartCommand(Intent intent, int flags, int startId) {
// user knows someone tried to run a command in termux context, since it may be malicious
// app or imported (tasker) plugin project and not the user himself. If a pending intent is
// also sent, then its creator is also logged and shown.
- errmsg = PluginUtils.checkIfAllowExternalAppsPolicyIsViolated(this, LOG_TAG);
+ errmsg = TermuxPluginUtils.checkIfAllowExternalAppsPolicyIsViolated(this, LOG_TAG);
if (errmsg != null) {
executionCommand.setStateFailed(Errno.ERRNO_FAILED.getCode(), errmsg);
- PluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand, true);
+ TermuxPluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand, true);
return stopService();
}
@@ -150,7 +150,7 @@ public int onStartCommand(Intent intent, int flags, int startId) {
if (executionCommand.executable == null || executionCommand.executable.isEmpty()) {
errmsg = this.getString(R.string.error_run_command_service_mandatory_extra_missing, RUN_COMMAND_SERVICE.EXTRA_COMMAND_PATH);
executionCommand.setStateFailed(Errno.ERRNO_FAILED.getCode(), errmsg);
- PluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand, false);
+ TermuxPluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand, false);
return stopService();
}
@@ -164,7 +164,7 @@ public int onStartCommand(Intent intent, int flags, int startId) {
false);
if (error != null) {
executionCommand.setStateFailed(error);
- PluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand, false);
+ TermuxPluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand, false);
return stopService();
}
@@ -185,7 +185,7 @@ public int onStartCommand(Intent intent, int flags, int startId) {
false, true);
if (error != null) {
executionCommand.setStateFailed(error);
- PluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand, false);
+ TermuxPluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand, false);
return stopService();
}
}
diff --git a/app/src/main/java/com/termux/app/TermuxApplication.java b/app/src/main/java/com/termux/app/TermuxApplication.java
index 3d438f71d0..78a4bf520b 100644
--- a/app/src/main/java/com/termux/app/TermuxApplication.java
+++ b/app/src/main/java/com/termux/app/TermuxApplication.java
@@ -3,23 +3,27 @@
import android.app.Application;
import android.content.Context;
+import com.termux.shared.errors.Error;
+import com.termux.shared.logger.Logger;
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.logger.Logger;
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 {
+ private static final String LOG_TAG = "TermuxApplication";
+
public void onCreate() {
super.onCreate();
Context context = getApplicationContext();
// Set crash handler for the app
- TermuxCrashUtils.setCrashHandler(this);
+ TermuxCrashUtils.setDefaultCrashHandler(this);
// Set log config for the app
setLogConfig(context);
@@ -31,6 +35,23 @@ public void onCreate() {
// Set NightMode.APP_NIGHT_MODE
TermuxThemeUtils.setAppNightMode(properties.getNightMode());
+
+ // Check and create termux files directory. If failed to access it like in case of secondary
+ // user or external sd card installation, then don't run files directory related code
+ Error error = TermuxFileUtils.isTermuxFilesDirectoryAccessible(this, true, true);
+ if (error != null) {
+ Logger.logErrorExtended(LOG_TAG, "Termux files directory is not accessible\n" + error);
+ } else {
+ Logger.logInfo(LOG_TAG, "Termux files directory is accessible");
+
+ error = TermuxFileUtils.isAppsTermuxAppDirectoryAccessible(true, true);
+ if (error != null) {
+ Logger.logErrorExtended(LOG_TAG, "Create apps/termux-app directory failed\n" + error);
+ return;
+ }
+
+ TermuxAmSocketServer.setupTermuxAmSocketServer(context);
+ }
}
public static void setLogConfig(Context context) {
@@ -43,4 +64,3 @@ public static void setLogConfig(Context context) {
}
}
-
diff --git a/app/src/main/java/com/termux/app/TermuxOpenReceiver.java b/app/src/main/java/com/termux/app/TermuxOpenReceiver.java
index 48f0607dc4..f8f21b5d32 100644
--- a/app/src/main/java/com/termux/app/TermuxOpenReceiver.java
+++ b/app/src/main/java/com/termux/app/TermuxOpenReceiver.java
@@ -13,7 +13,7 @@
import android.provider.MediaStore;
import android.webkit.MimeTypeMap;
-import com.termux.app.utils.PluginUtils;
+import com.termux.shared.termux.plugins.TermuxPluginUtils;
import com.termux.shared.data.DataUtils;
import com.termux.shared.data.IntentUtils;
import com.termux.shared.net.uri.UriUtils;
@@ -204,7 +204,7 @@ public ParcelFileDescriptor openFile(@NonNull Uri uri, @NonNull String mode) thr
}
// If TermuxConstants.PROP_ALLOW_EXTERNAL_APPS property to not set to "true", then throw exception
- String errmsg = PluginUtils.checkIfAllowExternalAppsPolicyIsViolated(getContext(), LOG_TAG);
+ String errmsg = TermuxPluginUtils.checkIfAllowExternalAppsPolicyIsViolated(getContext(), LOG_TAG);
if (errmsg != null) {
throw new IllegalArgumentException(errmsg);
}
diff --git a/app/src/main/java/com/termux/app/TermuxService.java b/app/src/main/java/com/termux/app/TermuxService.java
index 52f0c6cd69..59005c0a9e 100644
--- a/app/src/main/java/com/termux/app/TermuxService.java
+++ b/app/src/main/java/com/termux/app/TermuxService.java
@@ -20,7 +20,7 @@
import com.termux.R;
import com.termux.app.terminal.TermuxTerminalSessionClient;
-import com.termux.app.utils.PluginUtils;
+import com.termux.shared.termux.plugins.TermuxPluginUtils;
import com.termux.shared.data.IntentUtils;
import com.termux.shared.net.uri.UriUtils;
import com.termux.shared.errors.Errno;
@@ -286,7 +286,7 @@ private synchronized void killAllTermuxExecutionCommands() {
ExecutionCommand executionCommand = pendingPluginExecutionCommands.get(i);
if (!executionCommand.shouldNotProcessResults() && executionCommand.isPluginExecutionCommandWithPendingResult()) {
if (executionCommand.setStateFailed(Errno.ERRNO_CANCELLED.getCode(), this.getString(com.termux.shared.R.string.error_execution_cancelled))) {
- PluginUtils.processPluginExecutionCommandResult(this, LOG_TAG, executionCommand);
+ TermuxPluginUtils.processPluginExecutionCommandResult(this, LOG_TAG, executionCommand);
}
}
}
@@ -367,7 +367,7 @@ private void actionServiceExecute(Intent intent) {
if (Runner.runnerOf(executionCommand.runner) == null) {
String errmsg = this.getString(R.string.error_termux_service_invalid_execution_command_runner, executionCommand.runner);
executionCommand.setStateFailed(Errno.ERRNO_FAILED.getCode(), errmsg);
- PluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand, false);
+ TermuxPluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand, false);
return;
}
@@ -411,7 +411,7 @@ else if (Runner.TERMINAL_SESSION.equalsRunner(executionCommand.runner))
else {
String errmsg = getString(R.string.error_termux_service_unsupported_execution_command_runner, executionCommand.runner);
executionCommand.setStateFailed(Errno.ERRNO_FAILED.getCode(), errmsg);
- PluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand, false);
+ TermuxPluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand, false);
}
}
@@ -454,9 +454,11 @@ public synchronized AppShell createTermuxTask(ExecutionCommand executionCommand)
Logger.logError(LOG_TAG, "Failed to execute new TermuxTask command for:\n" + executionCommand.getCommandIdAndLabelLogString());
// If the execution command was started for a plugin, then process the error
if (executionCommand.isPluginExecutionCommand)
- PluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand, false);
- else
- Logger.logErrorExtended(LOG_TAG, executionCommand.toString());
+ TermuxPluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand, false);
+ else {
+ Logger.logError(LOG_TAG, "Set log level to debug or higher to see error in logs");
+ Logger.logErrorPrivateExtended(LOG_TAG, executionCommand.toString());
+ }
return null;
}
@@ -483,7 +485,7 @@ public void onAppShellExited(final AppShell termuxTask) {
// If the execution command was started for a plugin, then process the results
if (executionCommand != null && executionCommand.isPluginExecutionCommand)
- PluginUtils.processPluginExecutionCommandResult(this, LOG_TAG, executionCommand);
+ TermuxPluginUtils.processPluginExecutionCommandResult(this, LOG_TAG, executionCommand);
mTermuxTasks.remove(termuxTask);
}
@@ -517,7 +519,7 @@ else if (SessionCreateMode.NO_SESSION_WITH_NAME.equalsMode(executionCommand.sess
if (DataUtils.isNullOrEmpty(executionCommand.sessionName)) {
String errmsg = getString(R.string.error_termux_service_execution_command_session_name_unset, executionCommand.sessionCreateMode);
executionCommand.setStateFailed(Errno.ERRNO_FAILED.getCode(), errmsg);
- PluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand, false);
+ TermuxPluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand, false);
return;
} else {
newTermuxSession = getTermuxSessionForName(executionCommand.sessionName);
@@ -527,7 +529,7 @@ else if (SessionCreateMode.NO_SESSION_WITH_NAME.equalsMode(executionCommand.sess
else {
String errmsg = getString(R.string.error_termux_service_unsupported_execution_command_session_create_mode, executionCommand.sessionCreateMode);
executionCommand.setStateFailed(Errno.ERRNO_FAILED.getCode(), errmsg);
- PluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand, false);
+ TermuxPluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand, false);
return;
}
@@ -575,9 +577,11 @@ public synchronized TermuxSession createTermuxSession(ExecutionCommand execution
Logger.logError(LOG_TAG, "Failed to execute new TermuxSession command for:\n" + executionCommand.getCommandIdAndLabelLogString());
// If the execution command was started for a plugin, then process the error
if (executionCommand.isPluginExecutionCommand)
- PluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand, false);
- else
- Logger.logErrorExtended(LOG_TAG, executionCommand.toString());
+ TermuxPluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand, false);
+ else {
+ Logger.logError(LOG_TAG, "Set log level to debug or higher to see error in logs");
+ Logger.logErrorPrivateExtended(LOG_TAG, executionCommand.toString());
+ }
return null;
}
@@ -621,7 +625,7 @@ public void onTermuxSessionExited(final TermuxSession termuxSession) {
// If the execution command was started for a plugin, then process the results
if (executionCommand != null && executionCommand.isPluginExecutionCommand)
- PluginUtils.processPluginExecutionCommandResult(this, LOG_TAG, executionCommand);
+ TermuxPluginUtils.processPluginExecutionCommandResult(this, LOG_TAG, executionCommand);
mTermuxSessions.remove(termuxSession);
diff --git a/app/src/main/java/com/termux/app/models/UserAction.java b/app/src/main/java/com/termux/app/models/UserAction.java
index d32005f2a7..1e82255e99 100644
--- a/app/src/main/java/com/termux/app/models/UserAction.java
+++ b/app/src/main/java/com/termux/app/models/UserAction.java
@@ -3,7 +3,6 @@
public enum UserAction {
ABOUT("about"),
- PLUGIN_EXECUTION_COMMAND("plugin execution command"),
REPORT_ISSUE_FROM_TRANSCRIPT("report issue from transcript");
private final String name;
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index e49b4e8585..acc0f44b0d 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -121,8 +121,6 @@
- %1$s requires `allow-external-apps`
- property to be set to `true` in `%2$s` file.
Failed to start TermuxService. Check logcat for exception message.
Failed to start TermuxService while app is in background due to android bg restrictions.
diff --git a/termux-shared/build.gradle b/termux-shared/build.gradle
index 4883864e6f..0f3f6cf99a 100644
--- a/termux-shared/build.gradle
+++ b/termux-shared/build.gradle
@@ -26,12 +26,19 @@ android {
implementation "commons-io:commons-io:2.5"
implementation project(":terminal-view")
+
+ implementation 'com.github.termux:termux-am-library:1.0'
}
defaultConfig {
minSdkVersion project.properties.minSdkVersion.toInteger()
targetSdkVersion project.properties.targetSdkVersion.toInteger()
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+ externalNativeBuild {
+ ndkBuild {
+ cppFlags ''
+ }
+ }
}
buildTypes {
@@ -45,6 +52,11 @@ android {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
+ externalNativeBuild {
+ ndkBuild {
+ path file('src/main/cpp/Android.mk')
+ }
+ }
}
dependencies {
diff --git a/termux-shared/src/main/cpp/Android.mk b/termux-shared/src/main/cpp/Android.mk
new file mode 100644
index 0000000000..abc213c379
--- /dev/null
+++ b/termux-shared/src/main/cpp/Android.mk
@@ -0,0 +1,6 @@
+LOCAL_PATH:= $(call my-dir)
+include $(CLEAR_VARS)
+LOCAL_LDLIBS := -llog
+LOCAL_MODULE := local-socket
+LOCAL_SRC_FILES := local-socket.cpp
+include $(BUILD_SHARED_LIBRARY)
diff --git a/termux-shared/src/main/cpp/Application.mk b/termux-shared/src/main/cpp/Application.mk
new file mode 100644
index 0000000000..ce095350e6
--- /dev/null
+++ b/termux-shared/src/main/cpp/Application.mk
@@ -0,0 +1 @@
+APP_STL := c++_static
diff --git a/termux-shared/src/main/cpp/local-socket.cpp b/termux-shared/src/main/cpp/local-socket.cpp
new file mode 100644
index 0000000000..6457aca8af
--- /dev/null
+++ b/termux-shared/src/main/cpp/local-socket.cpp
@@ -0,0 +1,603 @@
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include
+
+#include
+#include
+#include
+#include
+
+#define LOG_TAG "local-socket"
+#define JNI_EXCEPTION "jni-exception"
+
+using namespace std;
+
+
+/* Convert a jstring to a std:string. */
+string jstring_to_stdstr(JNIEnv *env, jstring jString) {
+ jclass stringClass = env->FindClass("java/lang/String");
+ jmethodID getBytes = env->GetMethodID(stringClass, "getBytes", "()[B");
+ jbyteArray jStringBytesArray = (jbyteArray) env->CallObjectMethod(jString, getBytes);
+ jsize length = env->GetArrayLength(jStringBytesArray);
+ jbyte* jStringBytes = env->GetByteArrayElements(jStringBytesArray, nullptr);
+ std::string stdString((char *)jStringBytes, length);
+ env->ReleaseByteArrayElements(jStringBytesArray, jStringBytes, JNI_ABORT);
+ return stdString;
+}
+
+/* Get characters before first occurrence of the delim in a std:string. */
+string get_string_till_first_delim(string str, char delim) {
+ if (!str.empty()) {
+ stringstream cmdline_args(str);
+ string tmp;
+ if (getline(cmdline_args, tmp, delim))
+ return tmp;
+ }
+ return "";
+}
+
+/* Replace `\0` values with spaces in a std:string. */
+string replace_null_with_space(string str) {
+ if (str.empty())
+ return "";
+
+ stringstream tokens(str);
+ string tmp;
+ string str_spaced;
+ while (getline(tokens, tmp, '\0')){
+ str_spaced.append(" " + tmp);
+ }
+
+ if (!str_spaced.empty()) {
+ if (str_spaced.front() == ' ')
+ str_spaced.erase(0, 1);
+ }
+
+ return str_spaced;
+}
+
+/* Get class name of a jclazz object with a call to `Class.getName()`. */
+string get_class_name(JNIEnv *env, jclass clazz) {
+ jclass classClass = env->FindClass("java/lang/Class");
+ jmethodID getName = env->GetMethodID(classClass, "getName", "()Ljava/lang/String;");
+ jstring className = (jstring) env->CallObjectMethod(clazz, getName);
+ return jstring_to_stdstr(env, className);
+}
+
+
+
+/*
+ * Get /proc/[pid]/cmdline for a process with pid.
+ *
+ * https://manpages.debian.org/testing/manpages/proc.5.en.html
+ */
+string get_process_cmdline(const pid_t pid) {
+ string cmdline;
+ char buf[BUFSIZ];
+ size_t len;
+ char procfile[BUFSIZ];
+ sprintf(procfile, "/proc/%d/cmdline", pid);
+ FILE *fp = fopen(procfile, "rb");
+ if (fp) {
+ while ((len = fread(buf, 1, sizeof(buf), fp)) > 0) {
+ cmdline.append(buf, len);
+ }
+ fclose(fp);
+ }
+
+ return cmdline;
+}
+
+/* Extract process name from /proc/[pid]/cmdline value of a process. */
+string get_process_name_from_cmdline(string cmdline) {
+ return get_string_till_first_delim(cmdline, '\0');
+}
+
+/* Replace `\0` values with spaces in /proc/[pid]/cmdline value of a process. */
+string get_process_cmdline_spaced(string cmdline) {
+ return replace_null_with_space(cmdline);
+}
+
+
+/* Send an ERROR log message to android logcat. */
+void log_error(string message) {
+ __android_log_write(ANDROID_LOG_ERROR, LOG_TAG, message.c_str());
+}
+
+/* Send an WARN log message to android logcat. */
+void log_warn(string message) {
+ __android_log_write(ANDROID_LOG_WARN, LOG_TAG, message.c_str());
+}
+
+/* Get "title: message" formatted string. */
+string get_title_and_message(JNIEnv *env, jstring title, string message) {
+ if (title)
+ message = jstring_to_stdstr(env, title) + ": " + message;
+ return message;
+}
+
+
+/* Convert timespec to milliseconds. */
+int64_t timespec_to_milliseconds(const struct timespec* const time) {
+ return (((int64_t)time->tv_sec) * 1000) + (((int64_t)time->tv_nsec)/1000000);
+}
+
+/* Convert milliseconds to timeval. */
+timeval milliseconds_to_timeval(int milliseconds) {
+ struct timeval tv = {};
+ tv.tv_sec = milliseconds / 1000;
+ tv.tv_usec = (milliseconds % 1000) * 1000;
+ return tv;
+}
+
+
+// Note: Exceptions thrown from JNI must be caught with Throwable class instead of Exception,
+// otherwise exception will be sent to UncaughtExceptionHandler of the thread.
+// Android studio complains that getJniResult functions always return nullptr since linter is broken
+// for jboolean and jobject if comparisons.
+bool checkJniException(JNIEnv *env) {
+ if (env->ExceptionCheck()) {
+ jthrowable throwable = env->ExceptionOccurred();
+ if (throwable != NULL) {
+ env->ExceptionClear();
+ env->Throw(throwable);
+ return true;
+ }
+ }
+
+ return false;
+}
+
+string getJniResultString(const int retvalParam, const int errnoParam,
+ string errmsgParam, const int intDataParam) {
+ return "retval=" + to_string(retvalParam) + ", errno=" + to_string(errnoParam) +
+ ", errmsg=\"" + errmsgParam + "\"" + ", intData=" + to_string(intDataParam);
+}
+
+/* Get "com/termux/shared/jni/models/JniResult" object that can be returned as result for a JNI call. */
+jobject getJniResult(JNIEnv *env, jstring title, const int retvalParam, const int errnoParam,
+ string errmsgParam, const int intDataParam) {
+ jclass clazz = env->FindClass("com/termux/shared/jni/models/JniResult");
+ if (checkJniException(env)) return NULL;
+ if (!clazz) {
+ log_error(get_title_and_message(env, title,
+ "Failed to find JniResult class to create object for " +
+ getJniResultString(retvalParam, errnoParam, errmsgParam, intDataParam)));
+ return NULL;
+ }
+
+ jmethodID constructor = env->GetMethodID(clazz, "", "(IILjava/lang/String;I)V");
+ if (checkJniException(env)) return NULL;
+ if (!constructor) {
+ log_error(get_title_and_message(env, title,
+ "Failed to get constructor for JniResult class to create object for " +
+ getJniResultString(retvalParam, errnoParam, errmsgParam, intDataParam)));
+ return NULL;
+ }
+
+ if (!errmsgParam.empty())
+ errmsgParam = get_title_and_message(env, title, string(errmsgParam));
+
+ jobject obj = env->NewObject(clazz, constructor, retvalParam, errnoParam, env->NewStringUTF(errmsgParam.c_str()), intDataParam);
+ if (checkJniException(env)) return NULL;
+ if (obj == NULL) {
+ log_error(get_title_and_message(env, title,
+ "Failed to get JniResult object for " +
+ getJniResultString(retvalParam, errnoParam, errmsgParam, intDataParam)));
+ return NULL;
+ }
+
+ return obj;
+}
+
+
+jobject getJniResult(JNIEnv *env, jstring title, const int retvalParam, const int errnoParam) {
+ return getJniResult(env, title, retvalParam, errnoParam, strerror(errnoParam), 0);
+}
+
+jobject getJniResult(JNIEnv *env, jstring title, const int retvalParam, string errmsgPrefixParam) {
+ return getJniResult(env, title, retvalParam, 0, errmsgPrefixParam, 0);
+}
+
+jobject getJniResult(JNIEnv *env, jstring title, const int retvalParam, const int errnoParam, string errmsgPrefixParam) {
+ return getJniResult(env, title, retvalParam, errnoParam, errmsgPrefixParam + ": " + string(strerror(errnoParam)), 0);
+}
+
+jobject getJniResult(JNIEnv *env, jstring title, const int intDataParam) {
+ return getJniResult(env, title, 0, 0, "", intDataParam);
+}
+
+jobject getJniResult(JNIEnv *env, jstring title) {
+ return getJniResult(env, title, 0, 0, "", 0);
+}
+
+
+/* Set int fieldName field for clazz to value. */
+string setIntField(JNIEnv *env, jobject obj, jclass clazz, const string fieldName, const int value) {
+ jfieldID field = env->GetFieldID(clazz, fieldName.c_str(), "I");
+ if (checkJniException(env)) return JNI_EXCEPTION;
+ if (!field) {
+ return "Failed to get int \"" + string(fieldName) + "\" field of \"" +
+ get_class_name(env, clazz) + "\" class to set value \"" + to_string(value) + "\"";
+ }
+
+ env->SetIntField(obj, field, value);
+ if (checkJniException(env)) return JNI_EXCEPTION;
+
+ return "";
+}
+
+/* Set String fieldName field for clazz to value. */
+string setStringField(JNIEnv *env, jobject obj, jclass clazz, const string fieldName, const string value) {
+ jfieldID field = env->GetFieldID(clazz, fieldName.c_str(), "Ljava/lang/String;");
+ if (checkJniException(env)) return JNI_EXCEPTION;
+ if (!field) {
+ return "Failed to get String \"" + string(fieldName) + "\" field of \"" +
+ get_class_name(env, clazz) + "\" class to set value \"" + value + "\"";
+ }
+
+ env->SetObjectField(obj, field, env->NewStringUTF(value.c_str()));
+ if (checkJniException(env)) return JNI_EXCEPTION;
+
+ return "";
+}
+
+
+
+extern "C"
+JNIEXPORT jobject JNICALL
+Java_com_termux_shared_net_socket_local_LocalSocketManager_createServerSocketNative(JNIEnv *env, jclass clazz,
+ jstring logTitle,
+ jbyteArray pathArray,
+ jint backlog) {
+ if (backlog < 1 || backlog > 500) {
+ return getJniResult(env, logTitle, -1, "createServerSocketNative(): Backlog \"" +
+ to_string(backlog) + "\" is not between 1-500");
+ }
+
+ // Create server socket
+ int fd = socket(AF_UNIX, SOCK_STREAM, 0);
+ if (fd == -1) {
+ return getJniResult(env, logTitle, -1, errno, "createServerSocketNative(): Create local socket failed");
+ }
+
+ jbyte* path = env->GetByteArrayElements(pathArray, nullptr);
+ if (checkJniException(env)) return NULL;
+ if (path == nullptr) {
+ close(fd);
+ return getJniResult(env, logTitle, -1, "createServerSocketNative(): Path passed is null");
+ }
+
+ // On Linux, sun_path is 108 bytes (UNIX_PATH_MAX) in size
+ int chars = env->GetArrayLength(pathArray);
+ if (checkJniException(env)) return NULL;
+ if (chars >= 108 || chars >= sizeof(struct sockaddr_un) - sizeof(sa_family_t)) {
+ env->ReleaseByteArrayElements(pathArray, path, JNI_ABORT);
+ if (checkJniException(env)) return NULL;
+ close(fd);
+ return getJniResult(env, logTitle, -1, "createServerSocketNative(): Path passed is too long");
+ }
+
+ struct sockaddr_un adr = {.sun_family = AF_UNIX};
+ memcpy(&adr.sun_path, path, chars);
+
+ // Bind path to server socket
+ if (::bind(fd, reinterpret_cast(&adr), sizeof(adr)) == -1) {
+ int errnoBackup = errno;
+ env->ReleaseByteArrayElements(pathArray, path, JNI_ABORT);
+ if (checkJniException(env)) return NULL;
+ close(fd);
+ return getJniResult(env, logTitle, -1, errnoBackup,
+ "createServerSocketNative(): Bind to local socket at path \"" + string(adr.sun_path) + "\" with fd " + to_string(fd) + " failed");
+ }
+
+ // Start listening for client sockets on server socket
+ if (listen(fd, backlog) == -1) {
+ int errnoBackup = errno;
+ env->ReleaseByteArrayElements(pathArray, path, JNI_ABORT);
+ if (checkJniException(env)) return NULL;
+ close(fd);
+ return getJniResult(env, logTitle, -1, errnoBackup,
+ "createServerSocketNative(): Listen on local socket at path \"" + string(adr.sun_path) + "\" with fd " + to_string(fd) + " failed");
+ }
+
+ env->ReleaseByteArrayElements(pathArray, path, JNI_ABORT);
+ if (checkJniException(env)) return NULL;
+
+ // Return success and server socket fd in JniResult.intData field
+ return getJniResult(env, logTitle, fd);
+}
+
+extern "C"
+JNIEXPORT jobject JNICALL
+Java_com_termux_shared_net_socket_local_LocalSocketManager_closeSocketNative(JNIEnv *env, jclass clazz,
+ jstring logTitle, jint fd) {
+ if (fd < 0) {
+ return getJniResult(env, logTitle, -1, "closeSocketNative(): Invalid fd \"" + to_string(fd) + "\" passed");
+ }
+
+ if (close(fd) == -1) {
+ return getJniResult(env, logTitle, -1, errno, "closeSocketNative(): Failed to close socket fd " + to_string(fd));
+ }
+
+ // Return success
+ return getJniResult(env, logTitle);
+}
+
+extern "C"
+JNIEXPORT jobject JNICALL
+Java_com_termux_shared_net_socket_local_LocalSocketManager_acceptNative(JNIEnv *env, jclass clazz,
+ jstring logTitle, jint fd) {
+ if (fd < 0) {
+ return getJniResult(env, logTitle, -1, "acceptNative(): Invalid fd \"" + to_string(fd) + "\" passed");
+ }
+
+ // Accept client socket
+ int clientFd = accept(fd, nullptr, nullptr);
+ if (clientFd == -1) {
+ return getJniResult(env, logTitle, -1, errno, "acceptNative(): Failed to accept client on fd " + to_string(fd));
+ }
+
+ // Return success and client socket fd in JniResult.intData field
+ return getJniResult(env, logTitle, clientFd);
+}
+
+extern "C"
+JNIEXPORT jobject JNICALL
+Java_com_termux_shared_net_socket_local_LocalSocketManager_readNative(JNIEnv *env, jclass clazz,
+ jstring logTitle,
+ jint fd, jbyteArray dataArray,
+ jlong deadline) {
+ if (fd < 0) {
+ return getJniResult(env, logTitle, -1, "readNative(): Invalid fd \"" + to_string(fd) + "\" passed");
+ }
+
+ jbyte* data = env->GetByteArrayElements(dataArray, nullptr);
+ if (checkJniException(env)) return NULL;
+ if (data == nullptr) {
+ return getJniResult(env, logTitle, -1, "readNative(): data passed is null");
+ }
+
+ struct timespec time = {};
+ jbyte* current = data;
+ int bytes = env->GetArrayLength(dataArray);
+ if (checkJniException(env)) return NULL;
+ int bytesRead = 0;
+ while (bytesRead < bytes) {
+ if (deadline > 0) {
+ if (clock_gettime(CLOCK_REALTIME, &time) != -1) {
+ // If current time is greater than the time defined in deadline
+ if (timespec_to_milliseconds(&time) > deadline) {
+ env->ReleaseByteArrayElements(dataArray, data, 0);
+ if (checkJniException(env)) return NULL;
+ return getJniResult(env, logTitle, -1,
+ "readNative(): Deadline \"" + to_string(deadline) + "\" timeout");
+ }
+ } else {
+ log_warn(get_title_and_message(env, logTitle,
+ "readNative(): Deadline \"" + to_string(deadline) +
+ "\" timeout will not work since failed to get current time"));
+ }
+ }
+
+ // Read data from socket
+ int ret = read(fd, current, bytes);
+ if (ret == -1) {
+ int errnoBackup = errno;
+ env->ReleaseByteArrayElements(dataArray, data, 0);
+ if (checkJniException(env)) return NULL;
+ return getJniResult(env, logTitle, -1, errnoBackup, "readNative(): Failed to read on fd " + to_string(fd));
+ }
+ // EOF, peer closed writing end
+ if (ret == 0) {
+ break;
+ }
+
+ bytesRead += ret;
+ current += ret;
+ }
+
+ env->ReleaseByteArrayElements(dataArray, data, 0);
+ if (checkJniException(env)) return NULL;
+
+ // Return success and bytes read in JniResult.intData field
+ return getJniResult(env, logTitle, bytesRead);
+}
+
+
+extern "C"
+JNIEXPORT jobject JNICALL
+Java_com_termux_shared_net_socket_local_LocalSocketManager_sendNative(JNIEnv *env, jclass clazz,
+ jstring logTitle,
+ jint fd, jbyteArray dataArray,
+ jlong deadline) {
+ if (fd < 0) {
+ return getJniResult(env, logTitle, -1, "sendNative(): Invalid fd \"" + to_string(fd) + "\" passed");
+ }
+
+ jbyte* data = env->GetByteArrayElements(dataArray, nullptr);
+ if (checkJniException(env)) return NULL;
+ if (data == nullptr) {
+ return getJniResult(env, logTitle, -1, "sendNative(): data passed is null");
+ }
+
+ struct timespec time = {};
+ jbyte* current = data;
+ int bytes = env->GetArrayLength(dataArray);
+ if (checkJniException(env)) return NULL;
+ while (bytes > 0) {
+ if (deadline > 0) {
+ if (clock_gettime(CLOCK_REALTIME, &time) != -1) {
+ // If current time is greater than the time defined in deadline
+ if (timespec_to_milliseconds(&time) > deadline) {
+ env->ReleaseByteArrayElements(dataArray, data, JNI_ABORT);
+ if (checkJniException(env)) return NULL;
+ return getJniResult(env, logTitle, -1,
+ "sendNative(): Deadline \"" + to_string(deadline) + "\" timeout");
+ }
+ } else {
+ log_warn(get_title_and_message(env, logTitle,
+ "sendNative(): Deadline \"" + to_string(deadline) +
+ "\" timeout will not work since failed to get current time"));
+ }
+ }
+
+ // Send data to socket
+ int ret = send(fd, current, bytes, MSG_NOSIGNAL);
+ if (ret == -1) {
+ int errnoBackup = errno;
+ env->ReleaseByteArrayElements(dataArray, data, JNI_ABORT);
+ if (checkJniException(env)) return NULL;
+ return getJniResult(env, logTitle, -1, errnoBackup, "sendNative(): Failed to send on fd " + to_string(fd));
+ }
+
+ bytes -= ret;
+ current += ret;
+ }
+
+ env->ReleaseByteArrayElements(dataArray, data, JNI_ABORT);
+ if (checkJniException(env)) return NULL;
+
+ // Return success
+ return getJniResult(env, logTitle);
+}
+
+extern "C"
+JNIEXPORT jobject JNICALL
+Java_com_termux_shared_net_socket_local_LocalSocketManager_availableNative(JNIEnv *env, jclass clazz,
+ jstring logTitle, jint fd) {
+ if (fd < 0) {
+ return getJniResult(env, logTitle, -1, "availableNative(): Invalid fd \"" + to_string(fd) + "\" passed");
+ }
+
+ int available = 0;
+ if (ioctl(fd, SIOCINQ, &available) == -1) {
+ return getJniResult(env, logTitle, -1, errno,
+ "availableNative(): Failed to get number of unread bytes in the receive buffer of fd " + to_string(fd));
+ }
+
+ // Return success and bytes available in JniResult.intData field
+ return getJniResult(env, logTitle, available);
+}
+
+/* Sets socket option timeout in milliseconds. */
+int set_socket_timeout(int fd, int option, int timeout) {
+ struct timeval tv = milliseconds_to_timeval(timeout);
+ socklen_t len = sizeof(tv);
+ return setsockopt(fd, SOL_SOCKET, option, &tv, len);
+}
+
+extern "C"
+JNIEXPORT jobject JNICALL
+Java_com_termux_shared_net_socket_local_LocalSocketManager_setSocketReadTimeoutNative(JNIEnv *env, jclass clazz,
+ jstring logTitle,
+ jint fd, jint timeout) {
+ if (fd < 0) {
+ return getJniResult(env, logTitle, -1, "setSocketReadTimeoutNative(): Invalid fd \"" + to_string(fd) + "\" passed");
+ }
+
+ if (set_socket_timeout(fd, SO_RCVTIMEO, timeout) == -1) {
+ return getJniResult(env, logTitle, -1, errno,
+ "setSocketReadTimeoutNative(): Failed to set socket receiving (SO_RCVTIMEO) timeout for fd " + to_string(fd));
+ }
+
+ // Return success
+ return getJniResult(env, logTitle);
+}
+
+extern "C"
+JNIEXPORT jobject JNICALL
+Java_com_termux_shared_net_socket_local_LocalSocketManager_setSocketSendTimeoutNative(JNIEnv *env, jclass clazz,
+ jstring logTitle,
+ jint fd, jint timeout) {
+ if (fd < 0) {
+ return getJniResult(env, logTitle, -1, "setSocketSendTimeoutNative(): Invalid fd \"" +
+ to_string(fd) + "\" passed");
+ }
+
+ if (set_socket_timeout(fd, SO_SNDTIMEO, timeout) == -1) {
+ return getJniResult(env, logTitle, -1, errno,
+ "setSocketSendTimeoutNative(): Failed to set socket sending (SO_SNDTIMEO) timeout for fd " + to_string(fd));
+ }
+
+ // Return success
+ return getJniResult(env, logTitle);
+}
+
+extern "C"
+JNIEXPORT jobject JNICALL
+Java_com_termux_shared_net_socket_local_LocalSocketManager_getPeerCredNative(JNIEnv *env, jclass clazz,
+ jstring logTitle,
+ jint fd, jobject peerCred) {
+ if (fd < 0) {
+ return getJniResult(env, logTitle, -1, "getPeerCredNative(): Invalid fd \"" + to_string(fd) + "\" passed");
+ }
+
+ if (peerCred == nullptr) {
+ return getJniResult(env, logTitle, -1, "getPeerCredNative(): peerCred passed is null");
+ }
+
+ // Initialize to -1 instead of 0 in case a failed getsockopt() call somehow doesn't report failure and returns the uid of root
+ struct ucred cred = {};
+ cred.pid = -1; cred.uid = -1; cred.gid = -1;
+
+ socklen_t len = sizeof(cred);
+
+ if (getsockopt(fd, SOL_SOCKET, SO_PEERCRED, &cred, &len) == -1) {
+ return getJniResult(env, logTitle, -1, errno, "getPeerCredNative(): Failed to get peer credentials for fd " + to_string(fd));
+ }
+
+ // Fill "com.termux.shared.net.socket.local.PeerCred" object.
+ // The pid, uid and gid will always be set based on ucred.
+ // The pname and cmdline will only be set if current process has access to "/proc/[pid]/cmdline"
+ // of peer process. Processes of other users/apps are not normally accessible.
+ jclass peerCredClazz = env->GetObjectClass(peerCred);
+ if (checkJniException(env)) return NULL;
+ if (!peerCredClazz) {
+ return getJniResult(env, logTitle, -1, errno, "getPeerCredNative(): Failed to get PeerCred class");
+ }
+
+ string error;
+
+ error = setIntField(env, peerCred, peerCredClazz, "pid", cred.pid);
+ if (!error.empty()) {
+ if (error == JNI_EXCEPTION) return NULL;
+ return getJniResult(env, logTitle, -1, "getPeerCredNative(): " + error);
+ }
+
+ error = setIntField(env, peerCred, peerCredClazz, "uid", cred.uid);
+ if (!error.empty()) {
+ if (error == JNI_EXCEPTION) return NULL;
+ return getJniResult(env, logTitle, -1, "getPeerCredNative(): " + error);
+ }
+
+ error = setIntField(env, peerCred, peerCredClazz, "gid", cred.gid);
+ if (!error.empty()) {
+ if (error == JNI_EXCEPTION) return NULL;
+ return getJniResult(env, logTitle, -1, "getPeerCredNative(): " + error);
+ }
+
+ string cmdline = get_process_cmdline(cred.pid);
+ if (!cmdline.empty()) {
+ error = setStringField(env, peerCred, peerCredClazz, "pname", get_process_name_from_cmdline(cmdline));
+ if (!error.empty()) {
+ if (error == JNI_EXCEPTION) return NULL;
+ return getJniResult(env, logTitle, -1, "getPeerCredNative(): " + error);
+ }
+
+ error = setStringField(env, peerCred, peerCredClazz, "cmdline", get_process_cmdline_spaced(cmdline));
+ if (!error.empty()) {
+ if (error == JNI_EXCEPTION) return NULL;
+ return getJniResult(env, logTitle, -1, "getPeerCredNative(): " + error);
+ }
+ }
+
+ // Return success since PeerCred was filled successfully
+ return getJniResult(env, logTitle);
+}
diff --git a/termux-shared/src/main/java/com/termux/shared/android/ProcessUtils.java b/termux-shared/src/main/java/com/termux/shared/android/ProcessUtils.java
new file mode 100644
index 0000000000..c6d182b06e
--- /dev/null
+++ b/termux-shared/src/main/java/com/termux/shared/android/ProcessUtils.java
@@ -0,0 +1,58 @@
+package com.termux.shared.android;
+
+import android.app.ActivityManager;
+import android.content.Context;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.termux.shared.logger.Logger;
+
+import java.util.List;
+
+public class ProcessUtils {
+
+ public static final String LOG_TAG = "ProcessUtils";
+
+ /**
+ * Get the app process name for a pid with a call to {@link ActivityManager#getRunningAppProcesses()}.
+ *
+ * This will not return child process names. Android did not keep track of them before android 12
+ * phantom process addition, but there is no API via IActivityManager to get them.
+ *
+ * To get process name for pids of own app's child processes, check `get_process_name_from_cmdline()`
+ * in `local-socket.cpp`.
+ *
+ * https://cs.android.com/android/platform/superproject/+/android-12.0.0_r32:frameworks/base/core/java/android/app/ActivityManager.java;l=3362
+ * https://cs.android.com/android/platform/superproject/+/android-12.0.0_r32:frameworks/base/services/core/java/com/android/server/am/ActivityManagerService.java;l=8434
+ * https://cs.android.com/android/_/android/platform/frameworks/base/+/refs/tags/android-12.0.0_r32:services/core/java/com/android/server/am/PhantomProcessList.java
+ * https://cs.android.com/android/_/android/platform/frameworks/base/+/refs/tags/android-12.0.0_r32:services/core/java/com/android/server/am/PhantomProcessRecord.java
+ *
+ * @param context The {@link Context} for operations.
+ * @param pid The pid of the process.
+ * @return Returns the app process name if found, otherwise {@code null}.
+ */
+ @Nullable
+ public static String getAppProcessNameForPid(@NonNull Context context, int pid) {
+ if (pid < 0) return null;
+
+ ActivityManager activityManager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
+ if (activityManager == null) return null;
+ try {
+ List runningApps = activityManager.getRunningAppProcesses();
+ if (runningApps == null) {
+ return null;
+ }
+ for (ActivityManager.RunningAppProcessInfo procInfo : runningApps) {
+ if (procInfo.pid == pid) {
+ return procInfo.processName;
+ }
+ }
+ } catch (Exception e) {
+ Logger.logStackTraceWithMessage(LOG_TAG, "Failed to get app process name for pid " + pid, e);
+ }
+
+ return null;
+ }
+
+}
diff --git a/termux-shared/src/main/java/com/termux/shared/android/UserUtils.java b/termux-shared/src/main/java/com/termux/shared/android/UserUtils.java
new file mode 100644
index 0000000000..f819de3f4e
--- /dev/null
+++ b/termux-shared/src/main/java/com/termux/shared/android/UserUtils.java
@@ -0,0 +1,143 @@
+package com.termux.shared.android;
+
+import android.content.Context;
+import android.content.pm.PackageManager;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.termux.shared.logger.Logger;
+import com.termux.shared.reflection.ReflectionUtils;
+
+import java.lang.reflect.Method;
+
+public class UserUtils {
+
+ public static final String LOG_TAG = "UserUtils";
+
+ /**
+ * Get the user name for user id with a call to {@link #getNameForUidFromPackageManager(Context, int)}
+ * and if that fails, then a call to {@link #getNameForUidFromLibcore(int)}.
+ *
+ * @param context The {@link Context} for operations.
+ * @param uid The user id.
+ * @return Returns the user name if found, otherwise {@code null}.
+ */
+ @Nullable
+ public static String getNameForUid(@NonNull Context context, int uid) {
+ String name = getNameForUidFromPackageManager(context, uid);
+ if (name == null)
+ name = getNameForUidFromLibcore(uid);
+ return name;
+ }
+
+ /**
+ * Get the user name for user id with a call to {@link PackageManager#getNameForUid(int)}.
+ *
+ * This will not return user names for non app user id like for root user 0, use {@link #getNameForUidFromLibcore(int)}
+ * to get those.
+ *
+ * https://cs.android.com/android/platform/superproject/+/android-12.0.0_r32:frameworks/base/core/java/android/content/pm/PackageManager.java;l=5556
+ * https://cs.android.com/android/platform/superproject/+/android-12.0.0_r32:frameworks/base/core/java/android/app/ApplicationPackageManager.java;l=1028
+ * https://cs.android.com/android/platform/superproject/+/android-12.0.0_r32:frameworks/base/services/core/java/com/android/server/pm/PackageManagerService.java;l=10293
+ *
+ * @param context The {@link Context} for operations.
+ * @param uid The user id.
+ * @return Returns the user name if found, otherwise {@code null}.
+ */
+ @Nullable
+ public static String getNameForUidFromPackageManager(@NonNull Context context, int uid) {
+ if (uid < 0) return null;
+
+ try {
+ String name = context.getPackageManager().getNameForUid(uid);
+ if (name != null && name.endsWith(":" + uid))
+ name = name.replaceAll(":" + uid + "$", ""); // Remove ":" suffix
+ return name;
+ } catch (Exception e) {
+ Logger.logStackTraceWithMessage(LOG_TAG, "Failed to get name for uid \"" + uid + "\" from package manager", e);
+ return null;
+ }
+ }
+
+ /**
+ * Get the user name for user id with a call to `Libcore.os.getpwuid()`.
+ *
+ * This will return user names for non app user id like for root user 0 as well, but this call
+ * is expensive due to usage of reflection, and requires hidden API bypass, check
+ * {@link ReflectionUtils#bypassHiddenAPIReflectionRestrictions()} for details.
+ *
+ * `BlockGuardOs` implements the `Os` interface and its instance is stored in `Libcore` class static `os` field.
+ * The `getpwuid` method is implemented by `ForwardingOs`, which is the super class of `BlockGuardOs`.
+ * The `getpwuid` method returns `StructPasswd` object whose `pw_name` contains the user name for id.
+ *
+ * https://stackoverflow.com/a/28057167/14686958
+ * https://cs.android.com/android/platform/superproject/+/android-12.0.0_r32:libcore/luni/src/main/java/libcore/io/Libcore.java;l=39
+ * https://cs.android.com/android/platform/superproject/+/android-12.0.0_r32:libcore/luni/src/main/java/libcore/io/Os.java;l=279
+ * https://cs.android.com/android/platform/superproject/+/android-12.0.0_r32:libcore/luni/src/main/java/libcore/io/BlockGuardOs.java
+ * https://cs.android.com/android/platform/superproject/+/android-12.0.0_r32:libcore/luni/src/main/java/libcore/io/ForwardingOs.java;l=340
+ * https://cs.android.com/android/platform/superproject/+/android-12.0.0_r32:libcore/luni/src/main/java/android/system/StructPasswd.java
+ * https://cs.android.com/android/platform/superproject/+/android-12.0.0_r32:bionic/libc/bionic/grp_pwd.cpp;l=553
+ * https://cs.android.com/android/platform/superproject/+/android-12.0.0_r32:system/core/libcutils/include/private/android_filesystem_config.h;l=43
+ *
+ * @param uid The user id.
+ * @return Returns the user name if found, otherwise {@code null}.
+ */
+ @Nullable
+ public static String getNameForUidFromLibcore(int uid) {
+ if (uid < 0) return null;
+
+ ReflectionUtils.bypassHiddenAPIReflectionRestrictions();
+ try {
+ String libcoreClassName = "libcore.io.Libcore";
+ Class> clazz = Class.forName(libcoreClassName);
+ Object os; // libcore.io.BlockGuardOs
+ try {
+ os = ReflectionUtils.invokeField(Class.forName(libcoreClassName), "os", null).value;
+ } catch (Exception e) {
+ // ClassCastException may be thrown
+ Logger.logStackTraceWithMessage(LOG_TAG, "Failed to get \"os\" field value for " + libcoreClassName + " class", e);
+ return null;
+ }
+
+ if (os == null) {
+ Logger.logError(LOG_TAG, "Failed to get BlockGuardOs class obj from Libcore");
+ return null;
+ }
+
+ clazz = os.getClass().getSuperclass(); // libcore.io.ForwardingOs
+ if (clazz == null) {
+ Logger.logError(LOG_TAG, "Failed to find super class ForwardingOs from object of class " + os.getClass().getName());
+ return null;
+ }
+
+ Object structPasswd; // android.system.StructPasswd
+ try {
+ Method getpwuidMethod = ReflectionUtils.getDeclaredMethod(clazz, "getpwuid", int.class);
+ if (getpwuidMethod == null) return null;
+ structPasswd = ReflectionUtils.invokeMethod(getpwuidMethod, os, uid).value;
+ } catch (Exception e) {
+ Logger.logStackTraceWithMessage(LOG_TAG, "Failed to invoke getpwuid() method of " + clazz.getName() + " class", e);
+ return null;
+ }
+
+ if (structPasswd == null) {
+ Logger.logError(LOG_TAG, "Failed to get StructPasswd obj from call to ForwardingOs.getpwuid()");
+ return null;
+ }
+
+ try {
+ clazz = structPasswd.getClass();
+ return (String) ReflectionUtils.invokeField(clazz, "pw_name", structPasswd).value;
+ } catch (Exception e) {
+ // ClassCastException may be thrown
+ Logger.logStackTraceWithMessage(LOG_TAG, "Failed to get \"pw_name\" field value for " + clazz.getName() + " class", e);
+ return null;
+ }
+ } catch (Exception e) {
+ Logger.logStackTraceWithMessage(LOG_TAG, "Failed to get name for uid \"" + uid + "\" from Libcore", e);
+ return null;
+ }
+ }
+
+}
diff --git a/termux-shared/src/main/java/com/termux/shared/crash/CrashHandler.java b/termux-shared/src/main/java/com/termux/shared/crash/CrashHandler.java
index 79309f2487..f67a761283 100644
--- a/termux-shared/src/main/java/com/termux/shared/crash/CrashHandler.java
+++ b/termux-shared/src/main/java/com/termux/shared/crash/CrashHandler.java
@@ -19,31 +19,51 @@ public class CrashHandler implements Thread.UncaughtExceptionHandler {
private final Context mContext;
private final CrashHandlerClient mCrashHandlerClient;
- private final Thread.UncaughtExceptionHandler defaultUEH;
+ private final Thread.UncaughtExceptionHandler mDefaultUEH;
+ private final boolean mIsDefaultHandler;
private static final String LOG_TAG = "CrashUtils";
- private CrashHandler(@NonNull final Context context, @NonNull final CrashHandlerClient crashHandlerClient) {
- this.mContext = context;
- this.mCrashHandlerClient = crashHandlerClient;
- this.defaultUEH = Thread.getDefaultUncaughtExceptionHandler();
+ private CrashHandler(@NonNull final Context context, @NonNull final CrashHandlerClient crashHandlerClient,
+ boolean isDefaultHandler) {
+ mContext = context;
+ mCrashHandlerClient = crashHandlerClient;
+ mDefaultUEH = Thread.getDefaultUncaughtExceptionHandler();
+ mIsDefaultHandler = isDefaultHandler;
}
public void uncaughtException(@NonNull Thread thread, @NonNull Throwable throwable) {
Logger.logInfo(LOG_TAG, "uncaughtException() for " + thread + ": " + throwable.getMessage());
logCrash(thread, throwable);
- defaultUEH.uncaughtException(thread, throwable);
+
+ // Don't stop the app if not on the main thread
+ if (mIsDefaultHandler)
+ mDefaultUEH.uncaughtException(thread, throwable);
}
/**
- * Set default uncaught crash handler of current thread to {@link CrashHandler}.
+ * Set default uncaught crash handler for the app to {@link CrashHandler}.
*/
- public static void setCrashHandler(@NonNull final Context context, @NonNull final CrashHandlerClient crashHandlerClient) {
+ public static void setDefaultCrashHandler(@NonNull final Context context, @NonNull final CrashHandlerClient crashHandlerClient) {
if (!(Thread.getDefaultUncaughtExceptionHandler() instanceof CrashHandler)) {
- Thread.setDefaultUncaughtExceptionHandler(new CrashHandler(context, crashHandlerClient));
+ Thread.setDefaultUncaughtExceptionHandler(new CrashHandler(context, crashHandlerClient, true));
}
}
+ /**
+ * Set uncaught crash handler of current non-main thread to {@link CrashHandler}.
+ */
+ public static void setCrashHandler(@NonNull final Context context, @NonNull final CrashHandlerClient crashHandlerClient) {
+ Thread.currentThread().setUncaughtExceptionHandler(new CrashHandler(context, crashHandlerClient, false));
+ }
+
+ /**
+ * Get {@link CrashHandler} instance that can be set as uncaught crash handler of a non-main thread.
+ */
+ public static CrashHandler getCrashHandler(@NonNull final Context context, @NonNull final CrashHandlerClient crashHandlerClient) {
+ return new CrashHandler(context, crashHandlerClient, false);
+ }
+
/**
* Log a crash in the crash log file at path returned by {@link CrashHandlerClient#getCrashLogFilePath(Context)}.
*
@@ -56,7 +76,7 @@ public static void logCrash(@NonNull Context context,
@NonNull CrashHandlerClient crashHandlerClient,
@NonNull Thread thread, @NonNull Throwable throwable) {
Logger.logInfo(LOG_TAG, "logCrash() for " + thread + ": " + throwable.getMessage());
- new CrashHandler(context, crashHandlerClient).logCrash(thread, throwable);
+ new CrashHandler(context, crashHandlerClient, false).logCrash(thread, throwable);
}
public void logCrash(@NonNull Thread thread, @NonNull Throwable throwable) {
@@ -99,7 +119,7 @@ public interface CrashHandlerClient {
/**
* Called before {@link #logCrashToFile(Context, CrashHandlerClient, Thread, Throwable)} is called.
*
- * @param context The {@link Context} passed to {@link CrashHandler#CrashHandler(Context, CrashHandlerClient)}.
+ * @param context The {@link Context} passed to {@link CrashHandler#CrashHandler(Context, CrashHandlerClient, boolean)}.
* @param thread The {@link Thread} in which the crash happened.
* @param throwable The {@link Throwable} thrown for the crash.
* @return Should return {@code true} if crash has been handled and should not be logged,
@@ -110,7 +130,7 @@ public interface CrashHandlerClient {
/**
* Called after {@link #logCrashToFile(Context, CrashHandlerClient, Thread, Throwable)} is called.
*
- * @param context The {@link Context} passed to {@link CrashHandler#CrashHandler(Context, CrashHandlerClient)}.
+ * @param context The {@link Context} passed to {@link CrashHandler#CrashHandler(Context, CrashHandlerClient, boolean)}.
* @param thread The {@link Thread} in which the crash happened.
* @param throwable The {@link Throwable} thrown for the crash.
*/
@@ -119,7 +139,7 @@ public interface CrashHandlerClient {
/**
* Get crash log file path.
*
- * @param context The {@link Context} passed to {@link CrashHandler#CrashHandler(Context, CrashHandlerClient)}.
+ * @param context The {@link Context} passed to {@link CrashHandler#CrashHandler(Context, CrashHandlerClient, boolean)}.
* @return Should return the crash log file path.
*/
@NonNull
@@ -128,7 +148,7 @@ public interface CrashHandlerClient {
/**
* Get app info markdown string to add to crash log.
*
- * @param context The {@link Context} passed to {@link CrashHandler#CrashHandler(Context, CrashHandlerClient)}.
+ * @param context The {@link Context} passed to {@link CrashHandler#CrashHandler(Context, CrashHandlerClient, boolean)}.
* @return Should return app info markdown string.
*/
String getAppInfoMarkdownString(Context context);
diff --git a/termux-shared/src/main/java/com/termux/shared/data/DataUtils.java b/termux-shared/src/main/java/com/termux/shared/data/DataUtils.java
index 50f0e7ba23..f95f2b52f8 100644
--- a/termux-shared/src/main/java/com/termux/shared/data/DataUtils.java
+++ b/termux-shared/src/main/java/com/termux/shared/data/DataUtils.java
@@ -2,11 +2,15 @@
import android.os.Bundle;
+import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import com.google.common.base.Strings;
+
import java.io.ByteArrayOutputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
+import java.util.Collections;
public class DataUtils {
@@ -162,6 +166,51 @@ public static float rangedOrDefault(float value, float def, float min, float max
+ /**
+ * Add a space indent to a {@link String}. Each indent is 4 space characters long.
+ *
+ * @param string The {@link String} to add indent to.
+ * @param count The indent count.
+ * @return Returns the indented {@link String}.
+ */
+ public static String getSpaceIndentedString(String string, int count) {
+ if (string == null || string.isEmpty())
+ return string;
+ else
+ return getIndentedString(string, " ", count);
+ }
+
+ /**
+ * Add a tab indent to a {@link String}. Each indent is 1 tab character long.
+ *
+ * @param string The {@link String} to add indent to.
+ * @param count The indent count.
+ * @return Returns the indented {@link String}.
+ */
+ public static String getTabIndentedString(String string, int count) {
+ if (string == null || string.isEmpty())
+ return string;
+ else
+ return getIndentedString(string, "\t", count);
+ }
+
+ /**
+ * Add an indent to a {@link String}.
+ *
+ * @param string The {@link String} to add indent to.
+ * @param indent The indent characters.
+ * @param count The indent count.
+ * @return Returns the indented {@link String}.
+ */
+ public static String getIndentedString(String string, @NonNull String indent, int count) {
+ if (string == null || string.isEmpty())
+ return string;
+ else
+ return string.replaceAll("(?m)^", Strings.repeat(indent, Math.max(count, 1)));
+ }
+
+
+
/**
* Get the object itself if it is not {@code null}, otherwise default.
*
diff --git a/termux-shared/src/main/java/com/termux/shared/errors/Error.java b/termux-shared/src/main/java/com/termux/shared/errors/Error.java
index 6272b3e452..be00e34ff7 100644
--- a/termux-shared/src/main/java/com/termux/shared/errors/Error.java
+++ b/termux-shared/src/main/java/com/termux/shared/errors/Error.java
@@ -209,7 +209,7 @@ public String getErrorLogString() {
logString.append(getCodeString());
logString.append("\n").append(getTypeAndMessageLogString());
- if (this.throwablesList != null)
+ if (throwablesList != null && throwablesList.size() > 0)
logString.append("\n").append(geStackTracesLogString());
return logString.toString();
@@ -272,7 +272,8 @@ public String getErrorMarkdownString() {
markdownString.append(MarkdownUtils.getSingleLineMarkdownStringEntry("Error Code", getCode(), "-"));
markdownString.append("\n").append(MarkdownUtils.getMultiLineMarkdownStringEntry(
(Errno.TYPE.equals(getType()) ? "Error Message" : "Error Message (" + getType() + ")"), message, "-"));
- markdownString.append("\n\n").append(geStackTracesMarkdownString());
+ if (throwablesList != null && throwablesList.size() > 0)
+ markdownString.append("\n\n").append(geStackTracesMarkdownString());
return markdownString.toString();
}
diff --git a/termux-shared/src/main/java/com/termux/shared/file/FileUtils.java b/termux-shared/src/main/java/com/termux/shared/file/FileUtils.java
index 512aca2f94..f51de78283 100644
--- a/termux-shared/src/main/java/com/termux/shared/file/FileUtils.java
+++ b/termux-shared/src/main/java/com/termux/shared/file/FileUtils.java
@@ -1116,6 +1116,21 @@ public static Error deleteSymlinkFile(String label, final String filePath, final
return deleteFile(label, filePath, ignoreNonExistentFile, false, FileType.SYMLINK.getValue());
}
+ /**
+ * Delete socket file at path.
+ *
+ * This function is a wrapper for {@link #deleteFile(String, String, boolean, boolean, int)}.
+ *
+ * @param label The optional label for file to delete. This can optionally be {@code null}.
+ * @param filePath The {@code path} for file to delete.
+ * @param ignoreNonExistentFile The {@code boolean} that decides if it should be considered an
+ * error if file to deleted doesn't exist.
+ * @return Returns the {@code error} if deletion was not successful, otherwise {@code null}.
+ */
+ public static Error deleteSocketFile(String label, final String filePath, final boolean ignoreNonExistentFile) {
+ return deleteFile(label, filePath, ignoreNonExistentFile, false, FileType.SOCKET.getValue());
+ }
+
/**
* Delete regular, directory or symlink file at path.
*
@@ -1178,12 +1193,12 @@ public static Error deleteFile(String label, final String filePath, final boolea
if ((allowedFileTypeFlags & fileType.getValue()) <= 0) {
// If wrong file type is to be ignored
if (ignoreWrongFileType) {
- Logger.logVerbose(LOG_TAG, "Ignoring deletion of " + label + "file at path \"" + filePath + "\" not matching allowed file types: " + FileTypes.convertFileTypeFlagsToNamesString(allowedFileTypeFlags));
+ Logger.logVerbose(LOG_TAG, "Ignoring deletion of " + label + "file at path \"" + filePath + "\" of type \"" + fileType.getName() + "\" not matching allowed file types: " + FileTypes.convertFileTypeFlagsToNamesString(allowedFileTypeFlags));
return null;
}
// Else return with error
- return FileUtilsErrno.ERRNO_FILE_NOT_AN_ALLOWED_FILE_TYPE.getError(label + "file meant to be deleted", filePath, FileTypes.convertFileTypeFlagsToNamesString(allowedFileTypeFlags));
+ return FileUtilsErrno.ERRNO_FILE_NOT_AN_ALLOWED_FILE_TYPE.getError(label + "file meant to be deleted", filePath, fileType.getName(), FileTypes.convertFileTypeFlagsToNamesString(allowedFileTypeFlags));
}
Logger.logVerbose(LOG_TAG, "Deleting " + label + "file at path \"" + filePath + "\"");
diff --git a/termux-shared/src/main/java/com/termux/shared/file/FileUtilsErrno.java b/termux-shared/src/main/java/com/termux/shared/file/FileUtilsErrno.java
index b7da663713..4a0d73f442 100644
--- a/termux-shared/src/main/java/com/termux/shared/file/FileUtilsErrno.java
+++ b/termux-shared/src/main/java/com/termux/shared/file/FileUtilsErrno.java
@@ -33,7 +33,7 @@ public class FileUtilsErrno extends Errno {
public static final Errno ERRNO_NON_SYMLINK_FILE_FOUND = new Errno(TYPE, 156, "Non-symlink file found at %1$s path \"%2$s\".");
public static final Errno ERRNO_NON_SYMLINK_FILE_FOUND_SHORT = new Errno(TYPE, 157, "Non-symlink file found at %1$s path.");
- public static final Errno ERRNO_FILE_NOT_AN_ALLOWED_FILE_TYPE = new Errno(TYPE, 158, "The %1$s found at path \"%2$s\" is not one of allowed file types \"%3$s\".");
+ public static final Errno ERRNO_FILE_NOT_AN_ALLOWED_FILE_TYPE = new Errno(TYPE, 158, "The %1$s found at path \"%2$s\" of type \"%3$s\" is not one of allowed file types \"%4$s\".");
public static final Errno ERRNO_VALIDATE_FILE_EXISTENCE_AND_PERMISSIONS_FAILED_WITH_EXCEPTION = new Errno(TYPE, 159, "Validating file existence and permissions of %1$s at path \"%2$s\" failed.\nException: %3$s");
public static final Errno ERRNO_VALIDATE_DIRECTORY_EXISTENCE_AND_PERMISSIONS_FAILED_WITH_EXCEPTION = new Errno(TYPE, 160, "Validating directory existence and permissions of %1$s at path \"%2$s\" failed.\nException: %3$s");
diff --git a/termux-shared/src/main/java/com/termux/shared/file/filesystem/FileAttributes.java b/termux-shared/src/main/java/com/termux/shared/file/filesystem/FileAttributes.java
index 5e847a1e9e..4565acc3da 100644
--- a/termux-shared/src/main/java/com/termux/shared/file/filesystem/FileAttributes.java
+++ b/termux-shared/src/main/java/com/termux/shared/file/filesystem/FileAttributes.java
@@ -202,6 +202,10 @@ public boolean isFifo() {
return ((st_mode & UnixConstants.S_IFMT) == UnixConstants.S_IFIFO);
}
+ public boolean isSocket() {
+ return ((st_mode & UnixConstants.S_IFMT) == UnixConstants.S_IFSOCK);
+ }
+
public boolean isBlock() {
return ((st_mode & UnixConstants.S_IFMT) == UnixConstants.S_IFBLK);
}
diff --git a/termux-shared/src/main/java/com/termux/shared/file/filesystem/FileType.java b/termux-shared/src/main/java/com/termux/shared/file/filesystem/FileType.java
index 00ed06d62b..ff9418fc47 100644
--- a/termux-shared/src/main/java/com/termux/shared/file/filesystem/FileType.java
+++ b/termux-shared/src/main/java/com/termux/shared/file/filesystem/FileType.java
@@ -3,14 +3,15 @@
/** The {@link Enum} that defines file types. */
public enum FileType {
- NO_EXIST("no exist", 0), // 0000000
- REGULAR("regular", 1), // 0000001
- DIRECTORY("directory", 2), // 0000010
- SYMLINK("symlink", 4), // 0000100
- CHARACTER("character", 8), // 0001000
- FIFO("fifo", 16), // 0010000
- BLOCK("block", 32), // 0100000
- UNKNOWN("unknown", 64); // 1000000
+ NO_EXIST("no exist", 0), // 00000000
+ REGULAR("regular", 1), // 00000001
+ DIRECTORY("directory", 2), // 00000010
+ SYMLINK("symlink", 4), // 00000100
+ SOCKET("socket", 8), // 00001000
+ CHARACTER("character", 16), // 00010000
+ FIFO("fifo", 32), // 00100000
+ BLOCK("block", 64), // 01000000
+ UNKNOWN("unknown", 128); // 10000000
private final String name;
private final int value;
diff --git a/termux-shared/src/main/java/com/termux/shared/file/filesystem/FileTypes.java b/termux-shared/src/main/java/com/termux/shared/file/filesystem/FileTypes.java
index c739392d97..29f2b294de 100644
--- a/termux-shared/src/main/java/com/termux/shared/file/filesystem/FileTypes.java
+++ b/termux-shared/src/main/java/com/termux/shared/file/filesystem/FileTypes.java
@@ -104,6 +104,8 @@ else if (fileAttributes.isDirectory())
return FileType.DIRECTORY;
else if (fileAttributes.isSymbolicLink())
return FileType.SYMLINK;
+ else if (fileAttributes.isSocket())
+ return FileType.SOCKET;
else if (fileAttributes.isCharacter())
return FileType.CHARACTER;
else if (fileAttributes.isFifo())
diff --git a/termux-shared/src/main/java/com/termux/shared/file/filesystem/UnixConstants.java b/termux-shared/src/main/java/com/termux/shared/file/filesystem/UnixConstants.java
index 39e84eb674..72e7dc2ae9 100644
--- a/termux-shared/src/main/java/com/termux/shared/file/filesystem/UnixConstants.java
+++ b/termux-shared/src/main/java/com/termux/shared/file/filesystem/UnixConstants.java
@@ -88,6 +88,8 @@ private UnixConstants() { }
static final int S_IFLNK = OsConstants.S_IFLNK;
+ static final int S_IFSOCK = OsConstants.S_IFSOCK;
+
static final int S_IFCHR = OsConstants.S_IFCHR;
static final int S_IFBLK = OsConstants.S_IFBLK;
diff --git a/termux-shared/src/main/java/com/termux/shared/jni/models/JniResult.java b/termux-shared/src/main/java/com/termux/shared/jni/models/JniResult.java
new file mode 100644
index 0000000000..3621f9f3a6
--- /dev/null
+++ b/termux-shared/src/main/java/com/termux/shared/jni/models/JniResult.java
@@ -0,0 +1,109 @@
+package com.termux.shared.jni.models;
+
+import androidx.annotation.Keep;
+import androidx.annotation.NonNull;
+
+import com.termux.shared.logger.Logger;
+
+/**
+ * A class that can be used to return result for JNI calls with support for multiple fields to easily
+ * return success and error states.
+ *
+ * https://docs.oracle.com/javase/7/docs/technotes/guides/jni/spec/functions.html
+ * https://developer.android.com/training/articles/perf-jni
+ */
+@Keep
+public class JniResult {
+
+ /**
+ * The return value for the JNI call.
+ * This should be 0 for success.
+ */
+ public int retval;
+
+ /**
+ * The errno value for any failed native system or library calls if {@link #retval} does not equal 0.
+ * This should be 0 if no errno was set.
+ *
+ * https://manpages.debian.org/testing/manpages-dev/errno.3.en.html
+ */
+ public int errno;
+
+ /**
+ * The error message for the failure if {@link #retval} does not equal 0.
+ * The message will contain errno message returned by strerror() if errno was set.
+ *
+ * https://manpages.debian.org/testing/manpages-dev/strerror.3.en.html
+ */
+ public String errmsg;
+
+ /**
+ * Optional additional int data that needs to be returned by JNI call, like bytes read on success.
+ */
+ public int intData;
+
+ /**
+ * Create an new instance of {@link JniResult}.
+ *
+ * @param retval The {@link #retval} value.
+ * @param errno The {@link #errno} value.
+ * @param errmsg The {@link #errmsg} value.
+ */
+ public JniResult(int retval, int errno, String errmsg) {
+ this.retval = retval;
+ this.errno = errno;
+ this.errmsg = errmsg;
+ }
+
+ /**
+ * Create an new instance of {@link JniResult}.
+ *
+ * @param retval The {@link #retval} value.
+ * @param errno The {@link #errno} value.
+ * @param errmsg The {@link #errmsg} value.
+ * @param intData The {@link #intData} value.
+ */
+ public JniResult(int retval, int errno, String errmsg, int intData) {
+ this(retval, errno, errmsg);
+ this.intData = intData;
+ }
+
+ /**
+ * Create an new instance of {@link JniResult} from a {@link Throwable} with {@link #retval} -1.
+ *
+ * @param message The error message.
+ * @param throwable The {@link Throwable} value.
+ */
+ public JniResult(String message, Throwable throwable) {
+ this(-1, 0, Logger.getMessageAndStackTraceString(message, throwable));
+ }
+
+ /**
+ * Get error {@link String} for {@link JniResult}.
+ *
+ * @param result The {@link JniResult} to get error from.
+ * @return Returns the error {@link String}.
+ */
+ @NonNull
+ public static String getErrorString(final JniResult result) {
+ if (result == null) return "null";
+ return result.getErrorString();
+ }
+
+ /** Get error {@link String} for {@link JniResult}. */
+ @NonNull
+ public String getErrorString() {
+ StringBuilder logString = new StringBuilder();
+
+ logString.append(Logger.getSingleLineLogStringEntry("Retval", retval, "-"));
+
+ if (errno != 0)
+ logString.append("\n").append(Logger.getSingleLineLogStringEntry("Errno", errno, "-"));
+
+ if (errmsg != null && !errmsg.isEmpty())
+ logString.append("\n").append(Logger.getMultiLineLogStringEntry("Errmsg", errmsg, "-"));
+
+ return logString.toString();
+ }
+
+}
diff --git a/termux-shared/src/main/java/com/termux/shared/logger/Logger.java b/termux-shared/src/main/java/com/termux/shared/logger/Logger.java
index e2573a8a3e..e5075b1f91 100644
--- a/termux-shared/src/main/java/com/termux/shared/logger/Logger.java
+++ b/termux-shared/src/main/java/com/termux/shared/logger/Logger.java
@@ -120,6 +120,28 @@ public static void logErrorExtended(String message) {
+ public static void logErrorPrivate(String tag, String message) {
+ if (CURRENT_LOG_LEVEL >= LOG_LEVEL_DEBUG)
+ logMessage(Log.ERROR, tag, message);
+ }
+
+ public static void logErrorPrivate(String message) {
+ if (CURRENT_LOG_LEVEL >= LOG_LEVEL_DEBUG)
+ logMessage(Log.ERROR, DEFAULT_LOG_TAG, message);
+ }
+
+ public static void logErrorPrivateExtended(String tag, String message) {
+ if (CURRENT_LOG_LEVEL >= LOG_LEVEL_DEBUG)
+ logExtendedMessage(Log.ERROR, tag, message);
+ }
+
+ public static void logErrorPrivateExtended(String message) {
+ if (CURRENT_LOG_LEVEL >= LOG_LEVEL_DEBUG)
+ logExtendedMessage(Log.ERROR, DEFAULT_LOG_TAG, message);
+ }
+
+
+
public static void logWarn(String tag, String message) {
logMessage(Log.WARN, tag, message);
}
diff --git a/termux-shared/src/main/java/com/termux/shared/net/socket/local/ILocalSocketManager.java b/termux-shared/src/main/java/com/termux/shared/net/socket/local/ILocalSocketManager.java
new file mode 100644
index 0000000000..a520254ed1
--- /dev/null
+++ b/termux-shared/src/main/java/com/termux/shared/net/socket/local/ILocalSocketManager.java
@@ -0,0 +1,72 @@
+package com.termux.shared.net.socket.local;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.termux.shared.errors.Error;
+
+/**
+ * The interface for the {@link LocalSocketManager} for callbacks to manager client/server starter.
+ */
+public interface ILocalSocketManager {
+
+ /**
+ * This should return the {@link Thread.UncaughtExceptionHandler} that should be used for the
+ * client socket listener and client logic runner threads started for other interface methods.
+ *
+ * @param localSocketManager The {@link LocalSocketManager} for the server.
+ * @return Should return {@link Thread.UncaughtExceptionHandler} or {@code null}, if default
+ * handler should be used which just logs the exception.
+ */
+ @Nullable
+ Thread.UncaughtExceptionHandler getLocalSocketManagerClientThreadUEH(
+ @NonNull LocalSocketManager localSocketManager);
+
+ /**
+ * This is called if any error is raised by {@link LocalSocketManager}, {@link LocalServerSocket}
+ * or {@link LocalClientSocket}. The server will automatically close the client socket
+ * with a call to {@link LocalClientSocket#closeClientSocket(boolean)} if the error occurred due
+ * to the client.
+ *
+ * The {@link LocalClientSocket#getPeerCred()} can be used to get the {@link PeerCred} object
+ * containing info for the connected client/peer.
+ *
+ * @param localSocketManager The {@link LocalSocketManager} for the server.
+ * @param clientSocket The {@link LocalClientSocket} that connected. This will be {@code null}
+ * if error is not for a {@link LocalClientSocket}.
+ * @param error The {@link Error} auto generated that can be used for logging purposes.
+ */
+ void onError(@NonNull LocalSocketManager localSocketManager,
+ @Nullable LocalClientSocket clientSocket, @NonNull Error error);
+
+ /**
+ * This is called if a {@link LocalServerSocket} connects to the server which **does not** have
+ * the server app's user id or root user id. The server will automatically close the client socket
+ * with a call to {@link LocalClientSocket#closeClientSocket(boolean)}.
+ *
+ * The {@link LocalClientSocket#getPeerCred()} can be used to get the {@link PeerCred} object
+ * containing info for the connected client/peer.
+ *
+ * @param localSocketManager The {@link LocalSocketManager} for the server.
+ * @param clientSocket The {@link LocalClientSocket} that connected.
+ * @param error The {@link Error} auto generated that can be used for logging purposes.
+ */
+ void onDisallowedClientConnected(@NonNull LocalSocketManager localSocketManager,
+ @NonNull LocalClientSocket clientSocket, @NonNull Error error);
+
+ /**
+ * This is called if a {@link LocalServerSocket} connects to the server which has the
+ * the server app's user id or root user id. It is the responsibility of the interface
+ * implementation to close the client socket with a call to
+ * {@link LocalClientSocket#closeClientSocket(boolean)} once its done processing.
+ *
+ * The {@link LocalClientSocket#getPeerCred()} can be used to get the {@link PeerCred} object
+ * containing info for the connected client/peer.
+ *
+ * @param localSocketManager The {@link LocalSocketManager} for the server.
+ * @param clientSocket The {@link LocalClientSocket} that connected.
+ */
+ void onClientAccepted(@NonNull LocalSocketManager localSocketManager,
+ @NonNull LocalClientSocket clientSocket);
+
+}
diff --git a/termux-shared/src/main/java/com/termux/shared/net/socket/local/LocalClientSocket.java b/termux-shared/src/main/java/com/termux/shared/net/socket/local/LocalClientSocket.java
new file mode 100644
index 0000000000..75a7e6a8c3
--- /dev/null
+++ b/termux-shared/src/main/java/com/termux/shared/net/socket/local/LocalClientSocket.java
@@ -0,0 +1,483 @@
+package com.termux.shared.net.socket.local;
+
+import androidx.annotation.NonNull;
+
+import com.termux.shared.data.DataUtils;
+import com.termux.shared.errors.Error;
+import com.termux.shared.jni.models.JniResult;
+import com.termux.shared.logger.Logger;
+import com.termux.shared.markdown.MarkdownUtils;
+
+import java.io.BufferedWriter;
+import java.io.Closeable;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+
+/** The client socket for {@link LocalSocketManager}. */
+public class LocalClientSocket implements Closeable {
+
+ public static final String LOG_TAG = "LocalClientSocket";
+
+ /** The {@link LocalSocketManager} instance for the local socket. */
+ @NonNull protected final LocalSocketManager mLocalSocketManager;
+
+ /** The {@link LocalSocketRunConfig} containing run config for the {@link LocalClientSocket}. */
+ @NonNull protected final LocalSocketRunConfig mLocalSocketRunConfig;
+
+ /**
+ * The {@link LocalClientSocket} file descriptor.
+ * Value will be `>= 0` if socket has been connected and `-1` if closed.
+ */
+ protected int mFD;
+
+ /** The creation time of {@link LocalClientSocket}. This is also used for deadline. */
+ protected final long mCreationTime;
+
+ /** The {@link PeerCred} of the {@link LocalClientSocket} containing info of client/peer. */
+ @NonNull protected final PeerCred mPeerCred;
+
+ /** The {@link OutputStream} implementation for the {@link LocalClientSocket}. */
+ @NonNull protected final SocketOutputStream mOutputStream;
+
+ /** The {@link InputStream} implementation for the {@link LocalClientSocket}. */
+ @NonNull protected final SocketInputStream mInputStream;
+
+ /**
+ * Create an new instance of {@link LocalClientSocket}.
+ *
+ * @param localSocketManager The {@link #mLocalSocketManager} value.
+ * @param fd The {@link #mFD} value.
+ * @param peerCred The {@link #mPeerCred} value.
+ */
+ LocalClientSocket(@NonNull LocalSocketManager localSocketManager, int fd, @NonNull PeerCred peerCred) {
+ mLocalSocketManager = localSocketManager;
+ mLocalSocketRunConfig = localSocketManager.getLocalSocketRunConfig();
+ mCreationTime = System.currentTimeMillis();
+ mOutputStream = new SocketOutputStream();
+ mInputStream = new SocketInputStream();
+ mPeerCred = peerCred;
+
+ setFD(fd);
+ mPeerCred.fillPeerCred(localSocketManager.getContext());
+ }
+
+
+ /** Close client socket. */
+ public synchronized Error closeClientSocket(boolean logErrorMessage) {
+ try {
+ close();
+ } catch (IOException e) {
+ Error error = LocalSocketErrno.ERRNO_CLOSE_CLIENT_SOCKET_FAILED_WITH_EXCEPTION.getError(e, mLocalSocketRunConfig.getTitle(), e.getMessage());
+ if (logErrorMessage)
+ Logger.logErrorExtended(LOG_TAG, error.getErrorLogString());
+ return error;
+ }
+
+ return null;
+ }
+
+ /** Close client socket that exists at fd. */
+ public static void closeClientSocket(@NonNull LocalSocketManager localSocketManager, int fd) {
+ new LocalClientSocket(localSocketManager, fd, new PeerCred()).closeClientSocket(true);
+ }
+
+ /** Implementation for {@link Closeable#close()} to close client socket. */
+ @Override
+ public void close() throws IOException {
+ if (mFD >= 0) {
+ Logger.logVerbose(LOG_TAG, "Client socket close for \"" + mLocalSocketRunConfig.getTitle() + "\" server: " + getPeerCred().getMinimalString());
+ JniResult result = LocalSocketManager.closeSocket(mLocalSocketRunConfig.getLogTitle() + " (client)", mFD);
+ if (result == null || result.retval != 0) {
+ throw new IOException(JniResult.getErrorString(result));
+ }
+ // Update fd to signify that client socket has been closed
+ setFD(-1);
+ }
+ }
+
+
+ /**
+ * Attempts to read up to data buffer length bytes from file descriptor into the data buffer.
+ * On success, the number of bytes read is returned (zero indicates end of file) in bytesRead.
+ * It is not an error if bytesRead is smaller than the number of bytes requested; this may happen
+ * for example because fewer bytes are actually available right now (maybe because we were close
+ * to end-of-file, or because we are reading from a pipe), or because read() was interrupted by
+ * a signal.
+ *
+ * If while reading the {@link #mCreationTime} + the milliseconds returned by
+ * {@link LocalSocketRunConfig#getDeadline()} elapses but all the data has not been read, an
+ * error would be returned.
+ *
+ * This is a wrapper for {@link LocalSocketManager#read(String, int, byte[], long)}, which can
+ * be called instead if you want to get access to errno int value instead of {@link JniResult}
+ * error {@link String}.
+ *
+ * @param data The data buffer to read bytes into.
+ * @param bytesRead The actual bytes read.
+ * @return Returns the {@code error} if reading was not successful containing {@link JniResult}
+ * error {@link String}, otherwise {@code null}.
+ */
+ public Error read(@NonNull byte[] data, MutableInt bytesRead) {
+ bytesRead.value = 0;
+
+ if (mFD < 0) {
+ return LocalSocketErrno.ERRNO_USING_CLIENT_SOCKET_WITH_INVALID_FD.getError(mFD,
+ mLocalSocketRunConfig.getTitle());
+ }
+
+ JniResult result = LocalSocketManager.read(mLocalSocketRunConfig.getLogTitle() + " (client)",
+ mFD, data,
+ mLocalSocketRunConfig.getDeadline() > 0 ? mCreationTime + mLocalSocketRunConfig.getDeadline() : 0);
+ if (result == null || result.retval != 0) {
+ return LocalSocketErrno.ERRNO_READ_DATA_FROM_CLIENT_SOCKET_FAILED.getError(
+ mLocalSocketRunConfig.getTitle(), JniResult.getErrorString(result));
+ }
+
+ bytesRead.value = result.intData;
+ return null;
+ }
+
+ /**
+ * Attempts to send data buffer to the file descriptor.
+ *
+ * If while sending the {@link #mCreationTime} + the milliseconds returned by
+ * {@link LocalSocketRunConfig#getDeadline()} elapses but all the data has not been sent, an
+ * error would be returned.
+ *
+ * This is a wrapper for {@link LocalSocketManager#send(String, int, byte[], long)}, which can
+ * be called instead if you want to get access to errno int value instead of {@link JniResult}
+ * error {@link String}.
+ *
+ * @param data The data buffer containing bytes to send.
+ * @return Returns the {@code error} if sending was not successful containing {@link JniResult}
+ * error {@link String}, otherwise {@code null}.
+ */
+ public Error send(@NonNull byte[] data) {
+ if (mFD < 0) {
+ return LocalSocketErrno.ERRNO_USING_CLIENT_SOCKET_WITH_INVALID_FD.getError(mFD,
+ mLocalSocketRunConfig.getTitle());
+ }
+
+ JniResult result = LocalSocketManager.send(mLocalSocketRunConfig.getLogTitle() + " (client)",
+ mFD, data,
+ mLocalSocketRunConfig.getDeadline() > 0 ? mCreationTime + mLocalSocketRunConfig.getDeadline() : 0);
+ if (result == null || result.retval != 0) {
+ return LocalSocketErrno.ERRNO_SEND_DATA_TO_CLIENT_SOCKET_FAILED.getError(
+ mLocalSocketRunConfig.getTitle(), JniResult.getErrorString(result));
+ }
+
+ return null;
+ }
+
+ /**
+ * Attempts to read all the bytes available on {@link SocketInputStream} and appends them to
+ * {@code data} {@link StringBuilder}.
+ *
+ * This is a wrapper for {@link #read(byte[], MutableInt)} called via {@link SocketInputStream#read()}.
+ *
+ * @param data The data {@link StringBuilder} to append the bytes read into.
+ * @param closeStreamOnFinish If set to {@code true}, then underlying input stream will closed
+ * and further attempts to read from socket will fail.
+ * @return Returns the {@code error} if reading was not successful containing {@link JniResult}
+ * error {@link String}, otherwise {@code null}.
+ */
+ public Error readDataOnInputStream(@NonNull StringBuilder data, boolean closeStreamOnFinish) {
+ int c;
+ InputStreamReader inputStreamReader = getInputStreamReader();
+ try {
+ while ((c = inputStreamReader.read()) > 0) {
+ data.append((char) c);
+ }
+ } catch (IOException e) {
+ // The SocketInputStream.read() throws the Error message in an IOException,
+ // so just read the exception message and not the stack trace, otherwise it would result
+ // in a messy nested error message.
+ return LocalSocketErrno.ERRNO_READ_DATA_FROM_INPUT_STREAM_OF_CLIENT_SOCKET_FAILED_WITH_EXCEPTION.getError(
+ mLocalSocketRunConfig.getTitle(), DataUtils.getSpaceIndentedString(e.getMessage(), 1));
+ } catch (Exception e) {
+ return LocalSocketErrno.ERRNO_READ_DATA_FROM_INPUT_STREAM_OF_CLIENT_SOCKET_FAILED_WITH_EXCEPTION.getError(
+ e, mLocalSocketRunConfig.getTitle(), e.getMessage());
+ } finally {
+ if (closeStreamOnFinish) {
+ try { inputStreamReader.close();
+ } catch (IOException e) {
+ // Ignore
+ }
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Attempts to send all the bytes passed to {@link SocketOutputStream} .
+ *
+ * This is a wrapper for {@link #send(byte[])} called via {@link SocketOutputStream#write(int)}.
+ *
+ * @param data The {@link String} bytes to send.
+ * @param closeStreamOnFinish If set to {@code true}, then underlying output stream will closed
+ * and further attempts to send to socket will fail.
+ * @return Returns the {@code error} if sending was not successful containing {@link JniResult}
+ * error {@link String}, otherwise {@code null}.
+ */
+ public Error sendDataToOutputStream(@NonNull String data, boolean closeStreamOnFinish) {
+
+ OutputStreamWriter outputStreamWriter = getOutputStreamWriter();
+
+ try (BufferedWriter byteStreamWriter = new BufferedWriter(outputStreamWriter)) {
+ byteStreamWriter.write(data);
+ byteStreamWriter.flush();
+ } catch (IOException e) {
+ // The SocketOutputStream.write() throws the Error message in an IOException,
+ // so just read the exception message and not the stack trace, otherwise it would result
+ // in a messy nested error message.
+ return LocalSocketErrno.ERRNO_SEND_DATA_TO_OUTPUT_STREAM_OF_CLIENT_SOCKET_FAILED_WITH_EXCEPTION.getError(
+ mLocalSocketRunConfig.getTitle(), DataUtils.getSpaceIndentedString(e.getMessage(), 1));
+ } catch (Exception e) {
+ return LocalSocketErrno.ERRNO_SEND_DATA_TO_OUTPUT_STREAM_OF_CLIENT_SOCKET_FAILED_WITH_EXCEPTION.getError(
+ e, mLocalSocketRunConfig.getTitle(), e.getMessage());
+ } finally {
+ if (closeStreamOnFinish) {
+ try {
+ outputStreamWriter.close();
+ } catch (IOException e) {
+ // Ignore
+ }
+ }
+ }
+
+ return null;
+ }
+
+ /** Wrapper for {@link #available(MutableInt, boolean)} that checks deadline. The
+ * {@link SocketInputStream} calls this. */
+ public Error available(MutableInt available) {
+ return available(available, true);
+ }
+
+ /**
+ * Get available bytes on {@link #mInputStream} and optionally check if value returned by
+ * {@link LocalSocketRunConfig#getDeadline()} has passed.
+ */
+ public Error available(MutableInt available, boolean checkDeadline) {
+ available.value = 0;
+
+ if (mFD < 0) {
+ return LocalSocketErrno.ERRNO_USING_CLIENT_SOCKET_WITH_INVALID_FD.getError(mFD,
+ mLocalSocketRunConfig.getTitle());
+ }
+
+ if (checkDeadline && mLocalSocketRunConfig.getDeadline() > 0 && System.currentTimeMillis() > (mCreationTime + mLocalSocketRunConfig.getDeadline())) {
+ return null;
+ }
+
+ JniResult result = LocalSocketManager.available(mLocalSocketRunConfig.getLogTitle() + " (client)", mLocalSocketRunConfig.getFD());
+ if (result == null || result.retval != 0) {
+ return LocalSocketErrno.ERRNO_CHECK_AVAILABLE_DATA_ON_CLIENT_SOCKET_FAILED.getError(
+ mLocalSocketRunConfig.getTitle(), JniResult.getErrorString(result));
+ }
+
+ available.value = result.intData;
+ return null;
+ }
+
+
+
+ /** Set {@link LocalClientSocket} receiving (SO_RCVTIMEO) timeout to value returned by {@link LocalSocketRunConfig#getReceiveTimeout()}. */
+ public Error setReadTimeout() {
+ if (mFD >= 0) {
+ JniResult result = LocalSocketManager.setSocketReadTimeout(mLocalSocketRunConfig.getLogTitle() + " (client)",
+ mFD, mLocalSocketRunConfig.getReceiveTimeout());
+ if (result == null || result.retval != 0) {
+ return LocalSocketErrno.ERRNO_SET_CLIENT_SOCKET_READ_TIMEOUT_FAILED.getError(
+ mLocalSocketRunConfig.getTitle(), mLocalSocketRunConfig.getReceiveTimeout(), JniResult.getErrorString(result));
+ }
+ }
+ return null;
+ }
+
+ /** Set {@link LocalClientSocket} sending (SO_SNDTIMEO) timeout to value returned by {@link LocalSocketRunConfig#getSendTimeout()}. */
+ public Error setWriteTimeout() {
+ if (mFD >= 0) {
+ JniResult result = LocalSocketManager.setSocketSendTimeout(mLocalSocketRunConfig.getLogTitle() + " (client)",
+ mFD, mLocalSocketRunConfig.getSendTimeout());
+ if (result == null || result.retval != 0) {
+ return LocalSocketErrno.ERRNO_SET_CLIENT_SOCKET_SEND_TIMEOUT_FAILED.getError(
+ mLocalSocketRunConfig.getTitle(), mLocalSocketRunConfig.getSendTimeout(), JniResult.getErrorString(result));
+ }
+ }
+ return null;
+ }
+
+
+
+ /** Get {@link #mFD} for the client socket. */
+ public int getFD() {
+ return mFD;
+ }
+
+ /** Set {@link #mFD}. Value must be greater than 0 or -1. */
+ private void setFD(int fd) {
+ if (fd >= 0)
+ mFD = fd;
+ else
+ mFD = -1;
+ }
+
+ /** Get {@link #mPeerCred} for the client socket. */
+ public PeerCred getPeerCred() {
+ return mPeerCred;
+ }
+
+ /** Get {@link #mCreationTime} for the client socket. */
+ public long getCreationTime() {
+ return mCreationTime;
+ }
+
+ /** Get {@link #mOutputStream} for the client socket. The stream will automatically close when client socket is closed. */
+ public OutputStream getOutputStream() {
+ return mOutputStream;
+ }
+
+ /** Get {@link OutputStreamWriter} for {@link #mOutputStream} for the client socket. The stream will automatically close when client socket is closed. */
+ @NonNull
+ public OutputStreamWriter getOutputStreamWriter() {
+ return new OutputStreamWriter(getOutputStream());
+ }
+
+ /** Get {@link #mInputStream} for the client socket. The stream will automatically close when client socket is closed. */
+ public InputStream getInputStream() {
+ return mInputStream;
+ }
+
+ /** Get {@link InputStreamReader} for {@link #mInputStream} for the client socket. The stream will automatically close when client socket is closed. */
+ @NonNull
+ public InputStreamReader getInputStreamReader() {
+ return new InputStreamReader(getInputStream());
+ }
+
+
+
+ /** Get a log {@link String} for the {@link LocalClientSocket}. */
+ @NonNull
+ public String getLogString() {
+ StringBuilder logString = new StringBuilder();
+
+ logString.append("Client Socket:");
+ logString.append("\n").append(Logger.getSingleLineLogStringEntry("FD", mFD, "-"));
+ logString.append("\n").append(Logger.getSingleLineLogStringEntry("Creation Time", mCreationTime, "-"));
+ logString.append("\n\n\n");
+
+ logString.append(mPeerCred.getLogString());
+
+ return logString.toString();
+ }
+
+ /** Get a markdown {@link String} for the {@link LocalClientSocket}. */
+ @NonNull
+ public String getMarkdownString() {
+ StringBuilder markdownString = new StringBuilder();
+
+ markdownString.append("## ").append("Client Socket");
+ markdownString.append("\n").append(MarkdownUtils.getSingleLineMarkdownStringEntry("FD", mFD, "-"));
+ markdownString.append("\n").append(MarkdownUtils.getSingleLineMarkdownStringEntry("Creation Time", mCreationTime, "-"));
+ markdownString.append("\n\n\n");
+
+ markdownString.append(mPeerCred.getMarkdownString());
+
+ return markdownString.toString();
+ }
+
+
+
+
+
+ /** Wrapper class to allow pass by reference of int values. */
+ public static final class MutableInt {
+ public int value;
+
+ public MutableInt(int value) {
+ this.value = value;
+ }
+ }
+
+
+
+ /** The {@link InputStream} implementation for the {@link LocalClientSocket}. */
+ protected class SocketInputStream extends InputStream {
+ private final byte[] mBytes = new byte[1];
+
+ @Override
+ public int read() throws IOException {
+ MutableInt bytesRead = new MutableInt(0);
+ Error error = LocalClientSocket.this.read(mBytes, bytesRead);
+ if (error != null) {
+ throw new IOException(error.getErrorMarkdownString());
+ }
+
+ if (bytesRead.value == 0) {
+ return -1;
+ }
+
+ return mBytes[0];
+ }
+
+ @Override
+ public int read(byte[] bytes) throws IOException {
+ if (bytes == null) {
+ throw new NullPointerException("Read buffer can't be null");
+ }
+
+ MutableInt bytesRead = new MutableInt(0);
+ Error error = LocalClientSocket.this.read(bytes, bytesRead);
+ if (error != null) {
+ throw new IOException(error.getErrorMarkdownString());
+ }
+
+ if (bytesRead.value == 0) {
+ return -1;
+ }
+
+ return bytesRead.value;
+ }
+
+ @Override
+ public int available() throws IOException {
+ MutableInt available = new MutableInt(0);
+ Error error = LocalClientSocket.this.available(available);
+ if (error != null) {
+ throw new IOException(error.getErrorMarkdownString());
+ }
+ return available.value;
+ }
+ }
+
+
+
+ /** The {@link OutputStream} implementation for the {@link LocalClientSocket}. */
+ protected class SocketOutputStream extends OutputStream {
+ private final byte[] mBytes = new byte[1];
+
+ @Override
+ public void write(int b) throws IOException {
+ mBytes[0] = (byte) b;
+
+ Error error = LocalClientSocket.this.send(mBytes);
+ if (error != null) {
+ throw new IOException(error.getErrorMarkdownString());
+ }
+ }
+
+ @Override
+ public void write(byte[] bytes) throws IOException {
+ Error error = LocalClientSocket.this.send(bytes);
+ if (error != null) {
+ throw new IOException(error.getErrorMarkdownString());
+ }
+ }
+ }
+
+}
diff --git a/termux-shared/src/main/java/com/termux/shared/net/socket/local/LocalServerSocket.java b/termux-shared/src/main/java/com/termux/shared/net/socket/local/LocalServerSocket.java
new file mode 100644
index 0000000000..385c203465
--- /dev/null
+++ b/termux-shared/src/main/java/com/termux/shared/net/socket/local/LocalServerSocket.java
@@ -0,0 +1,303 @@
+package com.termux.shared.net.socket.local;
+
+import androidx.annotation.NonNull;
+
+import com.termux.shared.errors.Error;
+import com.termux.shared.file.FileUtils;
+import com.termux.shared.jni.models.JniResult;
+import com.termux.shared.logger.Logger;
+
+import java.io.Closeable;
+import java.io.File;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+
+/** The server socket for {@link LocalSocketManager}. */
+public class LocalServerSocket implements Closeable {
+
+ public static final String LOG_TAG = "LocalServerSocket";
+
+ /** The {@link LocalSocketManager} instance for the local socket. */
+ @NonNull protected final LocalSocketManager mLocalSocketManager;
+
+ /** The {@link LocalSocketRunConfig} containing run config for the {@link LocalServerSocket}. */
+ @NonNull protected final LocalSocketRunConfig mLocalSocketRunConfig;
+
+ /** The {@link ILocalSocketManager} client for the {@link LocalSocketManager}. */
+ @NonNull protected final ILocalSocketManager mLocalSocketManagerClient;
+
+ /** The {@link ClientSocketListener} {@link Thread} for the {@link LocalServerSocket}. */
+ @NonNull protected final Thread mClientSocketListener;
+
+ /**
+ * The required permissions for server socket file parent directory.
+ * Creation of a new socket will fail if the server starter app process does not have
+ * write and search (execute) permission on the directory in which the socket is created.
+ */
+ public static final String SERVER_SOCKET_PARENT_DIRECTORY_PERMISSIONS = "rwx"; // Default: "rwx"
+
+ /**
+ * Create an new instance of {@link LocalServerSocket}.
+ *
+ * @param localSocketManager The {@link #mLocalSocketManager} value.
+ */
+ protected LocalServerSocket(@NonNull LocalSocketManager localSocketManager) {
+ mLocalSocketManager = localSocketManager;
+ mLocalSocketRunConfig = localSocketManager.getLocalSocketRunConfig();
+ mLocalSocketManagerClient = mLocalSocketRunConfig.getLocalSocketManagerClient();
+ mClientSocketListener = new Thread(new ClientSocketListener());
+ }
+
+ /** Start server by creating server socket. */
+ public synchronized Error start() {
+ Logger.logDebug(LOG_TAG, "start");
+
+ String path = mLocalSocketRunConfig.getPath();
+ if (path == null || path.isEmpty()) {
+ return LocalSocketErrno.ERRNO_SERVER_SOCKET_PATH_NULL_OR_EMPTY.getError(mLocalSocketRunConfig.getTitle());
+ }
+ if (!mLocalSocketRunConfig.isAbstractNamespaceSocket()) {
+ path = FileUtils.getCanonicalPath(path, null);
+ }
+
+ // On Linux, sun_path is 108 bytes (UNIX_PATH_MAX) in size, so do an early check here to
+ // prevent useless parent directory creation since createServerSocket() call will fail since
+ // there is a native check as well.
+ if (path.getBytes(StandardCharsets.UTF_8).length > 108) {
+ return LocalSocketErrno.ERRNO_SERVER_SOCKET_PATH_TOO_LONG.getError(mLocalSocketRunConfig.getTitle(), path);
+ }
+
+ int backlog = mLocalSocketRunConfig.getBacklog();
+ if (backlog <= 0) {
+ return LocalSocketErrno.ERRNO_SERVER_SOCKET_BACKLOG_INVALID.getError(mLocalSocketRunConfig.getTitle(), backlog);
+ }
+
+ Error error;
+
+ // If server socket is not in abstract namespace
+ if (!mLocalSocketRunConfig.isAbstractNamespaceSocket()) {
+ if (!path.startsWith("/"))
+ return LocalSocketErrno.ERRNO_SERVER_SOCKET_PATH_NOT_ABSOLUTE.getError(mLocalSocketRunConfig.getTitle(), path);
+
+ // Create the server socket file parent directory and set SERVER_SOCKET_PARENT_DIRECTORY_PERMISSIONS if missing
+ String socketParentPath = new File(path).getParent();
+ error = FileUtils.validateDirectoryFileExistenceAndPermissions(mLocalSocketRunConfig.getTitle() + " server socket file parent",
+ socketParentPath,
+ null, true,
+ SERVER_SOCKET_PARENT_DIRECTORY_PERMISSIONS, true, true,
+ false, false);
+ if (error != null)
+ return error;
+
+
+ // Delete the server socket file to stop any existing servers and for bind() to succeed
+ error = deleteServerSocketFile();
+ if (error != null)
+ return error;
+ }
+
+ // Create the server socket
+ JniResult result = LocalSocketManager.createServerSocket(mLocalSocketRunConfig.getLogTitle() + " (server)",
+ path.getBytes(StandardCharsets.UTF_8), backlog);
+ if (result == null || result.retval != 0) {
+ return LocalSocketErrno.ERRNO_CREATE_SERVER_SOCKET_FAILED.getError(mLocalSocketRunConfig.getTitle(), JniResult.getErrorString(result));
+ }
+
+ int fd = result.intData;
+ if (fd < 0) {
+ return LocalSocketErrno.ERRNO_SERVER_SOCKET_FD_INVALID.getError(fd, mLocalSocketRunConfig.getTitle());
+ }
+
+ // Update fd to signify that server socket has been created successfully
+ mLocalSocketRunConfig.setFD(fd);
+
+ mClientSocketListener.setUncaughtExceptionHandler(mLocalSocketManager.getLocalSocketManagerClientThreadUEH());
+
+ try {
+ // Start listening to server clients
+ mClientSocketListener.start();
+ } catch (Exception e) {
+ Logger.logStackTraceWithMessage(LOG_TAG, "mClientSocketListener start failed", e);
+ }
+
+ return null;
+ }
+
+ /** Stop server. */
+ public synchronized Error stop() {
+ Logger.logDebug(LOG_TAG, "stop");
+
+ try {
+ // Stop the LocalClientSocket listener.
+ mClientSocketListener.interrupt();
+ } catch (Exception ignored) {}
+
+ Error error = closeServerSocket(false);
+ if (error != null)
+ return error;
+
+ return deleteServerSocketFile();
+ }
+
+ /** Close server socket. */
+ public synchronized Error closeServerSocket(boolean logErrorMessage) {
+ Logger.logDebug(LOG_TAG, "closeServerSocket");
+
+ try {
+ close();
+ } catch (IOException e) {
+ Error error = LocalSocketErrno.ERRNO_CLOSE_SERVER_SOCKET_FAILED_WITH_EXCEPTION.getError(e, mLocalSocketRunConfig.getTitle(), e.getMessage());
+ if (logErrorMessage)
+ Logger.logErrorExtended(LOG_TAG, error.getErrorLogString());
+ return error;
+ }
+
+ return null;
+ }
+
+ /** Implementation for {@link Closeable#close()} to close server socket. */
+ @Override
+ public synchronized void close() throws IOException {
+ Logger.logDebug(LOG_TAG, "close");
+
+ int fd = mLocalSocketRunConfig.getFD();
+
+ if (fd >= 0) {
+ JniResult result = LocalSocketManager.closeSocket(mLocalSocketRunConfig.getLogTitle() + " (server)", fd);
+ if (result == null || result.retval != 0) {
+ throw new IOException(JniResult.getErrorString(result));
+ }
+ // Update fd to signify that server socket has been closed
+ mLocalSocketRunConfig.setFD(-1);
+ }
+ }
+
+ /**
+ * Delete server socket file if not an abstract namespace socket. This will cause any existing
+ * running server to stop.
+ */
+ private Error deleteServerSocketFile() {
+ if (!mLocalSocketRunConfig.isAbstractNamespaceSocket())
+ return FileUtils.deleteSocketFile(mLocalSocketRunConfig.getTitle() + " server socket file", mLocalSocketRunConfig.getPath(), true);
+ else
+ return null;
+ }
+
+ /** Listen and accept new {@link LocalClientSocket}. */
+ public LocalClientSocket accept() {
+ Logger.logVerbose(LOG_TAG, "accept");
+
+ int clientFD;
+ while (true) {
+ // If server socket closed
+ int fd = mLocalSocketRunConfig.getFD();
+ if (fd < 0) {
+ return null;
+ }
+
+ JniResult result = LocalSocketManager.accept(mLocalSocketRunConfig.getLogTitle() + " (client)", fd);
+ if (result == null || result.retval != 0) {
+ mLocalSocketManager.onError(
+ LocalSocketErrno.ERRNO_ACCEPT_CLIENT_SOCKET_FAILED.getError(mLocalSocketRunConfig.getTitle(), JniResult.getErrorString(result)));
+ continue;
+ }
+
+ clientFD = result.intData;
+ if (clientFD < 0) {
+ mLocalSocketManager.onError(
+ LocalSocketErrno.ERRNO_CLIENT_SOCKET_FD_INVALID.getError(clientFD, mLocalSocketRunConfig.getTitle()));
+ continue;
+ }
+
+ PeerCred peerCred = new PeerCred();
+ result = LocalSocketManager.getPeerCred(mLocalSocketRunConfig.getLogTitle() + " (client)", clientFD, peerCred);
+ if (result == null || result.retval != 0) {
+ mLocalSocketManager.onError(
+ LocalSocketErrno.ERRNO_GET_CLIENT_SOCKET_PEER_UID_FAILED.getError(mLocalSocketRunConfig.getTitle(), JniResult.getErrorString(result)));
+ LocalClientSocket.closeClientSocket(mLocalSocketManager, clientFD);
+ continue;
+ }
+
+ int peerUid = peerCred.uid;
+ if (peerUid < 0) {
+ mLocalSocketManager.onError(
+ LocalSocketErrno.ERRNO_CLIENT_SOCKET_PEER_UID_INVALID.getError(peerUid, mLocalSocketRunConfig.getTitle()));
+ LocalClientSocket.closeClientSocket(mLocalSocketManager, clientFD);
+ continue;
+ }
+
+ LocalClientSocket clientSocket = new LocalClientSocket(mLocalSocketManager, clientFD, peerCred);
+ Logger.logVerbose(LOG_TAG, "Client socket accept for \"" + mLocalSocketRunConfig.getTitle() + "\" server\n" + clientSocket.getLogString());
+
+ // Only allow connection if the peer has the same uid as server app's user id or root user id
+ if (peerUid != mLocalSocketManager.getContext().getApplicationInfo().uid && peerUid != 0) {
+ mLocalSocketManager.onDisallowedClientConnected(clientSocket,
+ LocalSocketErrno.ERRNO_CLIENT_SOCKET_PEER_UID_DISALLOWED.getError(clientSocket.getPeerCred().getMinimalString(),
+ mLocalSocketManager.getLocalSocketRunConfig().getTitle()));
+ clientSocket.closeClientSocket(true);
+ continue;
+ }
+
+ return clientSocket;
+ }
+ }
+
+
+
+
+ /** The {@link LocalClientSocket} listener {@link java.lang.Runnable} for {@link LocalServerSocket}. */
+ protected class ClientSocketListener implements Runnable {
+
+ @Override
+ public void run() {
+ try {
+ Logger.logVerbose(LOG_TAG, "ClientSocketListener start");
+
+ while (!Thread.currentThread().isInterrupted()) {
+ LocalClientSocket clientSocket = null;
+ try {
+ // Listen for new client socket connections
+ clientSocket = null;
+ clientSocket = accept();
+ // If server socket is closed, then stop listener thread.
+ if (clientSocket == null)
+ break;
+
+ Error error;
+
+ error = clientSocket.setReadTimeout();
+ if (error != null) {
+ mLocalSocketManager.onError(clientSocket, error);
+ clientSocket.closeClientSocket(true);
+ continue;
+ }
+
+ error = clientSocket.setWriteTimeout();
+ if (error != null) {
+ mLocalSocketManager.onError(clientSocket, error);
+ clientSocket.closeClientSocket(true);
+ continue;
+ }
+
+ // Start new thread for client logic and pass control to ILocalSocketManager implementation
+ mLocalSocketManager.onClientAccepted(clientSocket);
+ } catch (Throwable t) {
+ mLocalSocketManager.onError(clientSocket,
+ LocalSocketErrno.ERRNO_CLIENT_SOCKET_LISTENER_FAILED_WITH_EXCEPTION.getError(t, mLocalSocketRunConfig.getTitle(), t.getMessage()));
+ if (clientSocket != null)
+ clientSocket.closeClientSocket(true);
+ }
+ }
+ } catch (Exception ignored) {
+ } finally {
+ try {
+ close();
+ } catch (Exception ignored) {}
+ }
+
+ Logger.logVerbose(LOG_TAG, "ClientSocketListener end");
+ }
+
+ }
+
+}
diff --git a/termux-shared/src/main/java/com/termux/shared/net/socket/local/LocalSocketErrno.java b/termux-shared/src/main/java/com/termux/shared/net/socket/local/LocalSocketErrno.java
new file mode 100644
index 0000000000..251f5c67e9
--- /dev/null
+++ b/termux-shared/src/main/java/com/termux/shared/net/socket/local/LocalSocketErrno.java
@@ -0,0 +1,43 @@
+package com.termux.shared.net.socket.local;
+
+import com.termux.shared.errors.Errno;
+
+public class LocalSocketErrno extends Errno {
+
+ public static final String TYPE = "LocalSocket Error";
+
+
+ /** Errors for {@link LocalSocketManager} (100-150) */
+ public static final Errno ERRNO_START_LOCAL_SOCKET_LIB_LOAD_FAILED_WITH_EXCEPTION = new Errno(TYPE, 100, "Failed to load \"%1$s\" library.\nException: %2$s");
+
+ /** Errors for {@link LocalServerSocket} (150-200) */
+ public static final Errno ERRNO_SERVER_SOCKET_PATH_NULL_OR_EMPTY = new Errno(TYPE, 150, "The \"%1$s\" server socket path is null or empty.");
+ public static final Errno ERRNO_SERVER_SOCKET_PATH_TOO_LONG = new Errno(TYPE, 151, "The \"%1$s\" server socket path \"%2$s\" is greater than 108 bytes.");
+ public static final Errno ERRNO_SERVER_SOCKET_PATH_NOT_ABSOLUTE = new Errno(TYPE, 152, "The \"%1$s\" server socket path \"%2$s\" is not an absolute file path.");
+ public static final Errno ERRNO_SERVER_SOCKET_BACKLOG_INVALID = new Errno(TYPE, 153, "The \"%1$s\" server socket backlog \"%2$s\" is not greater than 0.");
+ public static final Errno ERRNO_CREATE_SERVER_SOCKET_FAILED = new Errno(TYPE, 154, "Create \"%1$s\" server socket failed.\n%2$s");
+ public static final Errno ERRNO_SERVER_SOCKET_FD_INVALID = new Errno(TYPE, 155, "Invalid file descriptor \"%1$s\" returned when creating \"%2$s\" server socket.");
+ public static final Errno ERRNO_ACCEPT_CLIENT_SOCKET_FAILED = new Errno(TYPE, 156, "Accepting client socket for \"%1$s\" server failed.\n%2$s");
+ public static final Errno ERRNO_CLIENT_SOCKET_FD_INVALID = new Errno(TYPE, 157, "Invalid file descriptor \"%1$s\" returned when accept new client for \"%2$s\" server.");
+ public static final Errno ERRNO_GET_CLIENT_SOCKET_PEER_UID_FAILED = new Errno(TYPE, 158, "Getting peer uid for client socket for \"%1$s\" server failed.\n%2$s");
+ public static final Errno ERRNO_CLIENT_SOCKET_PEER_UID_INVALID = new Errno(TYPE, 158, "Invalid peer uid \"%1$s\" returned for new client for \"%2$s\" server.");
+ public static final Errno ERRNO_CLIENT_SOCKET_PEER_UID_DISALLOWED = new Errno(TYPE, 160, "Disallowed peer %1$s tried to connect with \"%2$s\" server.");
+ public static final Errno ERRNO_CLOSE_SERVER_SOCKET_FAILED_WITH_EXCEPTION = new Errno(TYPE, 161, "Close \"%1$s\" server socket failed.\nException: %2$s");
+ public static final Errno ERRNO_CLIENT_SOCKET_LISTENER_FAILED_WITH_EXCEPTION = new Errno(TYPE, 162, "Exception in client socket listener for \"%1$s\" server.\nException: %2$s");
+
+ /** Errors for {@link LocalClientSocket} (200-250) */
+ public static final Errno ERRNO_SET_CLIENT_SOCKET_READ_TIMEOUT_FAILED = new Errno(TYPE, 200, "Set \"%1$s\" client socket read (SO_RCVTIMEO) timeout to \"%2$s\" failed.\n%3$s");
+ public static final Errno ERRNO_SET_CLIENT_SOCKET_SEND_TIMEOUT_FAILED = new Errno(TYPE, 201, "Set \"%1$s\" client socket send (SO_SNDTIMEO) timeout \"%2$s\" failed.\n%3$s");
+ public static final Errno ERRNO_READ_DATA_FROM_CLIENT_SOCKET_FAILED = new Errno(TYPE, 202, "Read data from \"%1$s\" client socket failed.\n%2$s");
+ public static final Errno ERRNO_READ_DATA_FROM_INPUT_STREAM_OF_CLIENT_SOCKET_FAILED_WITH_EXCEPTION = new Errno(TYPE, 203, "Read data from \"%1$s\" client socket input stream failed.\n%2$s");
+ public static final Errno ERRNO_SEND_DATA_TO_CLIENT_SOCKET_FAILED = new Errno(TYPE, 204, "Send data to \"%1$s\" client socket failed.\n%2$s");
+ public static final Errno ERRNO_SEND_DATA_TO_OUTPUT_STREAM_OF_CLIENT_SOCKET_FAILED_WITH_EXCEPTION = new Errno(TYPE, 205, "Send data to \"%1$s\" client socket output stream failed.\n%2$s");
+ public static final Errno ERRNO_CHECK_AVAILABLE_DATA_ON_CLIENT_SOCKET_FAILED = new Errno(TYPE, 206, "Check available data on \"%1$s\" client socket failed.\n%2$s");
+ public static final Errno ERRNO_CLOSE_CLIENT_SOCKET_FAILED_WITH_EXCEPTION = new Errno(TYPE, 207, "Close \"%1$s\" client socket failed.\n%2$s");
+ public static final Errno ERRNO_USING_CLIENT_SOCKET_WITH_INVALID_FD = new Errno(TYPE, 208, "Trying to use client socket with invalid file descriptor \"%1$s\" for \"%2$s\" server.");
+
+ LocalSocketErrno(final String type, final int code, final String message) {
+ super(type, code, message);
+ }
+
+}
diff --git a/termux-shared/src/main/java/com/termux/shared/net/socket/local/LocalSocketManager.java b/termux-shared/src/main/java/com/termux/shared/net/socket/local/LocalSocketManager.java
new file mode 100644
index 0000000000..0b7c3dc936
--- /dev/null
+++ b/termux-shared/src/main/java/com/termux/shared/net/socket/local/LocalSocketManager.java
@@ -0,0 +1,448 @@
+package com.termux.shared.net.socket.local;
+
+import android.content.Context;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.termux.shared.errors.Error;
+import com.termux.shared.jni.models.JniResult;
+import com.termux.shared.logger.Logger;
+
+/**
+ * Manager for an AF_UNIX/SOCK_STREAM local server.
+ *
+ * Usage:
+ * 1. Implement the {@link ILocalSocketManager} that will receive call backs from the server including
+ * when client connects via {@link ILocalSocketManager#onClientAccepted(LocalSocketManager, LocalClientSocket)}.
+ * Optionally extend the {@link LocalSocketManagerClientBase} class that provides base implementation.
+ * 2. Create a {@link LocalSocketRunConfig} instance with the run config of the server.
+ * 3. Create a {@link LocalSocketManager} instance and call {@link #start()}.
+ * 4. Stop server if needed with a call to {@link #stop()}.
+ */
+public class LocalSocketManager {
+
+ public static final String LOG_TAG = "LocalSocketManager";
+
+ /** The native JNI local socket library. */
+ protected static String LOCAL_SOCKET_LIBRARY = "local-socket";
+
+ /** Whether {@link #LOCAL_SOCKET_LIBRARY} has been loaded or not. */
+ protected static boolean localSocketLibraryLoaded;
+
+ /** The {@link Context} that may needed for various operations. */
+ @NonNull protected final Context mContext;
+
+ /** The {@link LocalSocketRunConfig} containing run config for the {@link LocalSocketManager}. */
+ @NonNull protected final LocalSocketRunConfig mLocalSocketRunConfig;
+
+ /** The {@link LocalServerSocket} for the {@link LocalSocketManager}. */
+ @NonNull protected final LocalServerSocket mServerSocket;
+
+ /** The {@link ILocalSocketManager} client for the {@link LocalSocketManager}. */
+ @NonNull protected final ILocalSocketManager mLocalSocketManagerClient;
+
+ /** The {@link Thread.UncaughtExceptionHandler} used for client thread started by {@link LocalSocketManager}. */
+ @NonNull protected final Thread.UncaughtExceptionHandler mLocalSocketManagerClientThreadUEH;
+
+ /** Whether the {@link LocalServerSocket} managed by {@link LocalSocketManager} in running or not. */
+ protected boolean mIsRunning;
+
+
+ /**
+ * Create an new instance of {@link LocalSocketManager}.
+ *
+ * @param context The {@link #mContext} value.
+ * @param localSocketRunConfig The {@link #mLocalSocketRunConfig} value.
+ */
+ public LocalSocketManager(@NonNull Context context, @NonNull LocalSocketRunConfig localSocketRunConfig) {
+ mContext = context.getApplicationContext();
+ mLocalSocketRunConfig = localSocketRunConfig;
+ mServerSocket = new LocalServerSocket(this);
+ mLocalSocketManagerClient = mLocalSocketRunConfig.getLocalSocketManagerClient();
+ mLocalSocketManagerClientThreadUEH = getLocalSocketManagerClientThreadUEHOrDefault();
+ mIsRunning = false;
+ }
+
+ /**
+ * Create the {@link LocalServerSocket} and start listening for new {@link LocalClientSocket}.
+ */
+ public synchronized Error start() {
+ Logger.logDebugExtended(LOG_TAG, "start\n" + mLocalSocketRunConfig);
+
+ if (!localSocketLibraryLoaded) {
+ try {
+ Logger.logDebug(LOG_TAG, "Loading \"" + LOCAL_SOCKET_LIBRARY + "\" library");
+ System.loadLibrary(LOCAL_SOCKET_LIBRARY);
+ localSocketLibraryLoaded = true;
+ } catch (Exception e) {
+ return LocalSocketErrno.ERRNO_START_LOCAL_SOCKET_LIB_LOAD_FAILED_WITH_EXCEPTION.getError(e, LOCAL_SOCKET_LIBRARY, e.getMessage());
+ }
+ }
+
+ mIsRunning = true;
+ return mServerSocket.start();
+ }
+
+ /**
+ * Stop the {@link LocalServerSocket} and stop listening for new {@link LocalClientSocket}.
+ */
+ public synchronized Error stop() {
+ if (mIsRunning) {
+ Logger.logDebugExtended(LOG_TAG, "stop\n" + mLocalSocketRunConfig);
+ mIsRunning = false;
+ return mServerSocket.stop();
+ }
+ return null;
+ }
+
+
+
+
+ /*
+ Note: Exceptions thrown from JNI must be caught with Throwable class instead of Exception,
+ otherwise exception will be sent to UncaughtExceptionHandler of the thread.
+ */
+
+ /**
+ * Creates an AF_UNIX/SOCK_STREAM local server socket at {@code path}, with the specified backlog.
+ *
+ * @param serverTitle The server title used for logging and errors.
+ * @param path The path at which to create the socket.
+ * For a filesystem socket, this must be an absolute path to the socket file.
+ * For an abstract namespace socket, the first byte must be a null `\0` character.
+ * Max allowed length is 108 bytes as per sun_path size (UNIX_PATH_MAX) on Linux.
+ * @param backlog The maximum length to which the queue of pending connections for the socket
+ * may grow. This value may be ignored or may not have one-to-one mapping
+ * in kernel implementation. Value must be greater than 0.
+ * @return Returns the {@link JniResult}. If server creation was successful, then
+ * {@link JniResult#retval} will be 0 and {@link JniResult#intData} will contain the server socket
+ * fd.
+ */
+ @Nullable
+ public static JniResult createServerSocket(@NonNull String serverTitle, @NonNull byte[] path, int backlog) {
+ try {
+ return createServerSocketNative(serverTitle, path, backlog);
+ } catch (Throwable t) {
+ String message = "Exception in createServerSocketNative()";
+ Logger.logStackTraceWithMessage(LOG_TAG, message, t);
+ return new JniResult(message, t);
+ }
+ }
+
+ /**
+ * Closes the socket with fd.
+ *
+ * @param serverTitle The server title used for logging and errors.
+ * @param fd The socket fd.
+ * @return Returns the {@link JniResult}. If closing socket was successful, then
+ * {@link JniResult#retval} will be 0.
+ */
+ @Nullable
+ public static JniResult closeSocket(@NonNull String serverTitle, int fd) {
+ try {
+ return closeSocketNative(serverTitle, fd);
+ } catch (Throwable t) {
+ String message = "Exception in closeSocketNative()";
+ Logger.logStackTraceWithMessage(LOG_TAG, message, t);
+ return new JniResult(message, t);
+ }
+ }
+
+ /**
+ * Accepts a connection on the supplied server socket fd.
+ *
+ * @param serverTitle The server title used for logging and errors.
+ * @param fd The server socket fd.
+ * @return Returns the {@link JniResult}. If accepting socket was successful, then
+ * {@link JniResult#retval} will be 0 and {@link JniResult#intData} will contain the client socket
+ * fd.
+ */
+ @Nullable
+ public static JniResult accept(@NonNull String serverTitle, int fd) {
+ try {
+ return acceptNative(serverTitle, fd);
+ } catch (Throwable t) {
+ String message = "Exception in acceptNative()";
+ Logger.logStackTraceWithMessage(LOG_TAG, message, t);
+ return new JniResult(message, t);
+ }
+ }
+
+ /**
+ * Attempts to read up to data buffer length bytes from file descriptor fd into the data buffer.
+ * On success, the number of bytes read is returned (zero indicates end of file).
+ * It is not an error if bytes read is smaller than the number of bytes requested; this may happen
+ * for example because fewer bytes are actually available right now (maybe because we were close
+ * to end-of-file, or because we are reading from a pipe), or because read() was interrupted by
+ * a signal. On error, the {@link JniResult#errno} and {@link JniResult#errmsg} will be set.
+ *
+ * If while reading the deadline elapses but all the data has not been read, the call will fail.
+ *
+ * @param serverTitle The server title used for logging and errors.
+ * @param fd The socket fd.
+ * @param data The data buffer to read bytes into.
+ * @param deadline The deadline milliseconds since epoch.
+ * @return Returns the {@link JniResult}. If reading was successful, then {@link JniResult#retval}
+ * will be 0 and {@link JniResult#intData} will contain the bytes read.
+ */
+ @Nullable
+ public static JniResult read(@NonNull String serverTitle, int fd, @NonNull byte[] data, long deadline) {
+ try {
+ return readNative(serverTitle, fd, data, deadline);
+ } catch (Throwable t) {
+ String message = "Exception in readNative()";
+ Logger.logStackTraceWithMessage(LOG_TAG, message, t);
+ return new JniResult(message, t);
+ }
+ }
+
+ /**
+ * Attempts to send data buffer to the file descriptor. On error, the {@link JniResult#errno} and
+ * {@link JniResult#errmsg} will be set.
+ *
+ * If while sending the deadline elapses but all the data has not been sent, the call will fail.
+ *
+ * @param serverTitle The server title used for logging and errors.
+ * @param fd The socket fd.
+ * @param data The data buffer containing bytes to send.
+ * @param deadline The deadline milliseconds since epoch.
+ * @return Returns the {@link JniResult}. If sending was successful, then {@link JniResult#retval}
+ * will be 0.
+ */
+ @Nullable
+ public static JniResult send(@NonNull String serverTitle, int fd, @NonNull byte[] data, long deadline) {
+ try {
+ return sendNative(serverTitle, fd, data, deadline);
+ } catch (Throwable t) {
+ String message = "Exception in sendNative()";
+ Logger.logStackTraceWithMessage(LOG_TAG, message, t);
+ return new JniResult(message, t);
+ }
+ }
+
+ /**
+ * Gets the number of bytes available to read on the socket.
+ *
+ * @param serverTitle The server title used for logging and errors.
+ * @param fd The socket fd.
+ * @return Returns the {@link JniResult}. If checking availability was successful, then
+ * {@link JniResult#retval} will be 0 and {@link JniResult#intData} will contain the bytes available.
+ */
+ @Nullable
+ public static JniResult available(@NonNull String serverTitle, int fd) {
+ try {
+ return availableNative(serverTitle, fd);
+ } catch (Throwable t) {
+ String message = "Exception in availableNative()";
+ Logger.logStackTraceWithMessage(LOG_TAG, message, t);
+ return new JniResult(message, t);
+ }
+ }
+
+ /**
+ * Set receiving (SO_RCVTIMEO) timeout in milliseconds for socket.
+ *
+ * @param serverTitle The server title used for logging and errors.
+ * @param fd The socket fd.
+ * @param timeout The timeout value in milliseconds.
+ * @return Returns the {@link JniResult}. If setting timeout was successful, then
+ * {@link JniResult#retval} will be 0.
+ */
+ @Nullable
+ public static JniResult setSocketReadTimeout(@NonNull String serverTitle, int fd, int timeout) {
+ try {
+ return setSocketReadTimeoutNative(serverTitle, fd, timeout);
+ } catch (Throwable t) {
+ String message = "Exception in setSocketReadTimeoutNative()";
+ Logger.logStackTraceWithMessage(LOG_TAG, message, t);
+ return new JniResult(message, t);
+ }
+ }
+
+ /**
+ * Set sending (SO_SNDTIMEO) timeout in milliseconds for fd.
+ *
+ * @param serverTitle The server title used for logging and errors.
+ * @param fd The socket fd.
+ * @param timeout The timeout value in milliseconds.
+ * @return Returns the {@link JniResult}. If setting timeout was successful, then
+ * {@link JniResult#retval} will be 0.
+ */
+ @Nullable
+ public static JniResult setSocketSendTimeout(@NonNull String serverTitle, int fd, int timeout) {
+ try {
+ return setSocketSendTimeoutNative(serverTitle, fd, timeout);
+ } catch (Throwable t) {
+ String message = "Exception in setSocketSendTimeoutNative()";
+ Logger.logStackTraceWithMessage(LOG_TAG, message, t);
+ return new JniResult(message, t);
+ }
+ }
+
+ /**
+ * Get the {@link PeerCred} for the socket.
+ *
+ * @param serverTitle The server title used for logging and errors.
+ * @param fd The socket fd.
+ * @param peerCred The {@link PeerCred} object that should be filled.
+ * @return Returns the {@link JniResult}. If setting timeout was successful, then
+ * {@link JniResult#retval} will be 0.
+ */
+ @Nullable
+ public static JniResult getPeerCred(@NonNull String serverTitle, int fd, PeerCred peerCred) {
+ try {
+ return getPeerCredNative(serverTitle, fd, peerCred);
+ } catch (Throwable t) {
+ String message = "Exception in getPeerCredNative()";
+ Logger.logStackTraceWithMessage(LOG_TAG, message, t);
+ return new JniResult(message, t);
+ }
+ }
+
+
+
+ /** Wrapper for {@link #onError(LocalClientSocket, Error)} for {@code null} {@link LocalClientSocket}. */
+ public void onError(@NonNull Error error) {
+ onError(null, error);
+ }
+
+ /** Wrapper to call {@link ILocalSocketManager#onError(LocalSocketManager, LocalClientSocket, Error)} in a new thread. */
+ public void onError(@Nullable LocalClientSocket clientSocket, @NonNull Error error) {
+ startLocalSocketManagerClientThread(() ->
+ mLocalSocketManagerClient.onError(this, clientSocket, error));
+ }
+
+ /** Wrapper to call {@link ILocalSocketManager#onDisallowedClientConnected(LocalSocketManager, LocalClientSocket, Error)} in a new thread. */
+ public void onDisallowedClientConnected(@NonNull LocalClientSocket clientSocket, @NonNull Error error) {
+ startLocalSocketManagerClientThread(() ->
+ mLocalSocketManagerClient.onDisallowedClientConnected(this, clientSocket, error));
+ }
+
+ /** Wrapper to call {@link ILocalSocketManager#onClientAccepted(LocalSocketManager, LocalClientSocket)} in a new thread. */
+ public void onClientAccepted(@NonNull LocalClientSocket clientSocket) {
+ startLocalSocketManagerClientThread(() ->
+ mLocalSocketManagerClient.onClientAccepted(this, clientSocket));
+ }
+
+ /** All client accept logic must be run on separate threads so that incoming client acceptance is not blocked. */
+ public void startLocalSocketManagerClientThread(@NonNull Runnable runnable) {
+ Thread thread = new Thread(runnable);
+ thread.setUncaughtExceptionHandler(getLocalSocketManagerClientThreadUEH());
+ try {
+ thread.start();
+ } catch (Exception e) {
+ Logger.logStackTraceWithMessage(LOG_TAG, "LocalSocketManagerClientThread start failed", e);
+ }
+ }
+
+
+
+ /** Get {@link #mContext}. */
+ public Context getContext() {
+ return mContext;
+ }
+
+ /** Get {@link #mLocalSocketRunConfig}. */
+ public LocalSocketRunConfig getLocalSocketRunConfig() {
+ return mLocalSocketRunConfig;
+ }
+
+ /** Get {@link #mLocalSocketManagerClient}. */
+ public ILocalSocketManager getLocalSocketManagerClient() {
+ return mLocalSocketManagerClient;
+ }
+
+ /** Get {@link #mServerSocket}. */
+ public LocalServerSocket getServerSocket() {
+ return mServerSocket;
+ }
+
+ /** Get {@link #mLocalSocketManagerClientThreadUEH}. */
+ public Thread.UncaughtExceptionHandler getLocalSocketManagerClientThreadUEH() {
+ return mLocalSocketManagerClientThreadUEH;
+ }
+
+ /**
+ * Get {@link Thread.UncaughtExceptionHandler} returned by call to
+ * {@link ILocalSocketManager#getLocalSocketManagerClientThreadUEH(LocalSocketManager)}
+ * or the default handler that just logs the exception.
+ */
+ protected Thread.UncaughtExceptionHandler getLocalSocketManagerClientThreadUEHOrDefault() {
+ Thread.UncaughtExceptionHandler uncaughtExceptionHandler =
+ mLocalSocketManagerClient.getLocalSocketManagerClientThreadUEH(this);
+ if (uncaughtExceptionHandler == null)
+ uncaughtExceptionHandler = (t, e) ->
+ Logger.logStackTraceWithMessage(LOG_TAG, "Uncaught exception for " + t + " in " + mLocalSocketRunConfig.getTitle() + " server", e);
+ return uncaughtExceptionHandler;
+ }
+
+ /** Get {@link #mIsRunning}. */
+ public boolean isRunning() {
+ return mIsRunning;
+ }
+
+
+
+ /** Get an error log {@link String} for the {@link LocalSocketManager}. */
+ public static String getErrorLogString(@NonNull Error error,
+ @NonNull LocalSocketRunConfig localSocketRunConfig,
+ @Nullable LocalClientSocket clientSocket) {
+ StringBuilder logString = new StringBuilder();
+
+ logString.append(localSocketRunConfig.getTitle()).append(" Socket Server Error:\n");
+ logString.append(error.getErrorLogString());
+ logString.append("\n\n\n");
+
+ logString.append(localSocketRunConfig.getLogString());
+
+ if (clientSocket != null) {
+ logString.append("\n\n\n");
+ logString.append(clientSocket.getLogString());
+ }
+
+ return logString.toString();
+ }
+
+ /** Get an error markdown {@link String} for the {@link LocalSocketManager}. */
+ public static String getErrorMarkdownString(@NonNull Error error,
+ @NonNull LocalSocketRunConfig localSocketRunConfig,
+ @Nullable LocalClientSocket clientSocket) {
+ StringBuilder markdownString = new StringBuilder();
+
+ markdownString.append(error.getErrorMarkdownString());
+ markdownString.append("\n##\n\n\n");
+
+ markdownString.append(localSocketRunConfig.getMarkdownString());
+
+ if (clientSocket != null) {
+ markdownString.append("\n\n\n");
+ markdownString.append(clientSocket.getMarkdownString());
+ }
+
+ return markdownString.toString();
+ }
+
+
+
+
+
+ @Nullable private static native JniResult createServerSocketNative(@NonNull String serverTitle, @NonNull byte[] path, int backlog);
+
+ @Nullable private static native JniResult closeSocketNative(@NonNull String serverTitle, int fd);
+
+ @Nullable private static native JniResult acceptNative(@NonNull String serverTitle, int fd);
+
+ @Nullable private static native JniResult readNative(@NonNull String serverTitle, int fd, @NonNull byte[] data, long deadline);
+
+ @Nullable private static native JniResult sendNative(@NonNull String serverTitle, int fd, @NonNull byte[] data, long deadline);
+
+ @Nullable private static native JniResult availableNative(@NonNull String serverTitle, int fd);
+
+ private static native JniResult setSocketReadTimeoutNative(@NonNull String serverTitle, int fd, int timeout);
+
+ @Nullable private static native JniResult setSocketSendTimeoutNative(@NonNull String serverTitle, int fd, int timeout);
+
+ @Nullable private static native JniResult getPeerCredNative(@NonNull String serverTitle, int fd, PeerCred peerCred);
+
+}
diff --git a/termux-shared/src/main/java/com/termux/shared/net/socket/local/LocalSocketManagerClientBase.java b/termux-shared/src/main/java/com/termux/shared/net/socket/local/LocalSocketManagerClientBase.java
new file mode 100644
index 0000000000..4d7657c452
--- /dev/null
+++ b/termux-shared/src/main/java/com/termux/shared/net/socket/local/LocalSocketManagerClientBase.java
@@ -0,0 +1,47 @@
+package com.termux.shared.net.socket.local;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.termux.shared.errors.Error;
+import com.termux.shared.logger.Logger;
+
+/** Base helper implementation for {@link ILocalSocketManager}. */
+public abstract class LocalSocketManagerClientBase implements ILocalSocketManager {
+
+ @Nullable
+ @Override
+ public Thread.UncaughtExceptionHandler getLocalSocketManagerClientThreadUEH(
+ @NonNull LocalSocketManager localSocketManager) {
+ return null;
+ }
+
+ @Override
+ public void onError(@NonNull LocalSocketManager localSocketManager,
+ @Nullable LocalClientSocket clientSocket, @NonNull Error error) {
+ // Only log if log level is debug or higher since PeerCred.cmdline may contain private info
+ Logger.logErrorPrivate(getLogTag(), "onError");
+ Logger.logErrorPrivateExtended(getLogTag(), LocalSocketManager.getErrorLogString(error,
+ localSocketManager.getLocalSocketRunConfig(), clientSocket));
+ }
+
+ @Override
+ public void onDisallowedClientConnected(@NonNull LocalSocketManager localSocketManager,
+ @NonNull LocalClientSocket clientSocket, @NonNull Error error) {
+ Logger.logWarn(getLogTag(), "onDisallowedClientConnected");
+ Logger.logWarnExtended(getLogTag(), LocalSocketManager.getErrorLogString(error,
+ localSocketManager.getLocalSocketRunConfig(), clientSocket));
+ }
+
+ @Override
+ public void onClientAccepted(@NonNull LocalSocketManager localSocketManager,
+ @NonNull LocalClientSocket clientSocket) {
+ // Just close socket and let child class handle any required communication
+ clientSocket.closeClientSocket(true);
+ }
+
+
+
+ protected abstract String getLogTag();
+
+}
diff --git a/termux-shared/src/main/java/com/termux/shared/net/socket/local/LocalSocketRunConfig.java b/termux-shared/src/main/java/com/termux/shared/net/socket/local/LocalSocketRunConfig.java
new file mode 100644
index 0000000000..53ef895720
--- /dev/null
+++ b/termux-shared/src/main/java/com/termux/shared/net/socket/local/LocalSocketRunConfig.java
@@ -0,0 +1,265 @@
+package com.termux.shared.net.socket.local;
+
+import androidx.annotation.NonNull;
+
+import com.termux.shared.file.FileUtils;
+import com.termux.shared.logger.Logger;
+import com.termux.shared.markdown.MarkdownUtils;
+
+import java.io.Serializable;
+import java.nio.charset.StandardCharsets;
+
+
+/**
+ * Run config for {@link LocalSocketManager}.
+ */
+public class LocalSocketRunConfig implements Serializable {
+
+ /** The {@link LocalSocketManager} title. */
+ private final String mTitle;
+
+ /**
+ * The {@link LocalServerSocket} path.
+ *
+ * For a filesystem socket, this must be an absolute path to the socket file. Creation of a new
+ * socket will fail if the server starter app process does not have write and search (execute)
+ * permission on the directory in which the socket is created. The client process must have write
+ * permission on the socket to connect to it. Other app will not be able to connect to socket
+ * if its created in private app data directory.
+ *
+ * For an abstract namespace socket, the first byte must be a null `\0` character. Note that on
+ * Android 9+, if server app is using `targetSdkVersion` `28`, then other apps will not be able
+ * to connect to it due to selinux restrictions.
+ * > Per-app SELinux domains
+ * > Apps that target Android 9 or higher cannot share data with other apps using world-accessible
+ * Unix permissions. This change improves the integrity of the Android Application Sandbox,
+ * particularly the requirement that an app's private data is accessible only by that app.
+ * https://developer.android.com/about/versions/pie/android-9.0-changes-28
+ * https://github.com/android/ndk/issues/1469
+ * https://stackoverflow.com/questions/63806516/avc-denied-connectto-when-using-uds-on-android-10
+ *
+ * Max allowed length is 108 bytes as per sun_path size (UNIX_PATH_MAX) on Linux.
+ */
+ private final String mPath;
+
+ /** If abstract namespace {@link LocalServerSocket} instead of filesystem. */
+ protected final boolean mAbstractNamespaceSocket;
+
+ /** The {@link ILocalSocketManager} client for the {@link LocalSocketManager}. */
+ private final ILocalSocketManager mLocalSocketManagerClient;
+
+ /**
+ * The {@link LocalServerSocket} file descriptor.
+ * Value will be `>= 0` if socket has been created successfully and `-1` if not created or closed.
+ */
+ private int mFD = -1;
+
+ /**
+ * The {@link LocalClientSocket} receiving (SO_RCVTIMEO) timeout in milliseconds.
+ *
+ * https://manpages.debian.org/testing/manpages/socket.7.en.html
+ * https://cs.android.com/android/platform/superproject/+/android-12.0.0_r32:frameworks/base/services/core/java/com/android/server/am/NativeCrashListener.java;l=55
+ * Defaults to {@link #DEFAULT_RECEIVE_TIMEOUT}.
+ */
+ private Integer mReceiveTimeout;
+ public static final int DEFAULT_RECEIVE_TIMEOUT = 10000;
+
+ /**
+ * The {@link LocalClientSocket} sending (SO_SNDTIMEO) timeout in milliseconds.
+ *
+ * https://manpages.debian.org/testing/manpages/socket.7.en.html
+ * https://cs.android.com/android/platform/superproject/+/android-12.0.0_r32:frameworks/base/services/core/java/com/android/server/am/NativeCrashListener.java;l=55
+ * Defaults to {@link #DEFAULT_SEND_TIMEOUT}.
+ */
+ private Integer mSendTimeout;
+ public static final int DEFAULT_SEND_TIMEOUT = 10000;
+
+ /**
+ * The {@link LocalClientSocket} deadline in milliseconds. When the deadline has elapsed after
+ * creation time of client socket, all reads and writes will error out. Set to 0, for no
+ * deadline.
+ * Defaults to {@link #DEFAULT_DEADLINE}.
+ */
+ private Long mDeadline;
+ public static final int DEFAULT_DEADLINE = 0;
+
+ /**
+ * The {@link LocalServerSocket} backlog for the maximum length to which the queue of pending connections
+ * for the socket may grow. This value may be ignored or may not have one-to-one mapping
+ * in kernel implementation. Value must be greater than 0.
+ *
+ * https://cs.android.com/android/platform/superproject/+/android-12.0.0_r32:frameworks/base/core/java/android/net/LocalSocketManager.java;l=31
+ * Defaults to {@link #DEFAULT_BACKLOG}.
+ */
+ private Integer mBacklog;
+ public static final int DEFAULT_BACKLOG = 50;
+
+
+ /**
+ * Create an new instance of {@link LocalSocketRunConfig}.
+ *
+ * @param title The {@link #mTitle} value.
+ * @param path The {@link #mPath} value.
+ * @param localSocketManagerClient The {@link #mLocalSocketManagerClient} value.
+ */
+ public LocalSocketRunConfig(@NonNull String title, @NonNull String path, @NonNull ILocalSocketManager localSocketManagerClient) {
+ mTitle = title;
+ mLocalSocketManagerClient = localSocketManagerClient;
+ mAbstractNamespaceSocket = path.getBytes(StandardCharsets.UTF_8)[0] == 0;
+
+ if (mAbstractNamespaceSocket)
+ mPath = path;
+ else
+ mPath = FileUtils.getCanonicalPath(path, null);
+ }
+
+
+
+ /** Get {@link #mTitle}. */
+ public String getTitle() {
+ return mTitle;
+ }
+
+ /** Get log title that should be used for {@link LocalSocketManager}. */
+ public String getLogTitle() {
+ return Logger.getDefaultLogTag() + "." + mTitle;
+ }
+
+ /** Get {@link #mPath}. */
+ public String getPath() {
+ return mPath;
+ }
+
+ /** Get {@link #mAbstractNamespaceSocket}. */
+ public boolean isAbstractNamespaceSocket() {
+ return mAbstractNamespaceSocket;
+ }
+
+ /** Get {@link #mLocalSocketManagerClient}. */
+ public ILocalSocketManager getLocalSocketManagerClient() {
+ return mLocalSocketManagerClient;
+ }
+
+ /** Get {@link #mFD}. */
+ public Integer getFD() {
+ return mFD;
+ }
+
+ /** Set {@link #mFD}. Value must be greater than 0 or -1. */
+ public void setFD(int fd) {
+ if (fd >= 0)
+ mFD = fd;
+ else
+ mFD = -1;
+ }
+
+ /** Get {@link #mReceiveTimeout} if set, otherwise {@link #DEFAULT_RECEIVE_TIMEOUT}. */
+ public Integer getReceiveTimeout() {
+ return mReceiveTimeout != null ? mReceiveTimeout : DEFAULT_RECEIVE_TIMEOUT;
+ }
+
+ /** Set {@link #mReceiveTimeout}. */
+ public void setReceiveTimeout(Integer receiveTimeout) {
+ mReceiveTimeout = receiveTimeout;
+ }
+
+ /** Get {@link #mSendTimeout} if set, otherwise {@link #DEFAULT_SEND_TIMEOUT}. */
+ public Integer getSendTimeout() {
+ return mSendTimeout != null ? mSendTimeout : DEFAULT_SEND_TIMEOUT;
+ }
+
+ /** Set {@link #mSendTimeout}. */
+ public void setSendTimeout(Integer sendTimeout) {
+ mSendTimeout = sendTimeout;
+ }
+
+ /** Get {@link #mDeadline} if set, otherwise {@link #DEFAULT_DEADLINE}. */
+ public Long getDeadline() {
+ return mDeadline != null ? mDeadline : DEFAULT_DEADLINE;
+ }
+
+ /** Set {@link #mDeadline}. */
+ public void setDeadline(Long deadline) {
+ mDeadline = deadline;
+ }
+
+ /** Get {@link #mBacklog} if set, otherwise {@link #DEFAULT_BACKLOG}. */
+ public Integer getBacklog() {
+ return mBacklog != null ? mBacklog : DEFAULT_BACKLOG;
+ }
+
+ /** Set {@link #mBacklog}. Value must be greater than 0. */
+ public void setBacklog(Integer backlog) {
+ if (backlog > 0)
+ mBacklog = backlog;
+ }
+
+
+ /**
+ * Get a log {@link String} for {@link LocalSocketRunConfig}.
+ *
+ * @param config The {@link LocalSocketRunConfig} to get info of.
+ * @return Returns the log {@link String}.
+ */
+ @NonNull
+ public static String getRunConfigLogString(final LocalSocketRunConfig config) {
+ if (config == null) return "null";
+ return config.getLogString();
+ }
+
+ /** Get a log {@link String} for the {@link LocalSocketRunConfig}. */
+ @NonNull
+ public String getLogString() {
+ StringBuilder logString = new StringBuilder();
+
+ logString.append(mTitle).append(" Socket Server Run Config:");
+ logString.append("\n").append(Logger.getSingleLineLogStringEntry("Path", mPath, "-"));
+ logString.append("\n").append(Logger.getSingleLineLogStringEntry("AbstractNamespaceSocket", mAbstractNamespaceSocket, "-"));
+ logString.append("\n").append(Logger.getSingleLineLogStringEntry("LocalSocketManagerClient", mLocalSocketManagerClient.getClass().getName(), "-"));
+ logString.append("\n").append(Logger.getSingleLineLogStringEntry("FD", mFD, "-"));
+ logString.append("\n").append(Logger.getSingleLineLogStringEntry("ReceiveTimeout", getReceiveTimeout(), "-"));
+ logString.append("\n").append(Logger.getSingleLineLogStringEntry("SendTimeout", getSendTimeout(), "-"));
+ logString.append("\n").append(Logger.getSingleLineLogStringEntry("Deadline", getDeadline(), "-"));
+ logString.append("\n").append(Logger.getSingleLineLogStringEntry("Backlog", getBacklog(), "-"));
+
+ return logString.toString();
+ }
+
+ /**
+ * Get a markdown {@link String} for {@link LocalSocketRunConfig}.
+ *
+ * @param config The {@link LocalSocketRunConfig} to get info of.
+ * @return Returns the markdown {@link String}.
+ */
+ public static String getRunConfigMarkdownString(final LocalSocketRunConfig config) {
+ if (config == null) return "null";
+ return config.getMarkdownString();
+ }
+
+ /** Get a markdown {@link String} for the {@link LocalSocketRunConfig}. */
+ @NonNull
+ public String getMarkdownString() {
+ StringBuilder markdownString = new StringBuilder();
+
+ markdownString.append("## ").append(mTitle).append(" Socket Server Run Config");
+ markdownString.append("\n").append(MarkdownUtils.getSingleLineMarkdownStringEntry("Path", mPath, "-"));
+ markdownString.append("\n").append(MarkdownUtils.getSingleLineMarkdownStringEntry("AbstractNamespaceSocket", mAbstractNamespaceSocket, "-"));
+ markdownString.append("\n").append(MarkdownUtils.getSingleLineMarkdownStringEntry("LocalSocketManagerClient", mLocalSocketManagerClient.getClass().getName(), "-"));
+ markdownString.append("\n").append(MarkdownUtils.getSingleLineMarkdownStringEntry("FD", mFD, "-"));
+ markdownString.append("\n").append(MarkdownUtils.getSingleLineMarkdownStringEntry("ReceiveTimeout", getReceiveTimeout(), "-"));
+ markdownString.append("\n").append(MarkdownUtils.getSingleLineMarkdownStringEntry("SendTimeout", getSendTimeout(), "-"));
+ markdownString.append("\n").append(MarkdownUtils.getSingleLineMarkdownStringEntry("Deadline", getDeadline(), "-"));
+ markdownString.append("\n").append(MarkdownUtils.getSingleLineMarkdownStringEntry("Backlog", getBacklog(), "-"));
+
+ return markdownString.toString();
+ }
+
+
+
+ @NonNull
+ @Override
+ public String toString() {
+ return getLogString();
+ }
+
+}
diff --git a/termux-shared/src/main/java/com/termux/shared/net/socket/local/PeerCred.java b/termux-shared/src/main/java/com/termux/shared/net/socket/local/PeerCred.java
new file mode 100644
index 0000000000..6674eee0f7
--- /dev/null
+++ b/termux-shared/src/main/java/com/termux/shared/net/socket/local/PeerCred.java
@@ -0,0 +1,142 @@
+package com.termux.shared.net.socket.local;
+
+import android.content.Context;
+
+import androidx.annotation.Keep;
+import androidx.annotation.NonNull;
+
+import com.termux.shared.android.ProcessUtils;
+import com.termux.shared.android.UserUtils;
+import com.termux.shared.logger.Logger;
+import com.termux.shared.markdown.MarkdownUtils;
+
+/** The {@link PeerCred} of the {@link LocalClientSocket} containing info of client/peer. */
+@Keep
+public class PeerCred {
+
+ public static final String LOG_TAG = "PeerCred";
+
+ /** Process Id. */
+ public int pid;
+ /** Process Name. */
+ public String pname;
+
+ /** User Id. */
+ public int uid;
+ /** User name. */
+ public String uname;
+
+ /** Group Id. */
+ public int gid;
+ /** Group name. */
+ public String gname;
+
+ /** Command line that started the process. */
+ public String cmdline;
+
+ PeerCred() {
+ // Initialize to -1 instead of 0 in case a failed getPeerCred()/getsockopt() call somehow doesn't report failure and returns the uid of root
+ pid = -1; uid = -1; gid = -1;
+ }
+
+ /** Set data that was not set by JNI. */
+ public void fillPeerCred(@NonNull Context context) {
+ fillUnameAndGname(context);
+ fillPname(context);
+ }
+
+ /** Set {@link #uname} and {@link #gname} if not set. */
+ public void fillUnameAndGname(@NonNull Context context) {
+ uname = UserUtils.getNameForUid(context, uid);
+
+ if (gid != uid)
+ gname = UserUtils.getNameForUid(context, gid);
+ else
+ gname = uname;
+ }
+
+ /** Set {@link #pname} if not set. */
+ public void fillPname(@NonNull Context context) {
+ // If jni did not set process name since it wouldn't be able to access /proc/ of other
+ // users/apps, then try to see if any app has that pid, but this wouldn't check child
+ // processes of the app.
+ if (pid > 0 && pname == null)
+ pname = ProcessUtils.getAppProcessNameForPid(context, pid);
+ }
+
+ /**
+ * Get a log {@link String} for {@link PeerCred}.
+ *
+ * @param peerCred The {@link PeerCred} to get info of.
+ * @return Returns the log {@link String}.
+ */
+ @NonNull
+ public static String getPeerCredLogString(final PeerCred peerCred) {
+ if (peerCred == null) return "null";
+ return peerCred.getLogString();
+ }
+
+ /** Get a log {@link String} for the {@link PeerCred}. */
+ @NonNull
+ public String getLogString() {
+ StringBuilder logString = new StringBuilder();
+
+ logString.append("Peer Cred:");
+ logString.append("\n").append(Logger.getSingleLineLogStringEntry("Process", getProcessString(), "-"));
+ logString.append("\n").append(Logger.getSingleLineLogStringEntry("User", getUserString(), "-"));
+ logString.append("\n").append(Logger.getSingleLineLogStringEntry("Group", getGroupString(), "-"));
+
+ if (cmdline != null)
+ logString.append("\n").append(Logger.getMultiLineLogStringEntry("Cmdline", cmdline, "-"));
+
+ return logString.toString();
+ }
+
+ /**
+ * Get a markdown {@link String} for {@link PeerCred}.
+ *
+ * @param peerCred The {@link PeerCred} to get info of.
+ * @return Returns the markdown {@link String}.
+ */
+ public static String getPeerCredMarkdownString(final PeerCred peerCred) {
+ if (peerCred == null) return "null";
+ return peerCred.getMarkdownString();
+ }
+
+ /** Get a markdown {@link String} for the {@link PeerCred}. */
+ @NonNull
+ public String getMarkdownString() {
+ StringBuilder markdownString = new StringBuilder();
+
+ markdownString.append("## ").append("Peer Cred");
+ markdownString.append("\n").append(MarkdownUtils.getSingleLineMarkdownStringEntry("Process", getProcessString(), "-"));
+ markdownString.append("\n").append(MarkdownUtils.getSingleLineMarkdownStringEntry("User", getUserString(), "-"));
+ markdownString.append("\n").append(MarkdownUtils.getSingleLineMarkdownStringEntry("Group", getGroupString(), "-"));
+
+ if (cmdline != null)
+ markdownString.append("\n").append(MarkdownUtils.getMultiLineMarkdownStringEntry("Cmdline", cmdline, "-"));
+
+ return markdownString.toString();
+ }
+
+ @NonNull
+ public String getMinimalString() {
+ return "process=" + getProcessString() + ", user=" + getUserString() + ", group=" + getGroupString();
+ }
+
+ @NonNull
+ public String getProcessString() {
+ return pname != null && !pname.isEmpty() ? pid + " (" + pname + ")" : String.valueOf(pid);
+ }
+
+ @NonNull
+ public String getUserString() {
+ return uname != null ? uid + " (" + uname + ")" : String.valueOf(uid);
+ }
+
+ @NonNull
+ public String getGroupString() {
+ return gname != null ? gid + " (" + gname + ")" : String.valueOf(gid);
+ }
+
+}
diff --git a/termux-shared/src/main/java/com/termux/shared/reflection/ReflectionUtils.java b/termux-shared/src/main/java/com/termux/shared/reflection/ReflectionUtils.java
index d4f2ac2876..fadcb7d986 100644
--- a/termux-shared/src/main/java/com/termux/shared/reflection/ReflectionUtils.java
+++ b/termux-shared/src/main/java/com/termux/shared/reflection/ReflectionUtils.java
@@ -86,7 +86,7 @@ public static class FieldInvokeResult {
* {@link Object} value.
*/
@NonNull
- public static FieldInvokeResult invokeField(@NonNull Class clazz, @NonNull String fieldName, T object) {
+ public static FieldInvokeResult invokeField(@NonNull Class extends T> clazz, @NonNull String fieldName, T object) {
try {
Field field = getDeclaredField(clazz, fieldName);
if (field == null) return new FieldInvokeResult(false, null);
diff --git a/termux-shared/src/main/java/com/termux/shared/shell/ArgumentTokenizer.java b/termux-shared/src/main/java/com/termux/shared/shell/ArgumentTokenizer.java
new file mode 100644
index 0000000000..26af2e8322
--- /dev/null
+++ b/termux-shared/src/main/java/com/termux/shared/shell/ArgumentTokenizer.java
@@ -0,0 +1,229 @@
+/*BEGIN_COPYRIGHT_BLOCK
+ *
+ * Copyright (c) 2001-2010, JavaPLT group at Rice University (drjava@rice.edu)
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ * * Neither the names of DrJava, the JavaPLT group, Rice University, nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * This software is Open Source Initiative approved Open Source Software.
+ * Open Source Initative Approved is a trademark of the Open Source Initiative.
+ *
+ * This file is part of DrJava. Download the current version of this project
+ * from http://www.drjava.org/ or http://sourceforge.net/projects/drjava/
+ *
+ * END_COPYRIGHT_BLOCK*/
+
+package com.termux.shared.shell;
+
+import java.util.List;
+import java.util.LinkedList;
+
+/**
+ * Utility class which can tokenize a String into a list of String arguments,
+ * with behavior similar to parsing command line arguments to a program.
+ * Quoted Strings are treated as single arguments, and escaped characters
+ * are translated so that the tokenized arguments have the same meaning.
+ * Since all methods are static, the class is declared abstract to prevent
+ * instantiation.
+ * @version $Id$
+ */
+public abstract class ArgumentTokenizer {
+ private static final int NO_TOKEN_STATE = 0;
+ private static final int NORMAL_TOKEN_STATE = 1;
+ private static final int SINGLE_QUOTE_STATE = 2;
+ private static final int DOUBLE_QUOTE_STATE = 3;
+
+ /** Tokenizes the given String into String tokens
+ * @param arguments A String containing one or more command-line style arguments to be tokenized.
+ * @return A list of parsed and properly escaped arguments.
+ */
+ public static List tokenize(String arguments) {
+ return tokenize(arguments, false);
+ }
+
+ /** Tokenizes the given String into String tokens.
+ * @param arguments A String containing one or more command-line style arguments to be tokenized.
+ * @param stringify whether or not to include escape special characters
+ * @return A list of parsed and properly escaped arguments.
+ */
+ public static List tokenize(String arguments, boolean stringify) {
+
+ LinkedList argList = new LinkedList();
+ StringBuilder currArg = new StringBuilder();
+ boolean escaped = false;
+ int state = NO_TOKEN_STATE; // start in the NO_TOKEN_STATE
+ int len = arguments.length();
+
+ // Loop over each character in the string
+ for (int i = 0; i < len; i++) {
+ char c = arguments.charAt(i);
+ if (escaped) {
+ // Escaped state: just append the next character to the current arg.
+ escaped = false;
+ currArg.append(c);
+ }
+ else {
+ switch(state) {
+ case SINGLE_QUOTE_STATE:
+ if (c == '\'') {
+ // Seen the close quote; continue this arg until whitespace is seen
+ state = NORMAL_TOKEN_STATE;
+ }
+ else {
+ currArg.append(c);
+ }
+ break;
+ case DOUBLE_QUOTE_STATE:
+ if (c == '"') {
+ // Seen the close quote; continue this arg until whitespace is seen
+ state = NORMAL_TOKEN_STATE;
+ }
+ else if (c == '\\') {
+ // Look ahead, and only escape quotes or backslashes
+ i++;
+ char next = arguments.charAt(i);
+ if (next == '"' || next == '\\') {
+ currArg.append(next);
+ }
+ else {
+ currArg.append(c);
+ currArg.append(next);
+ }
+ }
+ else {
+ currArg.append(c);
+ }
+ break;
+// case NORMAL_TOKEN_STATE:
+// if (Character.isWhitespace(c)) {
+// // Whitespace ends the token; start a new one
+// argList.add(currArg.toString());
+// currArg = new StringBuffer();
+// state = NO_TOKEN_STATE;
+// }
+// else if (c == '\\') {
+// // Backslash in a normal token: escape the next character
+// escaped = true;
+// }
+// else if (c == '\'') {
+// state = SINGLE_QUOTE_STATE;
+// }
+// else if (c == '"') {
+// state = DOUBLE_QUOTE_STATE;
+// }
+// else {
+// currArg.append(c);
+// }
+// break;
+ case NO_TOKEN_STATE:
+ case NORMAL_TOKEN_STATE:
+ switch(c) {
+ case '\\':
+ escaped = true;
+ state = NORMAL_TOKEN_STATE;
+ break;
+ case '\'':
+ state = SINGLE_QUOTE_STATE;
+ break;
+ case '"':
+ state = DOUBLE_QUOTE_STATE;
+ break;
+ default:
+ if (!Character.isWhitespace(c)) {
+ currArg.append(c);
+ state = NORMAL_TOKEN_STATE;
+ }
+ else if (state == NORMAL_TOKEN_STATE) {
+ // Whitespace ends the token; start a new one
+ argList.add(currArg.toString());
+ currArg = new StringBuilder();
+ state = NO_TOKEN_STATE;
+ }
+ }
+ break;
+ default:
+ throw new IllegalStateException("ArgumentTokenizer state " + state + " is invalid!");
+ }
+ }
+ }
+
+ // If we're still escaped, put in the backslash
+ if (escaped) {
+ currArg.append('\\');
+ argList.add(currArg.toString());
+ }
+ // Close the last argument if we haven't yet
+ else if (state != NO_TOKEN_STATE) {
+ argList.add(currArg.toString());
+ }
+ // Format each argument if we've been told to stringify them
+ if (stringify) {
+ for (int i = 0; i < argList.size(); i++) {
+ argList.set(i, "\"" + _escapeQuotesAndBackslashes(argList.get(i)) + "\"");
+ }
+ }
+ return argList;
+ }
+
+ /** Inserts backslashes before any occurrences of a backslash or
+ * quote in the given string. Also converts any special characters
+ * appropriately.
+ */
+ protected static String _escapeQuotesAndBackslashes(String s) {
+ final StringBuilder buf = new StringBuilder(s);
+
+ // Walk backwards, looking for quotes or backslashes.
+ // If we see any, insert an extra backslash into the buffer at
+ // the same index. (By walking backwards, the index into the buffer
+ // will remain correct as we change the buffer.)
+ for (int i = s.length()-1; i >= 0; i--) {
+ char c = s.charAt(i);
+ if ((c == '\\') || (c == '"')) {
+ buf.insert(i, '\\');
+ }
+ // Replace any special characters with escaped versions
+ else if (c == '\n') {
+ buf.deleteCharAt(i);
+ buf.insert(i, "\\n");
+ }
+ else if (c == '\t') {
+ buf.deleteCharAt(i);
+ buf.insert(i, "\\t");
+ }
+ else if (c == '\r') {
+ buf.deleteCharAt(i);
+ buf.insert(i, "\\r");
+ }
+ else if (c == '\b') {
+ buf.deleteCharAt(i);
+ buf.insert(i, "\\b");
+ }
+ else if (c == '\f') {
+ buf.deleteCharAt(i);
+ buf.insert(i, "\\f");
+ }
+ }
+ return buf.toString();
+ }
+}
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 0000000000..f94e345311
--- /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 0000000000..74b26528dc
--- /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/shell/command/ExecutionCommand.java b/termux-shared/src/main/java/com/termux/shared/shell/command/ExecutionCommand.java
index 2ee38283e5..a579b313d4 100644
--- a/termux-shared/src/main/java/com/termux/shared/shell/command/ExecutionCommand.java
+++ b/termux-shared/src/main/java/com/termux/shared/shell/command/ExecutionCommand.java
@@ -472,7 +472,7 @@ public static String getExecutionCommandMarkdownString(final ExecutionCommand ex
markdownString.append("\n").append(MarkdownUtils.getSingleLineMarkdownStringEntry("Current State", executionCommand.currentState.getName(), "-"));
markdownString.append("\n").append(MarkdownUtils.getSingleLineMarkdownStringEntry("Executable", executionCommand.executable, "-"));
- markdownString.append("\n").append(getArgumentsMarkdownString(executionCommand.arguments));
+ markdownString.append("\n").append(getArgumentsMarkdownString("Arguments", executionCommand.arguments));
markdownString.append("\n").append(MarkdownUtils.getSingleLineMarkdownStringEntry("Working Directory", executionCommand.workingDirectory, "-"));
markdownString.append("\n").append(MarkdownUtils.getSingleLineMarkdownStringEntry("Runner", executionCommand.runner, "-"));
markdownString.append("\n").append(MarkdownUtils.getSingleLineMarkdownStringEntry("isFailsafe", executionCommand.isFailsafe, "-"));
@@ -547,7 +547,7 @@ public String getExecutableLogString() {
}
public String getArgumentsLogString() {
- return getArgumentsLogString(arguments);
+ return getArgumentsLogString("Arguments", arguments);
}
public String getWorkingDirectoryLogString() {
@@ -623,8 +623,8 @@ public String getIsPluginExecutionCommandLogString() {
* @param argumentsArray The {@link String[]} argumentsArray to convert.
* @return Returns the log friendly {@link String}.
*/
- public static String getArgumentsLogString(final String[] argumentsArray) {
- StringBuilder argumentsString = new StringBuilder("Arguments:");
+ public static String getArgumentsLogString(String label, final String[] argumentsArray) {
+ StringBuilder argumentsString = new StringBuilder(label + ":");
if (argumentsArray != null && argumentsArray.length != 0) {
argumentsString.append("\n```\n");
@@ -660,8 +660,8 @@ public static String getArgumentsLogString(final String[] argumentsArray) {
* @param argumentsArray The {@link String[]} argumentsArray to convert.
* @return Returns the markdown {@link String}.
*/
- public static String getArgumentsMarkdownString(final String[] argumentsArray) {
- StringBuilder argumentsString = new StringBuilder("**Arguments:**");
+ public static String getArgumentsMarkdownString(String label, final String[] argumentsArray) {
+ StringBuilder argumentsString = new StringBuilder("**" + label + ":**");
if (argumentsArray != null && argumentsArray.length != 0) {
argumentsString.append("\n");
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 68ec885cc6..3a33eb52ea 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.List;
/*
- * Version: v0.39.0
+ * Version: v0.41.0
* SPDX-License-Identifier: MIT
*
* Changelog
@@ -216,10 +216,10 @@
* - Added `TERMUX_PROPERTIES_FILE_PATHS_LIST` and `TERMUX_FLOAT_PROPERTIES_FILE_PATHS_LIST`.
*
* - 0.34.0 (2021-10-26)
- * - Move `RESULT_SENDER` to `com.termux.shared.shell.command.ShellCommandConstants`.
+ * - Move `RESULT_SENDER` to `com.termux.shared.shell.command.ShellCommandConstants`.
*
* - 0.35.0 (2022-01-28)
- * - Add `TERMUX_APP.TERMUX_ACTIVITY.EXTRA_RECREATE_ACTIVITY`.
+ * - Add `TERMUX_APP.TERMUX_ACTIVITY.EXTRA_RECREATE_ACTIVITY`.
*
* - 0.36.0 (2022-03-10)
* - Added `TERMUX_APP.TERMUX_SERVICE.EXTRA_RUNNER` and `TERMUX_APP.RUN_COMMAND_SERVICE.EXTRA_RUNNER`
@@ -233,6 +233,12 @@
* - 0.39.0 (2022-03-18)
* - Added `TERMUX_APP.TERMUX_SERVICE.EXTRA_SESSION_NAME`, `TERMUX_APP.RUN_COMMAND_SERVICE.EXTRA_SESSION_NAME`,
* `TERMUX_APP.TERMUX_SERVICE.EXTRA_SESSION_CREATE_MODE` and `TERMUX_APP.RUN_COMMAND_SERVICE.EXTRA_SESSION_CREATE_MODE`.
+ *
+ * - 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`.
*/
/**
@@ -657,6 +663,11 @@ public final class TermuxConstants {
+ /** Termux and plugin apps directory path */
+ public static final String TERMUX_APPS_DIR_PATH = TERMUX_FILES_DIR_PATH + "/apps"; // Default: "/data/data/com.termux/files/apps"
+ /** Termux and plugin apps directory */
+ public static final File TERMUX_APPS_DIR = new File(TERMUX_APPS_DIR_PATH);
+
/*
@@ -872,6 +883,13 @@ public final class TermuxConstants {
*/
public static final class TERMUX_APP {
+ /** 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/crash/TermuxCrashUtils.java b/termux-shared/src/main/java/com/termux/shared/termux/crash/TermuxCrashUtils.java
index 748f569ae4..5d551a9715 100644
--- a/termux-shared/src/main/java/com/termux/shared/termux/crash/TermuxCrashUtils.java
+++ b/termux-shared/src/main/java/com/termux/shared/termux/crash/TermuxCrashUtils.java
@@ -48,11 +48,27 @@ public enum TYPE {
}
/**
- * Set default uncaught crash handler of current thread to {@link CrashHandler} for Termux app
- * and its plugin to log crashes at {@link TermuxConstants#TERMUX_CRASH_LOG_FILE_PATH}.
+ * Set default uncaught crash handler of the app to {@link CrashHandler} for Termux app
+ * and its plugins to log crashes at {@link TermuxConstants#TERMUX_CRASH_LOG_FILE_PATH}.
+ */
+ public static void setDefaultCrashHandler(@NonNull final Context context) {
+ CrashHandler.setDefaultCrashHandler(context, new TermuxCrashUtils(TYPE.UNCAUGHT_EXCEPTION));
+ }
+
+ /**
+ * Set uncaught crash handler of current non-main thread to {@link CrashHandler} for Termux app
+ * and its plugins to log crashes at {@link TermuxConstants#TERMUX_CRASH_LOG_FILE_PATH}.
*/
public static void setCrashHandler(@NonNull final Context context) {
- CrashHandler.setCrashHandler(context, new TermuxCrashUtils(TYPE.UNCAUGHT_EXCEPTION));
+ CrashHandler.setCrashHandler(context, new TermuxCrashUtils(TYPE.CAUGHT_EXCEPTION));
+ }
+
+ /**
+ * Get {@link CrashHandler} for Termux app and its plugins that can be set as the uncaught
+ * crash handler of a non-main thread to log crashes at {@link TermuxConstants#TERMUX_CRASH_LOG_FILE_PATH}.
+ */
+ public static CrashHandler getCrashHandler(@NonNull final Context context) {
+ return CrashHandler.getCrashHandler(context, new TermuxCrashUtils(TYPE.CAUGHT_EXCEPTION));
}
/**
@@ -196,9 +212,9 @@ private static synchronized void notifyAppCrashFromCrashLogFileInner(final Conte
* @param message The message for the crash report.
* @param throwable The {@link Throwable} for the crash report.
*/
- public static void sendPluginCrashReportNotification(final Context currentPackageContext, String logTag,
- CharSequence title, String message, Throwable throwable) {
- TermuxCrashUtils.sendPluginCrashReportNotification(currentPackageContext, logTag,
+ public static void sendCrashReportNotification(final Context currentPackageContext, String logTag,
+ CharSequence title, String message, Throwable throwable) {
+ sendCrashReportNotification(currentPackageContext, logTag,
title, message,
MarkdownUtils.getMarkdownCodeForString(Logger.getMessageAndStackTraceString(message, throwable), true),
false, false, true);
@@ -214,10 +230,10 @@ public static void sendPluginCrashReportNotification(final Context currentPackag
* @param notificationTextString The text of the notification.
* @param message The message for the crash report.
*/
- public static void sendPluginCrashReportNotification(final Context currentPackageContext, String logTag,
- CharSequence title, String notificationTextString,
- String message) {
- TermuxCrashUtils.sendPluginCrashReportNotification(currentPackageContext, logTag,
+ public static void sendCrashReportNotification(final Context currentPackageContext, String logTag,
+ CharSequence title, String notificationTextString,
+ String message) {
+ sendCrashReportNotification(currentPackageContext, logTag,
title, notificationTextString, message,
false, false, true);
}
@@ -238,12 +254,12 @@ public static void sendPluginCrashReportNotification(final Context currentPackag
* @param showToast If set to {@code true}, then a toast will be shown for {@code notificationTextString}.
* @param addDeviceInfo If set to {@code true}, then device info should be appended to the message.
*/
- public static void sendPluginCrashReportNotification(final Context currentPackageContext, String logTag,
- CharSequence title, String notificationTextString,
- String message, boolean forceNotification,
- boolean showToast,
- boolean addDeviceInfo) {
- TermuxCrashUtils.sendCrashReportNotification(currentPackageContext, logTag,
+ public static void sendCrashReportNotification(final Context currentPackageContext, String logTag,
+ CharSequence title, String notificationTextString,
+ String message, boolean forceNotification,
+ boolean showToast,
+ boolean addDeviceInfo) {
+ sendCrashReportNotification(currentPackageContext, logTag,
title, notificationTextString, "## " + title + "\n\n" + message + "\n\n",
forceNotification, showToast, TermuxUtils.AppInfoMode.TERMUX_AND_PLUGIN_PACKAGE, addDeviceInfo);
}
diff --git a/termux-shared/src/main/java/com/termux/shared/termux/file/TermuxFileUtils.java b/termux-shared/src/main/java/com/termux/shared/termux/file/TermuxFileUtils.java
index 79770e790f..68c547dc25 100644
--- a/termux-shared/src/main/java/com/termux/shared/termux/file/TermuxFileUtils.java
+++ b/termux-shared/src/main/java/com/termux/shared/termux/file/TermuxFileUtils.java
@@ -307,6 +307,24 @@ public static Error isTermuxPrefixStagingDirectoryAccessible(boolean createDirec
false, false);
}
+ /**
+ * Validate if {@link TermuxConstants.TERMUX_APP#APPS_DIR_PATH} exists and has
+ * {@link FileUtils#APP_WORKING_DIRECTORY_PERMISSIONS} permissions.
+ *
+ * @param createDirectoryIfMissing The {@code boolean} that decides if directory file
+ * should be created if its missing.
+ * @param setMissingPermissions The {@code boolean} that decides if permissions are to be
+ * automatically set.
+ * @return Returns the {@code error} if path is not a directory file, failed to create it,
+ * or validating permissions failed, otherwise {@code null}.
+ */
+ public static Error isAppsTermuxAppDirectoryAccessible(boolean createDirectoryIfMissing, boolean setMissingPermissions) {
+ return FileUtils.validateDirectoryFileExistenceAndPermissions("apps/termux-app directory", TermuxConstants.TERMUX_APP.APPS_DIR_PATH,
+ null, createDirectoryIfMissing,
+ FileUtils.APP_WORKING_DIRECTORY_PERMISSIONS, setMissingPermissions, true,
+ false, false);
+ }
+
/**
* Get a markdown {@link String} for stat output for various Termux app files paths.
*
diff --git a/termux-shared/src/main/java/com/termux/shared/termux/models/UserAction.java b/termux-shared/src/main/java/com/termux/shared/termux/models/UserAction.java
index fe89d0f8ae..b7e1c56958 100644
--- a/termux-shared/src/main/java/com/termux/shared/termux/models/UserAction.java
+++ b/termux-shared/src/main/java/com/termux/shared/termux/models/UserAction.java
@@ -2,7 +2,8 @@
public enum UserAction {
- CRASH_REPORT("crash report");
+ CRASH_REPORT("crash report"),
+ PLUGIN_EXECUTION_COMMAND("plugin execution command");
private final String name;
diff --git a/app/src/main/java/com/termux/app/utils/PluginUtils.java b/termux-shared/src/main/java/com/termux/shared/termux/plugins/TermuxPluginUtils.java
similarity index 70%
rename from app/src/main/java/com/termux/app/utils/PluginUtils.java
rename to termux-shared/src/main/java/com/termux/shared/termux/plugins/TermuxPluginUtils.java
index a407747fb5..ed2d9a2016 100644
--- a/app/src/main/java/com/termux/app/utils/PluginUtils.java
+++ b/termux-shared/src/main/java/com/termux/shared/termux/plugins/TermuxPluginUtils.java
@@ -1,14 +1,15 @@
-package com.termux.app.utils;
+package com.termux.shared.termux.plugins;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
+import android.graphics.drawable.Icon;
import android.os.Environment;
import androidx.annotation.Nullable;
-import com.termux.R;
+import com.termux.shared.R;
import com.termux.shared.activities.ReportActivity;
import com.termux.shared.file.FileUtils;
import com.termux.shared.termux.file.TermuxFileUtils;
@@ -17,6 +18,7 @@
import com.termux.shared.errors.Errno;
import com.termux.shared.errors.Error;
import com.termux.shared.notification.NotificationUtils;
+import com.termux.shared.termux.models.UserAction;
import com.termux.shared.termux.notification.TermuxNotificationUtils;
import com.termux.shared.termux.settings.preferences.TermuxPreferenceConstants;
import com.termux.shared.shell.command.result.ResultSender;
@@ -30,14 +32,13 @@
import com.termux.shared.models.ReportInfo;
import com.termux.shared.termux.settings.properties.TermuxAppSharedProperties;
import com.termux.shared.shell.command.ExecutionCommand;
-import com.termux.app.models.UserAction;
import com.termux.shared.data.DataUtils;
import com.termux.shared.markdown.MarkdownUtils;
import com.termux.shared.termux.TermuxUtils;
-public class PluginUtils {
+public class TermuxPluginUtils {
- private static final String LOG_TAG = "PluginUtils";
+ private static final String LOG_TAG = "TermuxPluginUtils";
/**
* Process {@link ExecutionCommand} result.
@@ -92,9 +93,8 @@ public static void processPluginExecutionCommandResult(final Context context, St
sendPluginCommandErrorNotification(context, logTag, null,
ResultData.getErrorsListMinimalString(resultData),
ExecutionCommand.getExecutionCommandMarkdownString(executionCommand),
- false, true, TermuxUtils.AppInfoMode.TERMUX_AND_CALLING_PACKAGE,
- executionCommand.resultConfig.resultPendingIntent != null ? executionCommand.resultConfig.resultPendingIntent.getCreatorPackage(): null,
- true);
+ false, true, TermuxUtils.AppInfoMode.TERMUX_AND_CALLING_PACKAGE,true,
+ executionCommand.resultConfig.resultPendingIntent != null ? executionCommand.resultConfig.resultPendingIntent.getCreatorPackage(): null);
}
}
@@ -144,7 +144,9 @@ public static void processPluginExecutionCommandError(final Context context, Str
boolean isExecutionCommandLoggingEnabled = Logger.shouldEnableLoggingForCustomLogLevel(executionCommand.backgroundCustomLogLevel);
// Log the error and any exception. ResultData should not be logged if pending result since ResultSender will do it
- Logger.logErrorExtended(logTag, ExecutionCommand.getExecutionOutputLogString(executionCommand, true,
+ Logger.logError(logTag, "Processing plugin execution error for:\n" + executionCommand.getCommandIdAndLabelLogString());
+ Logger.logError(logTag, "Set log level to debug or higher to see error in logs");
+ Logger.logErrorPrivateExtended(logTag, ExecutionCommand.getExecutionOutputLogString(executionCommand, true,
!isPluginExecutionCommandWithPendingResult, isExecutionCommandLoggingEnabled));
// If execution command was started by a plugin which expects the result back
@@ -161,7 +163,7 @@ public static void processPluginExecutionCommandError(final Context context, Str
if (error != null) {
// error will be added to existing Errors
resultData.setStateFailed(error);
- Logger.logErrorExtended(logTag, ExecutionCommand.getExecutionOutputLogString(executionCommand, true, true, isExecutionCommandLoggingEnabled));
+ Logger.logErrorPrivateExtended(logTag, ExecutionCommand.getExecutionOutputLogString(executionCommand, true, true, isExecutionCommandLoggingEnabled));
forceNotification = true;
}
@@ -173,9 +175,8 @@ public static void processPluginExecutionCommandError(final Context context, Str
sendPluginCommandErrorNotification(context, logTag, null,
ResultData.getErrorsListMinimalString(resultData),
ExecutionCommand.getExecutionCommandMarkdownString(executionCommand),
- forceNotification, true, TermuxUtils.AppInfoMode.TERMUX_AND_CALLING_PACKAGE,
- executionCommand.resultConfig.resultPendingIntent != null ? executionCommand.resultConfig.resultPendingIntent.getCreatorPackage(): null,
- true);
+ forceNotification, true, TermuxUtils.AppInfoMode.TERMUX_AND_CALLING_PACKAGE, true,
+ executionCommand.resultConfig.resultPendingIntent != null ? executionCommand.resultConfig.resultPendingIntent.getCreatorPackage(): null);
}
/** Set variables which will be used by {@link ResultSender#sendCommandResultData(Context, String, String, ResultConfig, ResultData, boolean)}
@@ -208,11 +209,75 @@ public static void setPluginResultDirectoryVariables(ExecutionCommand executionC
+
/**
- * Send an error notification for {@link TermuxConstants#TERMUX_PLUGIN_COMMAND_ERRORS_NOTIFICATION_CHANNEL_ID}
+ * Send a plugin error report notification for {@link TermuxConstants#TERMUX_PLUGIN_COMMAND_ERRORS_NOTIFICATION_CHANNEL_ID}
* and {@link TermuxConstants#TERMUX_PLUGIN_COMMAND_ERRORS_NOTIFICATION_CHANNEL_NAME}.
*
- * @param context The {@link Context} for operations.
+ * @param currentPackageContext The {@link Context} of current package.
+ * @param logTag The log tag to use for logging.
+ * @param title The title for the error report and notification.
+ * @param message The message for the error report.
+ * @param throwable The {@link Throwable} for the error report.
+ */
+ public static void sendPluginCommandErrorNotification(final Context currentPackageContext, String logTag,
+ CharSequence title, String message, Throwable throwable) {
+ sendPluginCommandErrorNotification(currentPackageContext, logTag,
+ title, message,
+ MarkdownUtils.getMarkdownCodeForString(Logger.getMessageAndStackTraceString(message, throwable), true),
+ false, false, true);
+ }
+
+ /**
+ * Send a plugin error report notification for {@link TermuxConstants#TERMUX_PLUGIN_COMMAND_ERRORS_NOTIFICATION_CHANNEL_ID}
+ * and {@link TermuxConstants#TERMUX_PLUGIN_COMMAND_ERRORS_NOTIFICATION_CHANNEL_NAME}.
+ *
+ * @param currentPackageContext The {@link Context} of current package.
+ * @param logTag The log tag to use for logging.
+ * @param title The title for the error report and notification.
+ * @param notificationTextString The text of the notification.
+ * @param message The message for the error report.
+ */
+ public static void sendPluginCommandErrorNotification(final Context currentPackageContext, String logTag,
+ CharSequence title, String notificationTextString,
+ String message) {
+ sendPluginCommandErrorNotification(currentPackageContext, logTag,
+ title, notificationTextString, message,
+ false, false, true);
+ }
+
+ /**
+ * Send a plugin error report notification for {@link TermuxConstants#TERMUX_PLUGIN_COMMAND_ERRORS_NOTIFICATION_CHANNEL_ID}
+ * and {@link TermuxConstants#TERMUX_PLUGIN_COMMAND_ERRORS_NOTIFICATION_CHANNEL_NAME}.
+ *
+ * @param currentPackageContext The {@link Context} of current package.
+ * @param logTag The log tag to use for logging.
+ * @param title The title for the error report and notification.
+ * @param notificationTextString The text of the notification.
+ * @param message The message for the error report.
+ * @param forceNotification If set to {@code true}, then a notification will be shown
+ * regardless of if pending intent is {@code null} or
+ * {@link TermuxPreferenceConstants.TERMUX_APP#KEY_PLUGIN_ERROR_NOTIFICATIONS_ENABLED}
+ * is {@code false}.
+ * @param showToast If set to {@code true}, then a toast will be shown for {@code notificationTextString}.
+ * @param addDeviceInfo If set to {@code true}, then device info should be appended to the message.
+ */
+ public static void sendPluginCommandErrorNotification(final Context currentPackageContext, String logTag,
+ CharSequence title, String notificationTextString,
+ String message, boolean forceNotification,
+ boolean showToast,
+ boolean addDeviceInfo) {
+ sendPluginCommandErrorNotification(currentPackageContext, logTag,
+ title, notificationTextString, "## " + title + "\n\n" + message + "\n\n",
+ forceNotification, showToast, TermuxUtils.AppInfoMode.TERMUX_AND_PLUGIN_PACKAGE, addDeviceInfo, null);
+ }
+
+ /**
+ * Send a plugin error notification for {@link TermuxConstants#TERMUX_PLUGIN_COMMAND_ERRORS_NOTIFICATION_CHANNEL_ID}
+ * and {@link TermuxConstants#TERMUX_PLUGIN_COMMAND_ERRORS_NOTIFICATION_CHANNEL_NAME}.
+ *
+ * @param currentPackageContext The {@link Context} of current package.
+ * @param logTag The log tag to use for logging.
* @param title The title for the error report and notification.
* @param notificationTextString The text of the notification.
* @param message The message for the error report.
@@ -223,21 +288,30 @@ public static void setPluginResultDirectoryVariables(ExecutionCommand executionC
* @param showToast If set to {@code true}, then a toast will be shown for {@code notificationTextString}.
* @param appInfoMode The {@link TermuxUtils.AppInfoMode} to use to add app info to the message.
* Set to {@code null} if app info should not be appended to the message.
+ * @param addDeviceInfo If set to {@code true}, then device info should be appended to the message.
* @param callingPackageName The optional package name of the app for which the plugin command
* was run.
- * @param addDeviceInfo If set to {@code true}, then device info should be appended to the message.
*/
- public static void sendPluginCommandErrorNotification(Context context, String logTag,
+ public static void sendPluginCommandErrorNotification(Context currentPackageContext, String logTag,
CharSequence title,
String notificationTextString,
String message, boolean forceNotification,
boolean showToast,
TermuxUtils.AppInfoMode appInfoMode,
- String callingPackageName,
- boolean addDeviceInfo) {
- if (context == null) return;
+ boolean addDeviceInfo,
+ String callingPackageName) {
+ // Note: Do not change currentPackageContext or termuxPackageContext passed to functions or things will break
+
+ if (currentPackageContext == null) return;
+ String currentPackageName = currentPackageContext.getPackageName();
- TermuxAppSharedPreferences preferences = TermuxAppSharedPreferences.build(context);
+ final Context termuxPackageContext = TermuxUtils.getTermuxPackageContext(currentPackageContext);
+ if (termuxPackageContext == null) {
+ Logger.logWarn(LOG_TAG, "Ignoring call to sendPluginCommandErrorNotification() since failed to get \"" + TermuxConstants.TERMUX_PACKAGE_NAME + "\" package context from \"" + currentPackageName + "\" context");
+ return;
+ }
+
+ TermuxAppSharedPreferences preferences = TermuxAppSharedPreferences.build(termuxPackageContext);
if (preferences == null) return;
// If user has disabled notifications for plugin commands, then just return
@@ -247,7 +321,7 @@ public static void sendPluginCommandErrorNotification(Context context, String lo
logTag = DataUtils.getDefaultIfNull(logTag, LOG_TAG);
if (showToast)
- Logger.showToast(context, notificationTextString, true);
+ Logger.showToast(currentPackageContext, notificationTextString, true);
// Send a notification to show the error which when clicked will open the ReportActivity
// to show the details of the error
@@ -259,48 +333,49 @@ public static void sendPluginCommandErrorNotification(Context context, String lo
StringBuilder reportString = new StringBuilder(message);
if (appInfoMode != null)
- reportString.append("\n\n").append(TermuxUtils.getAppInfoMarkdownString(context, appInfoMode, callingPackageName));
+ reportString.append("\n\n").append(TermuxUtils.getAppInfoMarkdownString(currentPackageContext, appInfoMode,
+ callingPackageName != null ? callingPackageName : currentPackageName));
if (addDeviceInfo)
- reportString.append("\n\n").append(AndroidUtils.getDeviceInfoMarkdownString(context));
+ reportString.append("\n\n").append(AndroidUtils.getDeviceInfoMarkdownString(currentPackageContext));
String userActionName = UserAction.PLUGIN_EXECUTION_COMMAND.getName();
ReportInfo reportInfo = new ReportInfo(userActionName, logTag, title.toString());
reportInfo.setReportString(reportString.toString());
- reportInfo.setReportStringSuffix("\n\n" + TermuxUtils.getReportIssueMarkdownString(context));
+ reportInfo.setReportStringSuffix("\n\n" + TermuxUtils.getReportIssueMarkdownString(currentPackageContext));
reportInfo.setAddReportInfoHeaderToMarkdown(true);
reportInfo.setReportSaveFileLabelAndPath(userActionName,
Environment.getExternalStorageDirectory() + "/" +
FileUtils.sanitizeFileName(TermuxConstants.TERMUX_APP_NAME + "-" + userActionName + ".log", true, true));
- ReportActivity.NewInstanceResult result = ReportActivity.newInstance(context, reportInfo);
+ ReportActivity.NewInstanceResult result = ReportActivity.newInstance(termuxPackageContext, reportInfo);
if (result.contentIntent == null) return;
// Must ensure result code for PendingIntents and id for notification are unique otherwise will override previous
- int nextNotificationId = TermuxNotificationUtils.getNextNotificationId(context);
+ int nextNotificationId = TermuxNotificationUtils.getNextNotificationId(termuxPackageContext);
- PendingIntent contentIntent = PendingIntent.getActivity(context, nextNotificationId, result.contentIntent, PendingIntent.FLAG_UPDATE_CURRENT);
+ PendingIntent contentIntent = PendingIntent.getActivity(termuxPackageContext, nextNotificationId, result.contentIntent, PendingIntent.FLAG_UPDATE_CURRENT);
PendingIntent deleteIntent = null;
if (result.deleteIntent != null)
- deleteIntent = PendingIntent.getBroadcast(context, nextNotificationId, result.deleteIntent, PendingIntent.FLAG_UPDATE_CURRENT);
+ deleteIntent = PendingIntent.getBroadcast(termuxPackageContext, nextNotificationId, result.deleteIntent, PendingIntent.FLAG_UPDATE_CURRENT);
// Setup the notification channel if not already set up
- setupPluginCommandErrorsNotificationChannel(context);
+ setupPluginCommandErrorsNotificationChannel(termuxPackageContext);
// Use markdown in notification
- CharSequence notificationTextCharSequence = MarkdownUtils.getSpannedMarkdownText(context, notificationTextString);
+ CharSequence notificationTextCharSequence = MarkdownUtils.getSpannedMarkdownText(termuxPackageContext, notificationTextString);
//CharSequence notificationTextCharSequence = notificationTextString;
// Build the notification
- Notification.Builder builder = getPluginCommandErrorsNotificationBuilder(context, title,
- notificationTextCharSequence, notificationTextCharSequence, contentIntent, deleteIntent,
+ Notification.Builder builder = getPluginCommandErrorsNotificationBuilder(currentPackageContext, termuxPackageContext,
+ title, notificationTextCharSequence, notificationTextCharSequence, contentIntent, deleteIntent,
NotificationUtils.NOTIFICATION_MODE_VIBRATE);
if (builder == null) return;
// Send the notification
- NotificationManager notificationManager = NotificationUtils.getNotificationManager(context);
+ NotificationManager notificationManager = NotificationUtils.getNotificationManager(termuxPackageContext);
if (notificationManager != null)
notificationManager.notify(nextNotificationId, builder.build());
}
@@ -309,7 +384,8 @@ public static void sendPluginCommandErrorNotification(Context context, String lo
* Get {@link Notification.Builder} for {@link TermuxConstants#TERMUX_PLUGIN_COMMAND_ERRORS_NOTIFICATION_CHANNEL_ID}
* and {@link TermuxConstants#TERMUX_PLUGIN_COMMAND_ERRORS_NOTIFICATION_CHANNEL_NAME}.
*
- * @param context The {@link Context} for operations.
+ * @param currentPackageContext The {@link Context} of current package.
+ * @param termuxPackageContext The {@link Context} of termux package.
* @param title The title for the notification.
* @param notificationText The second line text of the notification.
* @param notificationBigText The full text of the notification that may optionally be styled.
@@ -319,11 +395,15 @@ public static void sendPluginCommandErrorNotification(Context context, String lo
* @return Returns the {@link Notification.Builder}.
*/
@Nullable
- public static Notification.Builder getPluginCommandErrorsNotificationBuilder(
- final Context context, final CharSequence title, final CharSequence notificationText,
- final CharSequence notificationBigText, final PendingIntent contentIntent, final PendingIntent deleteIntent, final int notificationMode) {
-
- Notification.Builder builder = NotificationUtils.geNotificationBuilder(context,
+ public static Notification.Builder getPluginCommandErrorsNotificationBuilder(final Context currentPackageContext,
+ final Context termuxPackageContext,
+ final CharSequence title,
+ final CharSequence notificationText,
+ final CharSequence notificationBigText,
+ final PendingIntent contentIntent,
+ final PendingIntent deleteIntent,
+ final int notificationMode) {
+ Notification.Builder builder = NotificationUtils.geNotificationBuilder(termuxPackageContext,
TermuxConstants.TERMUX_PLUGIN_COMMAND_ERRORS_NOTIFICATION_CHANNEL_ID, Notification.PRIORITY_HIGH,
title, notificationText, notificationBigText, contentIntent, deleteIntent, notificationMode);
@@ -333,7 +413,7 @@ public static Notification.Builder getPluginCommandErrorsNotificationBuilder(
builder.setShowWhen(true);
// Set notification icon
- builder.setSmallIcon(R.drawable.ic_error_notification);
+ builder.setSmallIcon(Icon.createWithResource(currentPackageContext, R.drawable.ic_error_notification));
// Set background color for small notification icon
builder.setColor(0xFF607D8B);
diff --git a/termux-shared/src/main/java/com/termux/shared/termux/settings/properties/TermuxPropertyConstants.java b/termux-shared/src/main/java/com/termux/shared/termux/settings/properties/TermuxPropertyConstants.java
index 34e3dfed5d..643b1ddd5b 100644
--- a/termux-shared/src/main/java/com/termux/shared/termux/settings/properties/TermuxPropertyConstants.java
+++ b/termux-shared/src/main/java/com/termux/shared/termux/settings/properties/TermuxPropertyConstants.java
@@ -1,6 +1,7 @@
package com.termux.shared.termux.settings.properties;
import com.google.common.collect.ImmutableBiMap;
+import com.termux.shared.termux.shell.am.TermuxAmSocketServer;
import com.termux.shared.theme.NightMode;
import com.termux.shared.file.FileUtils;
import com.termux.shared.file.filesystem.FileType;
@@ -120,6 +121,11 @@ public final class TermuxPropertyConstants {
+ /** Defines the key for whether the {@link TermuxAmSocketServer} should be run at app startup */
+ public static final String KEY_RUN_TERMUX_AM_SOCKET_SERVER = "run-termux-am-socket-server"; // Default: "run-termux-am-socket-server"
+
+
+
/** Defines the key for whether url links in terminal transcript will automatically open on click or on tap */
public static final String KEY_TERMINAL_ONCLICK_URL_OPEN = "terminal-onclick-url-open"; // Default: "terminal-onclick-url-open"
@@ -379,6 +385,7 @@ public final class TermuxPropertyConstants {
KEY_ENFORCE_CHAR_BASED_INPUT,
KEY_EXTRA_KEYS_TEXT_ALL_CAPS,
KEY_HIDE_SOFT_KEYBOARD_ON_STARTUP,
+ KEY_RUN_TERMUX_AM_SOCKET_SERVER,
KEY_TERMINAL_ONCLICK_URL_OPEN,
KEY_USE_CTRL_SPACE_WORKAROUND,
KEY_USE_FULLSCREEN,
@@ -436,7 +443,8 @@ public final class TermuxPropertyConstants {
* default: true
*/
public static final Set TERMUX_DEFAULT_TRUE_BOOLEAN_BEHAVIOUR_PROPERTIES_LIST = new HashSet<>(Arrays.asList(
- KEY_EXTRA_KEYS_TEXT_ALL_CAPS
+ KEY_EXTRA_KEYS_TEXT_ALL_CAPS,
+ KEY_RUN_TERMUX_AM_SOCKET_SERVER
));
/** Defines the set for keys loaded by termux that have default inverted boolean behaviour with false as default.
diff --git a/termux-shared/src/main/java/com/termux/shared/termux/settings/properties/TermuxSharedProperties.java b/termux-shared/src/main/java/com/termux/shared/termux/settings/properties/TermuxSharedProperties.java
index 4b1b7bfd80..ad863e9c66 100644
--- a/termux-shared/src/main/java/com/termux/shared/termux/settings/properties/TermuxSharedProperties.java
+++ b/termux-shared/src/main/java/com/termux/shared/termux/settings/properties/TermuxSharedProperties.java
@@ -583,6 +583,10 @@ public boolean shouldSoftKeyboardBeHiddenOnStartup() {
return (boolean) getInternalPropertyValue(TermuxPropertyConstants.KEY_HIDE_SOFT_KEYBOARD_ON_STARTUP, true);
}
+ public boolean shouldRunTermuxAmSocketServer() {
+ return (boolean) getInternalPropertyValue(TermuxPropertyConstants.KEY_RUN_TERMUX_AM_SOCKET_SERVER, true);
+ }
+
public boolean shouldOpenTerminalTranscriptURLOnClick() {
return (boolean) getInternalPropertyValue(TermuxPropertyConstants.KEY_TERMINAL_ONCLICK_URL_OPEN, true);
}
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 f3771f43a6..8ac6d2586f 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 static String[] buildEnvironment(Context currentPackageContext, boolean i
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 0000000000..32116b3083
--- /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;
+ }
+
+ }
+
+}
diff --git a/termux-shared/src/main/res/values/strings.xml b/termux-shared/src/main/res/values/strings.xml
index 5a70aab4b8..a0111fc3d1 100644
--- a/termux-shared/src/main/res/values/strings.xml
+++ b/termux-shared/src/main/res/values/strings.xml
@@ -39,6 +39,12 @@
+
+ %1$s requires `allow-external-apps`
+ property to be set to `true` in `%2$s` file.
+
+
+
Report Text
**Report Truncated**\n\nReport is too large to view here.