Skip to content

Commit

Permalink
Added: Write termux shell environment to `/data/data/com.termux/files…
Browse files Browse the repository at this point in the history
…/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
  • Loading branch information
agnostic-apollo committed Jun 11, 2022
1 parent f76c20d commit 03e1d14
Show file tree
Hide file tree
Showing 8 changed files with 196 additions and 5 deletions.
13 changes: 10 additions & 3 deletions app/src/main/java/com/termux/app/TermuxApplication.java
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
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;
Expand Down Expand Up @@ -48,9 +49,8 @@ public void onCreate() {
// 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);
Expand All @@ -59,10 +59,17 @@ public void onCreate() {
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);
}
}

Expand Down
6 changes: 6 additions & 0 deletions app/src/main/java/com/termux/app/TermuxService.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
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;
Expand Down Expand Up @@ -116,6 +117,8 @@ public void onCreate() {
mShellManager = TermuxShellManager.getShellManager();

runStartForeground();

SystemEventReceiver.registerPackageUpdateEvents(this);
}

@SuppressLint("Wakelock")
Expand Down Expand Up @@ -172,6 +175,9 @@ public void onDestroy() {
killAllTermuxExecutionCommands();

TermuxShellManager.onAppExit(this);

SystemEventReceiver.unregisterPackageUpdateEvents(this);

runStopForeground();
}

Expand Down
44 changes: 44 additions & 0 deletions app/src/main/java/com/termux/app/event/SystemEventReceiver.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,18 @@

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 {
Expand Down Expand Up @@ -35,6 +41,11 @@ public void onReceive(@NonNull Context context, @Nullable Intent intent) {
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);
}
Expand All @@ -44,4 +55,37 @@ public synchronized void onActionBootCompleted(@NonNull Context context, @NonNul
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());
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,60 @@ public static List<String> convertEnvironmentToEnviron(@NonNull HashMap<String,
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
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
import java.util.List;

/*
* Version: v0.49.0
* Version: v0.50.0
* SPDX-License-Identifier: MIT
*
* Changelog
Expand Down Expand Up @@ -266,8 +266,11 @@
* - 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`.
*/

/**
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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"




Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
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;
Expand Down Expand Up @@ -273,6 +274,17 @@ public static Object getTermuxAppAPKClassField(@NonNull Context currentPackageCo



/** 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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,13 @@

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;
Expand All @@ -28,11 +33,32 @@ public TermuxShellEnvironment() {
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
Expand Down

0 comments on commit 03e1d14

Please sign in to comment.