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 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.