diff --git a/app/src/main/java/com/termux/app/TermuxInstaller.java b/app/src/main/java/com/termux/app/TermuxInstaller.java index 7bc9a957..b8f6def5 100644 --- a/app/src/main/java/com/termux/app/TermuxInstaller.java +++ b/app/src/main/java/com/termux/app/TermuxInstaller.java @@ -19,6 +19,7 @@ import com.termux.shared.markdown.MarkdownUtils; 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; @@ -58,7 +59,7 @@ final class TermuxInstaller { 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; @@ -67,8 +68,9 @@ final class TermuxInstaller { // 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); @@ -78,7 +80,7 @@ final class TermuxInstaller { 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); @@ -216,7 +218,7 @@ final class TermuxInstaller { 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 { @@ -236,6 +238,13 @@ final class TermuxInstaller { }); } + 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"; diff --git a/app/src/main/java/com/termux/app/terminal/TermuxTerminalViewClient.java b/app/src/main/java/com/termux/app/terminal/TermuxTerminalViewClient.java index 05f54089..7e68d3ec 100644 --- a/app/src/main/java/com/termux/app/terminal/TermuxTerminalViewClient.java +++ b/app/src/main/java/com/termux/app/terminal/TermuxTerminalViewClient.java @@ -24,6 +24,7 @@ import com.termux.R; 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; @@ -665,6 +666,14 @@ public class TermuxTerminalViewClient extends TermuxTerminalViewClientBase { 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() { @@ -685,6 +694,12 @@ public class TermuxTerminalViewClient extends TermuxTerminalViewClientBase { 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, diff --git a/termux-shared/src/main/java/com/termux/shared/file/TermuxFileUtils.java b/termux-shared/src/main/java/com/termux/shared/file/TermuxFileUtils.java index 5b06725e..bdd77e81 100644 --- a/termux-shared/src/main/java/com/termux/shared/file/TermuxFileUtils.java +++ b/termux-shared/src/main/java/com/termux/shared/file/TermuxFileUtils.java @@ -5,14 +5,23 @@ import android.os.Environment; 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. * @@ -126,26 +135,75 @@ public class TermuxFileUtils { /** * 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 directory will not be created manually but by calling {@link Context#getFilesDir()} + * The permissions set to directory will be {@link FileUtils#APP_WORKING_DIRECTORY_PERMISSIONS}. + * + * 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//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. - * - * The permissions set to directory will be {@link FileUtils#APP_WORKING_DIRECTORY_PERMISSIONS}. - * + * 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 * + * + * 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 * should be created if its missing. @@ -166,4 +224,71 @@ public class TermuxFileUtils { 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(); + } + } diff --git a/termux-shared/src/main/java/com/termux/shared/packages/PackageUtils.java b/termux-shared/src/main/java/com/termux/shared/packages/PackageUtils.java index dbd27155..b6e8d98a 100644 --- a/termux-shared/src/main/java/com/termux/shared/packages/PackageUtils.java +++ b/termux-shared/src/main/java/com/termux/shared/packages/PackageUtils.java @@ -124,7 +124,8 @@ public class PackageUtils { } /** - * 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. @@ -133,6 +134,17 @@ public class PackageUtils { 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}. * diff --git a/termux-shared/src/main/java/com/termux/shared/termux/AndroidUtils.java b/termux-shared/src/main/java/com/termux/shared/termux/AndroidUtils.java index d433402a..9306d77a 100644 --- a/termux-shared/src/main/java/com/termux/shared/termux/AndroidUtils.java +++ b/termux-shared/src/main/java/com/termux/shared/termux/AndroidUtils.java @@ -40,6 +40,10 @@ public class AndroidUtils { 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")) 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 4981b206..85d7b417 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 @@ -355,10 +355,79 @@ public class TermuxUtils { markdownString.append("## ").append(TermuxConstants.TERMUX_APP_NAME).append(" APT Info\n\n"); markdownString.append(executionCommand.resultData.stdout.toString()); + markdownString.append("\n##\n"); return markdownString.toString(); } + /** + * Get a markdown {@link String} for info for termux debugging. + * + * @param context The context for operations. + * @return Returns the markdown {@link String}. + */ + public static String getTermuxDebugMarkdownString(@NonNull final Context context) { + String statInfo = TermuxFileUtils.getTermuxFilesDirStatMarkdownString(context); + String logcatInfo = getLogcatDumpMarkdownString(context); + + if (statInfo != null && logcatInfo != null) + return statInfo + "\n\n" + logcatInfo; + else if (statInfo != null) + return statInfo; + else + return logcatInfo; + + } + + /** + * Get a markdown {@link String} for logcat command dump. + * + * @param context The context for operations. + * @return Returns the markdown {@link String}. + */ + public static String getLogcatDumpMarkdownString(@NonNull final Context context) { + // Build script + // We need to prevent OutOfMemoryError since StreamGobbler StringBuilder + StringBuilder.toString() + // may require lot of memory if dump is too large. + // Putting a limit at 3000 lines. Assuming average 160 chars/line will result in 500KB usage + // per object. + // That many lines should be enough for debugging for recent issues anyways assuming termux + // has not been granted READ_LOGS permission s. + String logcatScript = "/system/bin/logcat -d -t 3000 2>&1"; + + // Run script + // Logging must be disabled for output of logcat command itself in StreamGobbler + ExecutionCommand executionCommand = new ExecutionCommand(1, "/system/bin/sh", null, logcatScript + "\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 logcatOutput = new StringBuilder(); + logcatOutput.append("$ ").append(logcatScript); + logcatOutput.append("\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) + logcatOutput.append("\n").append(executionCommand.resultData.stderr.toString()); + logcatOutput.append("\n").append("exit code: ").append(executionCommand.resultData.exitCode.toString()); + } + + // Build markdown output + StringBuilder markdownString = new StringBuilder(); + markdownString.append("## Logcat Dump\n\n"); + markdownString.append("\n\n").append(MarkdownUtils.getMarkdownCodeForString(logcatOutput.toString(), true)); + markdownString.append("\n##\n"); + + return markdownString.toString(); + } + + public static String getAPKRelease(String signingCertificateSHA256Digest) { if (signingCertificateSHA256Digest == null) return "null"; diff --git a/termux-shared/src/main/res/values/strings.xml b/termux-shared/src/main/res/values/strings.xml index 5a9e7ce0..f42f2a40 100644 --- a/termux-shared/src/main/res/values/strings.xml +++ b/termux-shared/src/main/res/values/strings.xml @@ -59,6 +59,12 @@ + + Yes + No + + + Log Level "Off"