mirror of
https://github.com/fankes/termux-app.git
synced 2025-09-05 18:25:31 +08:00
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:
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -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();
|
||||
}
|
||||
|
||||
|
@@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -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
|
||||
|
@@ -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);
|
||||
}
|
||||
}
|
@@ -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"
|
||||
|
||||
|
||||
|
||||
|
||||
|
@@ -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.
|
||||
|
@@ -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
|
||||
|
Reference in New Issue
Block a user