mirror of
https://github.com/fankes/termux-app.git
synced 2025-09-07 03:05:18 +08:00
Added: Bootstrap error and report issue (optionally) will contain primary 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.
This commit is contained in:
@@ -19,6 +19,7 @@ import com.termux.shared.markdown.MarkdownUtils;
|
|||||||
import com.termux.shared.models.errors.Error;
|
import com.termux.shared.models.errors.Error;
|
||||||
import com.termux.shared.packages.PackageUtils;
|
import com.termux.shared.packages.PackageUtils;
|
||||||
import com.termux.shared.termux.TermuxConstants;
|
import com.termux.shared.termux.TermuxConstants;
|
||||||
|
import com.termux.shared.termux.TermuxUtils;
|
||||||
|
|
||||||
import java.io.BufferedReader;
|
import java.io.BufferedReader;
|
||||||
import java.io.ByteArrayInputStream;
|
import java.io.ByteArrayInputStream;
|
||||||
@@ -58,7 +59,7 @@ final class TermuxInstaller {
|
|||||||
String bootstrapErrorMessage;
|
String bootstrapErrorMessage;
|
||||||
Error filesDirectoryAccessibleError;
|
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
|
// is created if it does not already exist
|
||||||
filesDirectoryAccessibleError = TermuxFileUtils.isTermuxFilesDirectoryAccessible(activity, true, true);
|
filesDirectoryAccessibleError = TermuxFileUtils.isTermuxFilesDirectoryAccessible(activity, true, true);
|
||||||
boolean isFilesDirectoryAccessible = filesDirectoryAccessibleError == null;
|
boolean isFilesDirectoryAccessible = filesDirectoryAccessibleError == null;
|
||||||
@@ -67,8 +68,9 @@ final class TermuxInstaller {
|
|||||||
// account has the expected file system paths. Verify that:
|
// account has the expected file system paths. Verify that:
|
||||||
if (!PackageUtils.isCurrentUserThePrimaryUser(activity)) {
|
if (!PackageUtils.isCurrentUserThePrimaryUser(activity)) {
|
||||||
bootstrapErrorMessage = activity.getString(R.string.bootstrap_error_not_primary_user_message, MarkdownUtils.getMarkdownCodeForString(TermuxConstants.TERMUX_PREFIX_DIR_PATH, false));
|
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);
|
Logger.logError(LOG_TAG, bootstrapErrorMessage);
|
||||||
CrashUtils.sendCrashReportNotification(activity, LOG_TAG, "## Bootstrap Error\n\n" + bootstrapErrorMessage, true, true);
|
sendBootstrapCrashReportNotification(activity, bootstrapErrorMessage);
|
||||||
MessageDialogUtils.exitAppWithErrorMessage(activity,
|
MessageDialogUtils.exitAppWithErrorMessage(activity,
|
||||||
activity.getString(R.string.bootstrap_error_title),
|
activity.getString(R.string.bootstrap_error_title),
|
||||||
bootstrapErrorMessage);
|
bootstrapErrorMessage);
|
||||||
@@ -78,7 +80,7 @@ final class TermuxInstaller {
|
|||||||
if (!isFilesDirectoryAccessible) {
|
if (!isFilesDirectoryAccessible) {
|
||||||
bootstrapErrorMessage = Error.getMinimalErrorString(filesDirectoryAccessibleError) + "\nTERMUX_FILES_DIR: " + MarkdownUtils.getMarkdownCodeForString(TermuxConstants.TERMUX_FILES_DIR_PATH, false);
|
bootstrapErrorMessage = Error.getMinimalErrorString(filesDirectoryAccessibleError) + "\nTERMUX_FILES_DIR: " + MarkdownUtils.getMarkdownCodeForString(TermuxConstants.TERMUX_FILES_DIR_PATH, false);
|
||||||
Logger.logError(LOG_TAG, bootstrapErrorMessage);
|
Logger.logError(LOG_TAG, bootstrapErrorMessage);
|
||||||
CrashUtils.sendCrashReportNotification(activity, LOG_TAG, "## Bootstrap Error\n\n" + bootstrapErrorMessage, true, true);
|
sendBootstrapCrashReportNotification(activity, bootstrapErrorMessage);
|
||||||
MessageDialogUtils.showMessage(activity,
|
MessageDialogUtils.showMessage(activity,
|
||||||
activity.getString(R.string.bootstrap_error_title),
|
activity.getString(R.string.bootstrap_error_title),
|
||||||
bootstrapErrorMessage, null);
|
bootstrapErrorMessage, null);
|
||||||
@@ -216,7 +218,7 @@ final class TermuxInstaller {
|
|||||||
Logger.logErrorExtended(LOG_TAG, "Bootstrap Error:\n" + message);
|
Logger.logErrorExtended(LOG_TAG, "Bootstrap Error:\n" + message);
|
||||||
|
|
||||||
// Send a notification with the exception so that the user knows why bootstrap setup failed
|
// 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(() -> {
|
activity.runOnUiThread(() -> {
|
||||||
try {
|
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) {
|
static void setupStorageSymlinks(final Context context) {
|
||||||
final String LOG_TAG = "termux-storage";
|
final String LOG_TAG = "termux-storage";
|
||||||
|
|
||||||
|
@@ -24,6 +24,7 @@ import com.termux.R;
|
|||||||
import com.termux.app.TermuxActivity;
|
import com.termux.app.TermuxActivity;
|
||||||
import com.termux.shared.data.UrlUtils;
|
import com.termux.shared.data.UrlUtils;
|
||||||
import com.termux.shared.file.FileUtils;
|
import com.termux.shared.file.FileUtils;
|
||||||
|
import com.termux.shared.interact.MessageDialogUtils;
|
||||||
import com.termux.shared.shell.ShellUtils;
|
import com.termux.shared.shell.ShellUtils;
|
||||||
import com.termux.shared.terminal.TermuxTerminalViewClientBase;
|
import com.termux.shared.terminal.TermuxTerminalViewClientBase;
|
||||||
import com.termux.shared.termux.AndroidUtils;
|
import com.termux.shared.termux.AndroidUtils;
|
||||||
@@ -665,6 +666,14 @@ public class TermuxTerminalViewClient extends TermuxTerminalViewClientBase {
|
|||||||
final String transcriptText = ShellUtils.getTerminalSessionTranscriptText(session, false, true);
|
final String transcriptText = ShellUtils.getTerminalSessionTranscriptText(session, false, true);
|
||||||
if (transcriptText == null) return;
|
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);
|
Logger.showToast(mActivity, mActivity.getString(R.string.msg_generating_report), true);
|
||||||
|
|
||||||
new Thread() {
|
new Thread() {
|
||||||
@@ -685,6 +694,12 @@ public class TermuxTerminalViewClient extends TermuxTerminalViewClientBase {
|
|||||||
if (termuxAptInfo != null)
|
if (termuxAptInfo != null)
|
||||||
reportString.append("\n\n").append(termuxAptInfo);
|
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();
|
String userActionName = UserAction.REPORT_ISSUE_FROM_TRANSCRIPT.getName();
|
||||||
ReportActivity.startReportActivity(mActivity,
|
ReportActivity.startReportActivity(mActivity,
|
||||||
new ReportInfo(userActionName,
|
new ReportInfo(userActionName,
|
||||||
|
@@ -5,14 +5,23 @@ import android.os.Environment;
|
|||||||
|
|
||||||
import androidx.annotation.NonNull;
|
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.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.TermuxConstants;
|
||||||
|
import com.termux.shared.termux.TermuxUtils;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
public class TermuxFileUtils {
|
public class TermuxFileUtils {
|
||||||
|
|
||||||
|
private static final String LOG_TAG = "TermuxFileUtils";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Replace "$PREFIX/" or "~/" prefix with termux absolute paths.
|
* 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}.
|
* 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
|
* 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`
|
* data directory `/data/user/0/[package_name]` if it does not already exist and a `logcat`
|
||||||
* error will be logged by android.
|
* error will be logged by android.
|
||||||
* {@code Failed to ensure /data/user/0/<package_name>/files: mkdir failed: ENOENT (No such file or directory)}
|
* {@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`
|
* 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 create at app install or update time and not at app startup.
|
* 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()} will
|
*
|
||||||
|
* Note that the path returned by {@link Context#getFilesDir()} may
|
||||||
* be under `/data/user/[id]/[package_name]` instead of `/data/data/[package_name]`
|
* 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
|
* 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
|
* 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]`
|
* and will not be automatically created, unless there is a bind mount from `/data/data` to
|
||||||
* to `/data/data`, ideally in the right namespace.
|
* `/data/user/[id]`, ideally in the right namespace.
|
||||||
*
|
|
||||||
* The permissions set to directory will be {@link FileUtils#APP_WORKING_DIRECTORY_PERMISSIONS}.
|
|
||||||
*
|
|
||||||
* https://source.android.com/devices/tech/admin/multi-user
|
* 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 context The {@link Context} for operations.
|
||||||
* @param createDirectoryIfMissing The {@code boolean} that decides if directory file
|
* @param createDirectoryIfMissing The {@code boolean} that decides if directory file
|
||||||
* should be created if its missing.
|
* should be created if its missing.
|
||||||
@@ -166,4 +224,71 @@ public class TermuxFileUtils {
|
|||||||
FileUtils.APP_WORKING_DIRECTORY_PERMISSIONS, false);
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -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.
|
* @param context The {@link Context} for the package.
|
||||||
* @return Returns the {@code versionName}. This will be {@code null} if an exception is raised.
|
* @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 ) );
|
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}.
|
* Get the {@code versionCode} for the package associated with the {@code context}.
|
||||||
*
|
*
|
||||||
|
@@ -40,6 +40,10 @@ public class AndroidUtils {
|
|||||||
AndroidUtils.appendPropertyToMarkdown(markdownString,"TARGET_SDK", PackageUtils.getTargetSDKForPackage(context));
|
AndroidUtils.appendPropertyToMarkdown(markdownString,"TARGET_SDK", PackageUtils.getTargetSDKForPackage(context));
|
||||||
AndroidUtils.appendPropertyToMarkdown(markdownString,"IS_DEBUG_BUILD", PackageUtils.isAppForPackageADebugBuild(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();
|
String filesDir = context.getFilesDir().getAbsolutePath();
|
||||||
if (!filesDir.equals("/data/user/0/" + context.getPackageName() + "/files") &&
|
if (!filesDir.equals("/data/user/0/" + context.getPackageName() + "/files") &&
|
||||||
!filesDir.equals("/data/data/" + context.getPackageName() + "/files"))
|
!filesDir.equals("/data/data/" + context.getPackageName() + "/files"))
|
||||||
|
@@ -355,10 +355,79 @@ public class TermuxUtils {
|
|||||||
|
|
||||||
markdownString.append("## ").append(TermuxConstants.TERMUX_APP_NAME).append(" APT Info\n\n");
|
markdownString.append("## ").append(TermuxConstants.TERMUX_APP_NAME).append(" APT Info\n\n");
|
||||||
markdownString.append(executionCommand.resultData.stdout.toString());
|
markdownString.append(executionCommand.resultData.stdout.toString());
|
||||||
|
markdownString.append("\n##\n");
|
||||||
|
|
||||||
return markdownString.toString();
|
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) {
|
public static String getAPKRelease(String signingCertificateSHA256Digest) {
|
||||||
if (signingCertificateSHA256Digest == null) return "null";
|
if (signingCertificateSHA256Digest == null) return "null";
|
||||||
|
@@ -59,6 +59,12 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<!-- Miscellaneous -->
|
||||||
|
<string name="action_yes">Yes</string>
|
||||||
|
<string name="action_no">No</string>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<!-- Log Level -->
|
<!-- Log Level -->
|
||||||
<string name="log_level_title">Log Level</string>
|
<string name="log_level_title">Log Level</string>
|
||||||
<string name="log_level_off">"Off"</string>
|
<string name="log_level_off">"Off"</string>
|
||||||
|
Reference in New Issue
Block a user