From 03e1d14e1e65e054eeeb0ad672a46c2c17254d3e Mon Sep 17 00:00:00 2001 From: agnostic-apollo Date: Sun, 12 Jun 2022 00:51:19 +0500 Subject: [PATCH] Added: Write termux shell environment to `/data/data/com.termux/files/usr/etc/termux/termux.env` on app startup and package changes The `termux.env` can be sourced by shells to set termux environment normally exported. This can be useful for users starting termux shells with `adb` `run-as` or `root`. The file will not contain `SHELL_CMD__` variables since those are shell command specific. The items in the `termux.env` file have the format `export name="value"`. The `"`\$` characters will be escaped with `a backslash `\`, like `\"` if characters are for literal value. Note that if `$` is escaped and if its part of variable, then variable expansion will not happen if `.env` file is sourced. The `\` at the end of a value line means line continuation. Value can contain newline characters. The `termux.env` file should be sourceable by `POSIX` compliant shells like `bash`, `zsh`, `sh`, android's `mksh`, etc. Other shells with require manual parsing of the file to export variables. Related discussion #2565 --- .../com/termux/app/TermuxApplication.java | 13 +++-- .../java/com/termux/app/TermuxService.java | 6 +++ .../termux/app/event/SystemEventReceiver.java | 44 +++++++++++++++ .../environment/ShellEnvironmentUtils.java | 54 +++++++++++++++++++ .../environment/ShellEnvironmentVariable.java | 28 ++++++++++ .../termux/shared/termux/TermuxConstants.java | 18 ++++++- .../com/termux/shared/termux/TermuxUtils.java | 12 +++++ .../environment/TermuxShellEnvironment.java | 26 +++++++++ 8 files changed, 196 insertions(+), 5 deletions(-) create mode 100644 termux-shared/src/main/java/com/termux/shared/shell/command/environment/ShellEnvironmentVariable.java diff --git a/app/src/main/java/com/termux/app/TermuxApplication.java b/app/src/main/java/com/termux/app/TermuxApplication.java index 1688ab0b..1123abf6 100644 --- a/app/src/main/java/com/termux/app/TermuxApplication.java +++ b/app/src/main/java/com/termux/app/TermuxApplication.java @@ -12,6 +12,7 @@ import com.termux.shared.termux.crash.TermuxCrashUtils; import com.termux.shared.termux.file.TermuxFileUtils; import com.termux.shared.termux.settings.preferences.TermuxAppSharedPreferences; import com.termux.shared.termux.settings.properties.TermuxAppSharedProperties; +import com.termux.shared.termux.shell.command.environment.TermuxShellEnvironment; import com.termux.shared.termux.shell.am.TermuxAmSocketServer; import com.termux.shared.termux.shell.TermuxShellManager; import com.termux.shared.termux.theme.TermuxThemeUtils; @@ -48,9 +49,8 @@ public class TermuxApplication extends Application { // 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 { + boolean isTermuxFilesDirectoryAccessible = error == null; + if (isTermuxFilesDirectoryAccessible) { Logger.logInfo(LOG_TAG, "Termux files directory is accessible"); error = TermuxFileUtils.isAppsTermuxAppDirectoryAccessible(true, true); @@ -59,10 +59,17 @@ public class TermuxApplication extends Application { return; } + // Setup termux-am-socket server TermuxAmSocketServer.setupTermuxAmSocketServer(context); + } else { + Logger.logErrorExtended(LOG_TAG, "Termux files directory is not accessible\n" + error); + } // Init TermuxShellEnvironment constants and caches after everything has been setup including termux-am-socket server TermuxShellEnvironment.init(this); + + if (isTermuxFilesDirectoryAccessible) { + TermuxShellEnvironment.writeEnvironmentToFile(this); } } diff --git a/app/src/main/java/com/termux/app/TermuxService.java b/app/src/main/java/com/termux/app/TermuxService.java index 4e947916..31581a6c 100644 --- a/app/src/main/java/com/termux/app/TermuxService.java +++ b/app/src/main/java/com/termux/app/TermuxService.java @@ -19,6 +19,7 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.termux.R; +import com.termux.app.event.SystemEventReceiver; import com.termux.app.terminal.TermuxTerminalSessionClient; import com.termux.shared.termux.plugins.TermuxPluginUtils; import com.termux.shared.data.IntentUtils; @@ -116,6 +117,8 @@ public final class TermuxService extends Service implements AppShell.AppShellCli mShellManager = TermuxShellManager.getShellManager(); runStartForeground(); + + SystemEventReceiver.registerPackageUpdateEvents(this); } @SuppressLint("Wakelock") @@ -172,6 +175,9 @@ public final class TermuxService extends Service implements AppShell.AppShellCli killAllTermuxExecutionCommands(); TermuxShellManager.onAppExit(this); + + SystemEventReceiver.unregisterPackageUpdateEvents(this); + runStopForeground(); } diff --git a/app/src/main/java/com/termux/app/event/SystemEventReceiver.java b/app/src/main/java/com/termux/app/event/SystemEventReceiver.java index 49a3c172..efc710f3 100644 --- a/app/src/main/java/com/termux/app/event/SystemEventReceiver.java +++ b/app/src/main/java/com/termux/app/event/SystemEventReceiver.java @@ -2,12 +2,18 @@ package com.termux.app.event; import android.content.BroadcastReceiver; import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.net.Uri; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.termux.shared.data.IntentUtils; import com.termux.shared.logger.Logger; +import com.termux.shared.termux.TermuxUtils; +import com.termux.shared.termux.file.TermuxFileUtils; +import com.termux.shared.termux.shell.command.environment.TermuxShellEnvironment; import com.termux.shared.termux.shell.TermuxShellManager; public class SystemEventReceiver extends BroadcastReceiver { @@ -35,6 +41,11 @@ public class SystemEventReceiver extends BroadcastReceiver { case Intent.ACTION_BOOT_COMPLETED: onActionBootCompleted(context, intent); break; + case Intent.ACTION_PACKAGE_ADDED: + case Intent.ACTION_PACKAGE_REMOVED: + case Intent.ACTION_PACKAGE_REPLACED: + onActionPackageUpdated(context, intent); + break; default: Logger.logError(LOG_TAG, "Invalid action \"" + action + "\" passed to " + LOG_TAG); } @@ -44,4 +55,37 @@ public class SystemEventReceiver extends BroadcastReceiver { TermuxShellManager.onActionBootCompleted(context, intent); } + public synchronized void onActionPackageUpdated(@NonNull Context context, @NonNull Intent intent) { + Uri data = intent.getData(); + if (data != null && TermuxUtils.isUriDataForTermuxPluginPackage(data)) { + Logger.logDebug(LOG_TAG, intent.getAction().replaceAll("^android.intent.action.", "") + + " event received for \"" + data.toString().replaceAll("^package:", "") + "\""); + if (TermuxFileUtils.isTermuxFilesDirectoryAccessible(context, false, false) == null) + TermuxShellEnvironment.writeEnvironmentToFile(context); + } + } + + + + /** + * Register {@link SystemEventReceiver} to listen to {@link Intent#ACTION_PACKAGE_ADDED}, + * {@link Intent#ACTION_PACKAGE_REMOVED} and {@link Intent#ACTION_PACKAGE_REPLACED} broadcasts. + * They must be registered dynamically and cannot be registered implicitly in + * the AndroidManifest.xml due to Android 8+ restrictions. + * + * https://developer.android.com/guide/components/broadcast-exceptions + */ + public synchronized static void registerPackageUpdateEvents(@NonNull Context context) { + IntentFilter intentFilter = new IntentFilter(); + intentFilter.addAction(Intent.ACTION_PACKAGE_ADDED); + intentFilter.addAction(Intent.ACTION_PACKAGE_REMOVED); + intentFilter.addAction(Intent.ACTION_PACKAGE_REPLACED); + intentFilter.addDataScheme("package"); + context.registerReceiver(getInstance(), intentFilter); + } + + public synchronized static void unregisterPackageUpdateEvents(@NonNull Context context) { + context.unregisterReceiver(getInstance()); + } + } diff --git a/termux-shared/src/main/java/com/termux/shared/shell/command/environment/ShellEnvironmentUtils.java b/termux-shared/src/main/java/com/termux/shared/shell/command/environment/ShellEnvironmentUtils.java index 03ec6ab5..9b331c33 100644 --- a/termux-shared/src/main/java/com/termux/shared/shell/command/environment/ShellEnvironmentUtils.java +++ b/termux-shared/src/main/java/com/termux/shared/shell/command/environment/ShellEnvironmentUtils.java @@ -42,6 +42,60 @@ public class ShellEnvironmentUtils { return environmentList; } + /** + * Convert environment {@link HashMap} to {@link String} where each item equals "key=value". + * + */ + @NonNull + public static String convertEnvironmentToDotEnvFile(@NonNull HashMap environmentMap) { + return convertEnvironmentToDotEnvFile(convertEnvironmentMapToEnvironmentVariableList(environmentMap)); + } + + /** + * Convert environment {@link HashMap} to `.env` file {@link String}. + * + * The items in the `.env` file have the format `export name="value"`. + * + * If the {@link ShellEnvironmentVariable#escaped} is set to {@code true}, then + * {@link ShellEnvironmentVariable#value} will be considered to be a literal value that has + * already been escaped by the caller, otherwise all the `"`\$` in the value will be escaped + * with `a backslash `\`, like `\"`. Note that if `$` is escaped and if its part of variable, + * then variable expansion will not happen if `.env` file is sourced. + * + * The `\` at the end of a value line means line continuation. Value can contain newline characters. + * + * Check {@link #isValidEnvironmentVariableName(String)} and {@link #isValidEnvironmentVariableValue(String)} + * for valid variable names and values. + * + * https://github.com/ko1nksm/shdotenv#env-file-syntax + * https://github.com/ko1nksm/shdotenv/blob/main/docs/specification.md + */ + @NonNull + public static String convertEnvironmentToDotEnvFile(@NonNull List environmentList) { + StringBuilder environment = new StringBuilder(); + Collections.sort(environmentList); + for (ShellEnvironmentVariable variable : environmentList) { + if (isValidEnvironmentVariableNameValuePair(variable.name, variable.value, true) && variable.value != null) { + environment.append("export ").append(variable.name).append("=\"") + .append(variable.escaped ? variable.value : variable.value.replaceAll("([\"`\\\\$])", "\\\\$1")) + .append("\"\n"); + } + } + return environment.toString(); + } + + /** + * Convert environment {@link HashMap} to {@link List< ShellEnvironmentVariable >}. Each item + * will have its {@link ShellEnvironmentVariable#escaped} set to {@code false}. + */ + @NonNull + public static List convertEnvironmentMapToEnvironmentVariableList(@NonNull HashMap environmentMap) { + List environmentList = new ArrayList<>(); + for (String name :environmentMap.keySet()) { + environmentList.add(new ShellEnvironmentVariable(name, environmentMap.get(name), false)); + } + return environmentList; + } /** * Check if environment variable name and value pair is valid. Errors will be logged if diff --git a/termux-shared/src/main/java/com/termux/shared/shell/command/environment/ShellEnvironmentVariable.java b/termux-shared/src/main/java/com/termux/shared/shell/command/environment/ShellEnvironmentVariable.java new file mode 100644 index 00000000..f4abee3a --- /dev/null +++ b/termux-shared/src/main/java/com/termux/shared/shell/command/environment/ShellEnvironmentVariable.java @@ -0,0 +1,28 @@ +package com.termux.shared.shell.command.environment; + +public class ShellEnvironmentVariable implements Comparable { + + /** The name for environment variable */ + public String name; + + /** The value for environment variable */ + public String value; + + /** If environment variable {@link #value} is already escaped. */ + public boolean escaped; + + public ShellEnvironmentVariable(String name, String value) { + this(name, value, false); + } + + public ShellEnvironmentVariable(String name, String value, boolean escaped) { + this.name = name; + this.value = value; + this.escaped = escaped; + } + + @Override + public int compareTo(ShellEnvironmentVariable other) { + return this.name.compareTo(other.name); + } +} 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 309ef970..8678027e 100644 --- a/termux-shared/src/main/java/com/termux/shared/termux/TermuxConstants.java +++ b/termux-shared/src/main/java/com/termux/shared/termux/TermuxConstants.java @@ -11,7 +11,7 @@ import java.util.Formatter; import java.util.List; /* - * Version: v0.49.0 + * Version: v0.50.0 * SPDX-License-Identifier: MIT * * Changelog @@ -266,8 +266,11 @@ import java.util.List; * - Removed `TERMUX_GAME_PACKAGES_GITHUB_*`, `TERMUX_SCIENCE_PACKAGES_GITHUB_*`, * `TERMUX_ROOT_PACKAGES_GITHUB_*`, `TERMUX_UNSTABLE_PACKAGES_GITHUB_*` * - * - 0.49.0 (2022-06-10) + * - 0.49.0 (2022-06-11) * - Added `TERMUX_ENV_PREFIX_ROOT`. + * + * - 0.50.0 (2022-06-11) + * - Added `TERMUX_CONFIG_PREFIX_DIR_PATH`, `TERMUX_ENV_FILE_PATH` and `TERMUX_ENV_TEMP_FILE_PATH`. */ /** @@ -650,6 +653,11 @@ public final class TermuxConstants { /** Termux app config home directory */ public static final File TERMUX_CONFIG_HOME_DIR = new File(TERMUX_CONFIG_HOME_DIR_PATH); + /** Termux app config $PREFIX directory path */ + public static final String TERMUX_CONFIG_PREFIX_DIR_PATH = TERMUX_ETC_PREFIX_DIR_PATH + "/termux"; // Default: "/data/data/com.termux/files/usr/etc/termux" + /** Termux app config $PREFIX directory */ + public static final File TERMUX_CONFIG_PREFIX_DIR = new File(TERMUX_CONFIG_PREFIX_DIR_PATH); + /** Termux app data home directory path */ public static final String TERMUX_DATA_HOME_DIR_PATH = TERMUX_HOME_DIR_PATH + "/.termux"; // Default: "/data/data/com.termux/files/home/.termux" @@ -756,6 +764,12 @@ public final class TermuxConstants { public static final String TERMUX_CRASH_LOG_BACKUP_FILE_PATH = TERMUX_HOME_DIR_PATH + "/crash_log_backup.md"; // Default: "/data/data/com.termux/files/home/crash_log_backup.md" + /** Termux app environment file path */ + public static final String TERMUX_ENV_FILE_PATH = TERMUX_CONFIG_PREFIX_DIR_PATH + "/termux.env"; // Default: "/data/data/com.termux/files/usr/etc/termux/termux.env" + + /** Termux app environment temp file path */ + public static final String TERMUX_ENV_TEMP_FILE_PATH = TERMUX_CONFIG_PREFIX_DIR_PATH + "/termux.env.tmp"; // Default: "/data/data/com.termux/files/usr/etc/termux/termux.env.tmp" + diff --git a/termux-shared/src/main/java/com/termux/shared/termux/TermuxUtils.java b/termux-shared/src/main/java/com/termux/shared/termux/TermuxUtils.java index 4104fe5b..793968af 100644 --- a/termux-shared/src/main/java/com/termux/shared/termux/TermuxUtils.java +++ b/termux-shared/src/main/java/com/termux/shared/termux/TermuxUtils.java @@ -6,6 +6,7 @@ import android.content.Intent; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; +import android.net.Uri; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -273,6 +274,17 @@ public class TermuxUtils { + /** Returns {@code true} if {@link Uri} has `package:` scheme for {@link TermuxConstants#TERMUX_PACKAGE_NAME} or its sub plugin package. */ + public static boolean isUriDataForTermuxOrPluginPackage(@NonNull Uri data) { + return data.toString().equals("package:" + TermuxConstants.TERMUX_PACKAGE_NAME) || + data.toString().startsWith("package:" + TermuxConstants.TERMUX_PACKAGE_NAME + "."); + } + + /** Returns {@code true} if {@link Uri} has `package:` scheme for {@link TermuxConstants#TERMUX_PACKAGE_NAME} sub plugin package. */ + public static boolean isUriDataForTermuxPluginPackage(@NonNull Uri data) { + return data.toString().startsWith("package:" + TermuxConstants.TERMUX_PACKAGE_NAME + "."); + } + /** * Send the {@link TermuxConstants#BROADCAST_TERMUX_OPENED} broadcast to notify apps that Termux * app has been opened. diff --git a/termux-shared/src/main/java/com/termux/shared/termux/shell/command/environment/TermuxShellEnvironment.java b/termux-shared/src/main/java/com/termux/shared/termux/shell/command/environment/TermuxShellEnvironment.java index 635a4326..acabc47e 100644 --- a/termux-shared/src/main/java/com/termux/shared/termux/shell/command/environment/TermuxShellEnvironment.java +++ b/termux-shared/src/main/java/com/termux/shared/termux/shell/command/environment/TermuxShellEnvironment.java @@ -4,8 +4,13 @@ import android.content.Context; import androidx.annotation.NonNull; +import com.termux.shared.errors.Error; +import com.termux.shared.file.FileUtils; +import com.termux.shared.logger.Logger; import com.termux.shared.shell.command.ExecutionCommand; import com.termux.shared.shell.command.environment.AndroidShellEnvironment; +import com.termux.shared.shell.command.environment.ShellEnvironmentUtils; +import com.termux.shared.shell.command.environment.ShellCommandShellEnvironment; import com.termux.shared.termux.TermuxBootstrap; import com.termux.shared.termux.TermuxConstants; import com.termux.shared.termux.shell.TermuxShellUtils; @@ -28,11 +33,32 @@ public class TermuxShellEnvironment extends AndroidShellEnvironment { shellCommandShellEnvironment = new TermuxShellCommandShellEnvironment(); } + /** Init {@link TermuxShellEnvironment} constants and caches. */ public synchronized static void init(@NonNull Context currentPackageContext) { TermuxAppShellEnvironment.setTermuxAppEnvironment(currentPackageContext); } + /** Init {@link TermuxShellEnvironment} constants and caches. */ + public synchronized static void writeEnvironmentToFile(@NonNull Context currentPackageContext) { + HashMap environmentMap = new TermuxShellEnvironment().getEnvironment(currentPackageContext, false); + String environmentString = ShellEnvironmentUtils.convertEnvironmentToDotEnvFile(environmentMap); + + // Write environment string to temp file and then move to final location since otherwise + // writing may happen while file is being sourced/read + Error error = FileUtils.writeTextToFile("termux.env.tmp", TermuxConstants.TERMUX_ENV_TEMP_FILE_PATH, + Charset.defaultCharset(), environmentString, false); + if (error != null) { + Logger.logErrorExtended(LOG_TAG, error.toString()); + return; + } + + error = FileUtils.moveRegularFile("termux.env.tmp", TermuxConstants.TERMUX_ENV_TEMP_FILE_PATH, TermuxConstants.TERMUX_ENV_FILE_PATH, true); + if (error != null) { + Logger.logErrorExtended(LOG_TAG, error.toString()); + } + } + /** Get shell environment for Termux. */ @NonNull @Override