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
This commit is contained in:
agnostic-apollo
2022-06-12 00:51:19 +05:00
parent f76c20d036
commit 03e1d14e1e
8 changed files with 196 additions and 5 deletions

View File

@@ -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);
}
}

View File

@@ -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();
}

View File

@@ -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());
}
}

View File

@@ -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<String, String> 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<ShellEnvironmentVariable> 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<ShellEnvironmentVariable> convertEnvironmentMapToEnvironmentVariableList(@NonNull HashMap<String, String> environmentMap) {
List<ShellEnvironmentVariable> 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

View File

@@ -0,0 +1,28 @@
package com.termux.shared.shell.command.environment;
public class ShellEnvironmentVariable implements Comparable<ShellEnvironmentVariable> {
/** 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);
}
}

View File

@@ -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"

View File

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

View File

@@ -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<String, String> 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