Skip to content

Commit

Permalink
Added: Bootstrap error and report issue (optionally) will contain pri…
Browse files Browse the repository at this point in the history
…mary termux files stat info and logcat dump

Users have been reporting issues with bootstrap installation (and `login` file access) failure on email and github but "most" have been useless since they don't follow instructions to debug the issue and report back. The real reason may depend on device. One could be that `/data/data/com.termux` does not exist on the device in which case termux won't work on the device, at least without root. Other reasons could be wrong ownership or selinux context, selinux denials or attempting to install on external sd card (as reported by a user) where likely files dir was different from `/data/data/com.termux/files`.

This commit will save dev and possibly user time and automatically generate the required info to debug such issues. The `ls` command will generate `stat` info for all the major termux directories and files so that existence or ownership issues can be shown. It will also run `logcat` command to take a dump (last `3000` lines) in case other failures are being logged, like selinux denials as per `avc` entries. It will also show if app is installed on external sd card. This info will automatically be shown on bootstrap install failure report.

Moreover, users can generate termux files `stat` info and `logcat` dump manually too with terminal's long hold options menu `More` -> `Report Issue` option and selecting `YES` in the prompt shown to add debug info. This can be helpful for reporting and debugging other issues. If the report generated is too large, then `Save To File` option in context menu (3 dots on top right) of `ReportActivity` can be used and the file viewed/shared instead.

Users must post complete report (optionally without sensitive info) when reporting issues, instead of (partial) screenshots which won't be accepted anymore.

There has been some design changes in android 11 for `/data/data` and `/data/user/0` directory. You can check javadoc for `isTermuxFilesDirectoryAccessible()` function in [`TermuxFileUtils`](termux-shared/src/main/java/com/termux/shared/file/TermuxFileUtils.java) for details.
  • Loading branch information
agnostic-apollo committed Aug 20, 2021
1 parent 351934a commit 24ea83d
Show file tree
Hide file tree
Showing 7 changed files with 254 additions and 14 deletions.
17 changes: 13 additions & 4 deletions app/src/main/java/com/termux/app/TermuxInstaller.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import com.termux.shared.models.errors.Error;
import com.termux.shared.packages.PackageUtils;
import com.termux.shared.termux.TermuxConstants;
import com.termux.shared.termux.TermuxUtils;

import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
Expand Down Expand Up @@ -58,7 +59,7 @@ static void setupBootstrapIfNeeded(final Activity activity, final Runnable whenD
String bootstrapErrorMessage;
Error filesDirectoryAccessibleError;

// This will also call Context.getFilesDir(), which should ensure that TERMUX_FILES_DIR_PATH
// This will also call Context.getFilesDir(), which should ensure that termux files directory
// is created if it does not already exist
filesDirectoryAccessibleError = TermuxFileUtils.isTermuxFilesDirectoryAccessible(activity, true, true);
boolean isFilesDirectoryAccessible = filesDirectoryAccessibleError == null;
Expand All @@ -67,8 +68,9 @@ static void setupBootstrapIfNeeded(final Activity activity, final Runnable whenD
// account has the expected file system paths. Verify that:
if (!PackageUtils.isCurrentUserThePrimaryUser(activity)) {
bootstrapErrorMessage = activity.getString(R.string.bootstrap_error_not_primary_user_message, MarkdownUtils.getMarkdownCodeForString(TermuxConstants.TERMUX_PREFIX_DIR_PATH, false));
Logger.logError(LOG_TAG, "isFilesDirectoryAccessible: " + isFilesDirectoryAccessible);
Logger.logError(LOG_TAG, bootstrapErrorMessage);
CrashUtils.sendCrashReportNotification(activity, LOG_TAG, "## Bootstrap Error\n\n" + bootstrapErrorMessage, true, true);
sendBootstrapCrashReportNotification(activity, bootstrapErrorMessage);
MessageDialogUtils.exitAppWithErrorMessage(activity,
activity.getString(R.string.bootstrap_error_title),
bootstrapErrorMessage);
Expand All @@ -78,7 +80,7 @@ static void setupBootstrapIfNeeded(final Activity activity, final Runnable whenD
if (!isFilesDirectoryAccessible) {
bootstrapErrorMessage = Error.getMinimalErrorString(filesDirectoryAccessibleError) + "\nTERMUX_FILES_DIR: " + MarkdownUtils.getMarkdownCodeForString(TermuxConstants.TERMUX_FILES_DIR_PATH, false);
Logger.logError(LOG_TAG, bootstrapErrorMessage);
CrashUtils.sendCrashReportNotification(activity, LOG_TAG, "## Bootstrap Error\n\n" + bootstrapErrorMessage, true, true);
sendBootstrapCrashReportNotification(activity, bootstrapErrorMessage);
MessageDialogUtils.showMessage(activity,
activity.getString(R.string.bootstrap_error_title),
bootstrapErrorMessage, null);
Expand Down Expand Up @@ -216,7 +218,7 @@ public static void showBootstrapErrorDialog(Activity activity, String PREFIX_FIL
Logger.logErrorExtended(LOG_TAG, "Bootstrap Error:\n" + message);

// Send a notification with the exception so that the user knows why bootstrap setup failed
CrashUtils.sendCrashReportNotification(activity, LOG_TAG, "## Bootstrap Error\n\n" + message, true, true);
sendBootstrapCrashReportNotification(activity, message);

activity.runOnUiThread(() -> {
try {
Expand All @@ -236,6 +238,13 @@ public static void showBootstrapErrorDialog(Activity activity, String PREFIX_FIL
});
}

private static void sendBootstrapCrashReportNotification(Activity activity, String message) {
CrashUtils.sendCrashReportNotification(activity, LOG_TAG,
"## Bootstrap Error\n\n" + message + "\n\n" +
TermuxUtils.getTermuxDebugMarkdownString(activity),
true, true);
}

static void setupStorageSymlinks(final Context context) {
final String LOG_TAG = "termux-storage";

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import com.termux.app.TermuxActivity;
import com.termux.shared.data.UrlUtils;
import com.termux.shared.file.FileUtils;
import com.termux.shared.interact.MessageDialogUtils;
import com.termux.shared.shell.ShellUtils;
import com.termux.shared.terminal.TermuxTerminalViewClientBase;
import com.termux.shared.termux.AndroidUtils;
Expand Down Expand Up @@ -665,6 +666,14 @@ public void reportIssueFromTranscript() {
final String transcriptText = ShellUtils.getTerminalSessionTranscriptText(session, false, true);
if (transcriptText == null) return;

MessageDialogUtils.showMessage(mActivity, TermuxConstants.TERMUX_APP_NAME + " Report Issue",
mActivity.getString(R.string.msg_add_termux_debug_info),
mActivity.getString(R.string.action_yes), (dialog, which) -> reportIssueFromTranscript(transcriptText, true),
mActivity.getString(R.string.action_no), (dialog, which) -> reportIssueFromTranscript(transcriptText, false),
null);
}

private void reportIssueFromTranscript(String transcriptText, boolean addTermuxDebugInfo) {
Logger.showToast(mActivity, mActivity.getString(R.string.msg_generating_report), true);

new Thread() {
Expand All @@ -685,6 +694,12 @@ public void run() {
if (termuxAptInfo != null)
reportString.append("\n\n").append(termuxAptInfo);

if (addTermuxDebugInfo) {
String termuxDebugInfo = TermuxUtils.getTermuxDebugMarkdownString(mActivity);
if (termuxDebugInfo != null)
reportString.append("\n\n").append(termuxDebugInfo);
}

String userActionName = UserAction.REPORT_ISSUE_FROM_TRANSCRIPT.getName();
ReportActivity.startReportActivity(mActivity,
new ReportInfo(userActionName,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,23 @@

import androidx.annotation.NonNull;

import com.termux.shared.logger.Logger;
import com.termux.shared.markdown.MarkdownUtils;
import com.termux.shared.models.ExecutionCommand;
import com.termux.shared.models.errors.Error;
import com.termux.shared.shell.TermuxShellEnvironmentClient;
import com.termux.shared.shell.TermuxTask;
import com.termux.shared.termux.AndroidUtils;
import com.termux.shared.termux.TermuxConstants;
import com.termux.shared.termux.TermuxUtils;

import java.io.File;
import java.util.regex.Pattern;

public class TermuxFileUtils {

private static final String LOG_TAG = "TermuxFileUtils";

/**
* Replace "$PREFIX/" or "~/" prefix with termux absolute paths.
*
Expand Down Expand Up @@ -126,25 +135,74 @@ public static Error validateDirectoryFileExistenceAndPermissions(String label, f

/**
* Validate the existence and permissions of {@link TermuxConstants#TERMUX_FILES_DIR_PATH}.
* This is required because binaries compiled for termux are hard coded with
* {@link TermuxConstants#TERMUX_PREFIX_DIR_PATH} and the path must be accessible.
*
* The permissions set to directory will be {@link FileUtils#APP_WORKING_DIRECTORY_PERMISSIONS}.
*
* The directory will not be created manually but by calling {@link Context#getFilesDir()}
* This function does not create the directory manually but by calling {@link Context#getFilesDir()}
* so that android itself creates it. However, the call will not create its parent package
* data directory `/data/user/0/[package_name]` if it does not already exist and a `logcat`
* error will be logged by android.
* {@code Failed to ensure /data/user/0/<package_name>/files: mkdir failed: ENOENT (No such file or directory)}
* An android app likely can't create the package data directory since its parent `/data/user/0`
* is owned by `system` user and is normally create at app install or update time and not at app startup.
* Note that the path returned by {@link Context#getFilesDir()} will
* An android app normally can't create the package data directory since its parent `/data/user/0`
* is owned by `system` user and is normally created at app install or update time and not at app startup.
*
* Note that the path returned by {@link Context#getFilesDir()} may
* be under `/data/user/[id]/[package_name]` instead of `/data/data/[package_name]`
* defined by default by {@link TermuxConstants#TERMUX_FILES_DIR_PATH}, where id will be 0 for
* defined by default by {@link TermuxConstants#TERMUX_FILES_DIR_PATH} where id will be 0 for
* primary user and a higher number for other users/profiles. If app is running under work profile
* or secondary user, then {@link TermuxConstants#TERMUX_FILES_DIR_PATH} will not be accessible
* and will not be automatically created, unless there is a bind mount from `/data/user/[id]`
* to `/data/data`, ideally in the right namespace.
* and will not be automatically created, unless there is a bind mount from `/data/data` to
* `/data/user/[id]`, ideally in the right namespace.
* https://source.android.com/devices/tech/admin/multi-user
*
* The permissions set to directory will be {@link FileUtils#APP_WORKING_DIRECTORY_PERMISSIONS}.
*
* https://source.android.com/devices/tech/admin/multi-user
* On Android version `<=10`, the `/data/user/0` is a symlink to `/data/data` directory.
* https://cs.android.com/android/platform/superproject/+/android-10.0.0_r47:system/core/rootdir/init.rc;l=589
* {@code
* symlink /data/data /data/user/0
* }
*
* {@code
* /system/bin/ls -lhd /data/data /data/user/0
* drwxrwx--x 179 system system 8.0K 2021-xx-xx xx:xx /data/data
* lrwxrwxrwx 1 root root 10 2021-xx-xx xx:xx /data/user/0 -> /data/data
* }
*
* On Android version `>=11`, the `/data/data` directory is bind mounted at `/data/user/0`.
* https://cs.android.com/android/platform/superproject/+/android-11.0.0_r40:system/core/rootdir/init.rc;l=705
* https://cs.android.com/android/_/android/platform/system/core/+/3cca270e95ca8d8bc8b800e2b5d7da1825fd7100
* {@code
* # Unlink /data/user/0 if we previously symlink it to /data/data
* rm /data/user/0
*
* # Bind mount /data/user/0 to /data/data
* mkdir /data/user/0 0700 system system encryption=None
* mount none /data/data /data/user/0 bind rec
* }
*
* {@code
* /system/bin/grep -E '( /data )|( /data/data )|( /data/user/[0-9]+ )' /proc/self/mountinfo 2>&1 | /system/bin/grep -v '/data_mirror' 2>&1
* 87 32 253:5 / /data rw,nosuid,nodev,noatime shared:27 - ext4 /dev/block/dm-5 rw,seclabel,resgid=1065,errors=panic
* 91 87 253:5 /data /data/user/0 rw,nosuid,nodev,noatime shared:27 - ext4 /dev/block/dm-5 rw,seclabel,resgid=1065,errors=panic
* }
*
* The column 4 defines the root of the mount within the filesystem.
* Basically, `/dev/block/dm-5/` is mounted at `/data` and `/dev/block/dm-5/data` is mounted at
* `/data/user/0`.
* https://www.kernel.org/doc/Documentation/filesystems/proc.txt (section 3.5)
* https://www.kernel.org/doc/Documentation/filesystems/sharedsubtree.txt
* https://unix.stackexchange.com/a/571959
*
*
* Also note that running `/system/bin/ls -lhd /data/user/0/com.termux` as secondary user will result
* in `ls: /data/user/0/com.termux: Permission denied` where `0` is primary user id but running
* `/system/bin/ls -lhd /data/user/10/com.termux` will result in
* `drwx------ 6 u10_a149 u10_a149 4.0K 2021-xx-xx xx:xx /data/user/10/com.termux` where `10` is
* secondary user id. So can't stat directory (not contents) of primary user from secondary user
* but can the other way around. However, this is happening on android 10 avd, but not on android
* 11 avd.
*
* @param context The {@link Context} for operations.
* @param createDirectoryIfMissing The {@code boolean} that decides if directory file
Expand All @@ -166,4 +224,71 @@ public static Error isTermuxFilesDirectoryAccessible(@NonNull final Context cont
FileUtils.APP_WORKING_DIRECTORY_PERMISSIONS, false);
}

/**
* Get a markdown {@link String} for stat output for various Termux app files paths.
*
* @param context The context for operations.
* @return Returns the markdown {@link String}.
*/
public static String getTermuxFilesDirStatMarkdownString(@NonNull final Context context) {
Context termuxPackageContext = TermuxUtils.getTermuxPackageContext(context);
if (termuxPackageContext == null) return null;

// Also ensures that termux files directory is created if it does not already exist
String filesDir = termuxPackageContext.getFilesDir().getAbsolutePath();

// Build script
StringBuilder statScript = new StringBuilder();
statScript
.append("echo 'ls info:'\n")
.append("/system/bin/ls -lhd")
.append(" '/data/data'")
.append(" '/data/user/0'")
.append(" '" + TermuxConstants.TERMUX_INTERNAL_PRIVATE_APP_DATA_DIR_PATH + "'")
.append(" '/data/user/0/" + TermuxConstants.TERMUX_PACKAGE_NAME + "'")
.append(" '" + TermuxConstants.TERMUX_FILES_DIR_PATH + "'")
.append(" '" + filesDir + "'")
.append(" '/data/user/0/" + TermuxConstants.TERMUX_PACKAGE_NAME + "/files'")
.append(" '/data/user/" + TermuxConstants.TERMUX_PACKAGE_NAME + "/files'")
.append(" '" + TermuxConstants.TERMUX_STAGING_PREFIX_DIR_PATH + "'")
.append(" '" + TermuxConstants.TERMUX_PREFIX_DIR_PATH + "'")
.append(" '" + TermuxConstants.TERMUX_HOME_DIR_PATH + "'")
.append(" '" + TermuxConstants.TERMUX_BIN_PREFIX_DIR_PATH + "/login'")
.append(" 2>&1")
.append("\necho; echo 'mount info:'\n")
.append("/system/bin/grep -E '( /data )|( /data/data )|( /data/user/[0-9]+ )' /proc/self/mountinfo 2>&1 | /system/bin/grep -v '/data_mirror' 2>&1");

// Run script
ExecutionCommand executionCommand = new ExecutionCommand(1, "/system/bin/sh", null, statScript.toString() + "\n", "/", true, true);
executionCommand.backgroundCustomLogLevel = Logger.LOG_LEVEL_OFF;
TermuxTask termuxTask = TermuxTask.execute(context, executionCommand, null, new TermuxShellEnvironmentClient(), true);
if (termuxTask == null || !executionCommand.isSuccessful()) {
Logger.logError(LOG_TAG, executionCommand.toString());
return null;
}

// Build script output
StringBuilder statOutput = new StringBuilder();
statOutput.append("$ ").append(statScript.toString());
statOutput.append("\n\n").append(executionCommand.resultData.stdout.toString());

boolean stderrSet = !executionCommand.resultData.stderr.toString().isEmpty();
if (executionCommand.resultData.exitCode != 0 || stderrSet) {
Logger.logError(LOG_TAG, executionCommand.toString());
if (stderrSet)
statOutput.append("\n").append(executionCommand.resultData.stderr.toString());
statOutput.append("\n").append("exit code: ").append(executionCommand.resultData.exitCode.toString());
}

// Build markdown output
StringBuilder markdownString = new StringBuilder();
markdownString.append("## ").append(TermuxConstants.TERMUX_APP_NAME).append(" Files Info\n\n");
AndroidUtils.appendPropertyToMarkdown(markdownString,"TERMUX_REQUIRED_FILES_DIR_PATH ($PREFIX)", TermuxConstants.TERMUX_FILES_DIR_PATH);
AndroidUtils.appendPropertyToMarkdown(markdownString,"ANDROID_ASSIGNED_FILES_DIR_PATH", filesDir);
markdownString.append("\n\n").append(MarkdownUtils.getMarkdownCodeForString(statOutput.toString(), true));
markdownString.append("\n##\n");

return markdownString.toString();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,8 @@ public static int getTargetSDKForPackage(@NonNull final Context context) {
}

/**
* Get the {@code versionName} for the package associated with the {@code context}.
* Check if the app associated with the {@code context} has {@link ApplicationInfo#FLAG_DEBUGGABLE}
* set.
*
* @param context The {@link Context} for the package.
* @return Returns the {@code versionName}. This will be {@code null} if an exception is raised.
Expand All @@ -133,6 +134,17 @@ public static Boolean isAppForPackageADebugBuild(@NonNull final Context context)
return ( 0 != ( context.getApplicationInfo().flags & ApplicationInfo.FLAG_DEBUGGABLE ) );
}

/**
* Check if the app associated with the {@code context} has {@link ApplicationInfo#FLAG_EXTERNAL_STORAGE}
* set.
*
* @param context The {@link Context} for the package.
* @return Returns the {@code versionName}. This will be {@code null} if an exception is raised.
*/
public static Boolean isAppInstalledOnExternalStorage(@NonNull final Context context) {
return ( 0 != ( context.getApplicationInfo().flags & ApplicationInfo.FLAG_EXTERNAL_STORAGE ) );
}

/**
* Get the {@code versionCode} for the package associated with the {@code context}.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@ public static String getAppInfoMarkdownString(@NonNull final Context context) {
AndroidUtils.appendPropertyToMarkdown(markdownString,"TARGET_SDK", PackageUtils.getTargetSDKForPackage(context));
AndroidUtils.appendPropertyToMarkdown(markdownString,"IS_DEBUG_BUILD", PackageUtils.isAppForPackageADebugBuild(context));

if (PackageUtils.isAppInstalledOnExternalStorage(context)) {
AndroidUtils.appendPropertyToMarkdown(markdownString,"IS_INSTALLED_ON_EXTERNAL_STORAGE", true);
}

String filesDir = context.getFilesDir().getAbsolutePath();
if (!filesDir.equals("/data/user/0/" + context.getPackageName() + "/files") &&
!filesDir.equals("/data/data/" + context.getPackageName() + "/files"))
Expand Down
Loading

0 comments on commit 24ea83d

Please sign in to comment.