412 lines
23 KiB
Java
412 lines
23 KiB
Java
package com.termux.app;
|
|
|
|
import android.app.Activity;
|
|
import android.app.AlertDialog;
|
|
import android.app.ProgressDialog;
|
|
import android.content.Context;
|
|
import android.os.Build;
|
|
import android.os.Environment;
|
|
import android.system.Os;
|
|
import android.util.Pair;
|
|
import android.view.WindowManager;
|
|
|
|
import com.termux.R;
|
|
import com.termux.shared.file.FileUtils;
|
|
import com.termux.shared.shell.command.ExecutionCommand;
|
|
import com.termux.shared.shell.command.runner.app.AppShell;
|
|
import com.termux.shared.termux.crash.TermuxCrashUtils;
|
|
import com.termux.shared.termux.file.TermuxFileUtils;
|
|
import com.termux.shared.interact.MessageDialogUtils;
|
|
import com.termux.shared.logger.Logger;
|
|
import com.termux.shared.markdown.MarkdownUtils;
|
|
import com.termux.shared.errors.Error;
|
|
import com.termux.shared.android.PackageUtils;
|
|
import com.termux.shared.termux.TermuxConstants;
|
|
import com.termux.shared.termux.TermuxUtils;
|
|
import com.termux.shared.termux.shell.command.environment.TermuxShellEnvironment;
|
|
|
|
import java.io.BufferedReader;
|
|
import java.io.ByteArrayInputStream;
|
|
import java.io.File;
|
|
import java.io.FileOutputStream;
|
|
import java.io.InputStreamReader;
|
|
import java.util.ArrayList;
|
|
import java.util.List;
|
|
import java.util.zip.ZipEntry;
|
|
import java.util.zip.ZipInputStream;
|
|
|
|
import static com.termux.shared.termux.TermuxConstants.TERMUX_PREFIX_DIR;
|
|
import static com.termux.shared.termux.TermuxConstants.TERMUX_PREFIX_DIR_PATH;
|
|
import static com.termux.shared.termux.TermuxConstants.TERMUX_STAGING_PREFIX_DIR;
|
|
import static com.termux.shared.termux.TermuxConstants.TERMUX_STAGING_PREFIX_DIR_PATH;
|
|
|
|
/**
|
|
* Install the Termux bootstrap packages if necessary by following the below steps:
|
|
* <p/>
|
|
* (1) If $PREFIX already exist, assume that it is correct and be done. Note that this relies on that we do not create a
|
|
* broken $PREFIX directory below.
|
|
* <p/>
|
|
* (2) A progress dialog is shown with "Installing..." message and a spinner.
|
|
* <p/>
|
|
* (3) A staging directory, $STAGING_PREFIX, is cleared if left over from broken installation below.
|
|
* <p/>
|
|
* (4) The zip file is loaded from a shared library.
|
|
* <p/>
|
|
* (5) The zip, containing entries relative to the $PREFIX, is is downloaded and extracted by a zip input stream
|
|
* continuously encountering zip file entries:
|
|
* <p/>
|
|
* (5.1) If the zip entry encountered is SYMLINKS.txt, go through it and remember all symlinks to setup.
|
|
* <p/>
|
|
* (5.2) For every other zip entry, extract it into $STAGING_PREFIX and set execute permissions if necessary.
|
|
*/
|
|
final class TermuxInstaller {
|
|
|
|
private static final String LOG_TAG = "TermuxInstaller";
|
|
|
|
/** Performs bootstrap setup if necessary. */
|
|
static void setupBootstrapIfNeeded(final Activity activity, final Runnable whenDone) {
|
|
String bootstrapErrorMessage;
|
|
Error filesDirectoryAccessibleError;
|
|
|
|
// 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;
|
|
|
|
// Termux can only be run as the primary user (device owner) since only that
|
|
// account has the expected file system paths. Verify that:
|
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && !PackageUtils.isCurrentUserThePrimaryUser(activity)) {
|
|
bootstrapErrorMessage = activity.getString(R.string.bootstrap_error_not_primary_user_message,
|
|
MarkdownUtils.getMarkdownCodeForString(TERMUX_PREFIX_DIR_PATH, false));
|
|
Logger.logError(LOG_TAG, "isFilesDirectoryAccessible: " + isFilesDirectoryAccessible);
|
|
Logger.logError(LOG_TAG, bootstrapErrorMessage);
|
|
sendBootstrapCrashReportNotification(activity, bootstrapErrorMessage);
|
|
MessageDialogUtils.exitAppWithErrorMessage(activity,
|
|
activity.getString(R.string.bootstrap_error_title),
|
|
bootstrapErrorMessage);
|
|
return;
|
|
}
|
|
|
|
if (!isFilesDirectoryAccessible) {
|
|
bootstrapErrorMessage = Error.getMinimalErrorString(filesDirectoryAccessibleError);
|
|
//noinspection SdCardPath
|
|
if (PackageUtils.isAppInstalledOnExternalStorage(activity) &&
|
|
!TermuxConstants.TERMUX_FILES_DIR_PATH.equals(activity.getFilesDir().getAbsolutePath().replaceAll("^/data/user/0/", "/data/data/"))) {
|
|
bootstrapErrorMessage += "\n\n" + activity.getString(R.string.bootstrap_error_installed_on_portable_sd,
|
|
MarkdownUtils.getMarkdownCodeForString(TERMUX_PREFIX_DIR_PATH, false));
|
|
}
|
|
|
|
Logger.logError(LOG_TAG, bootstrapErrorMessage);
|
|
sendBootstrapCrashReportNotification(activity, bootstrapErrorMessage);
|
|
MessageDialogUtils.showMessage(activity,
|
|
activity.getString(R.string.bootstrap_error_title),
|
|
bootstrapErrorMessage, null);
|
|
return;
|
|
}
|
|
|
|
// If prefix directory exists, even if its a symlink to a valid directory and symlink is not broken/dangling
|
|
if (FileUtils.directoryFileExists(TERMUX_PREFIX_DIR_PATH, true)) {
|
|
if (TermuxFileUtils.isTermuxPrefixDirectoryEmpty()) {
|
|
Logger.logInfo(LOG_TAG, "The termux prefix directory \"" + TERMUX_PREFIX_DIR_PATH + "\" exists but is empty or only contains specific unimportant files.");
|
|
} else {
|
|
whenDone.run();
|
|
return;
|
|
}
|
|
} else if (FileUtils.fileExists(TERMUX_PREFIX_DIR_PATH, false)) {
|
|
Logger.logInfo(LOG_TAG, "The termux prefix directory \"" + TERMUX_PREFIX_DIR_PATH + "\" does not exist but another file exists at its destination.");
|
|
}
|
|
|
|
final ProgressDialog progress = ProgressDialog.show(activity, null, activity.getString(R.string.bootstrap_installer_body), true, false);
|
|
new Thread() {
|
|
@Override
|
|
public void run() {
|
|
try {
|
|
Logger.logInfo(LOG_TAG, "Installing " + TermuxConstants.TERMUX_APP_NAME + " bootstrap packages.");
|
|
|
|
Error error;
|
|
|
|
// Delete prefix staging directory or any file at its destination
|
|
error = FileUtils.deleteFile("termux prefix staging directory", TERMUX_STAGING_PREFIX_DIR_PATH, true);
|
|
if (error != null) {
|
|
showBootstrapErrorDialog(activity, whenDone, Error.getErrorMarkdownString(error));
|
|
return;
|
|
}
|
|
|
|
// Delete prefix directory or any file at its destination
|
|
error = FileUtils.deleteFile("termux prefix directory", TERMUX_PREFIX_DIR_PATH, true);
|
|
if (error != null) {
|
|
showBootstrapErrorDialog(activity, whenDone, Error.getErrorMarkdownString(error));
|
|
return;
|
|
}
|
|
|
|
// Create prefix staging directory if it does not already exist and set required permissions
|
|
error = TermuxFileUtils.isTermuxPrefixStagingDirectoryAccessible(true, true);
|
|
if (error != null) {
|
|
showBootstrapErrorDialog(activity, whenDone, Error.getErrorMarkdownString(error));
|
|
return;
|
|
}
|
|
|
|
// Create prefix directory if it does not already exist and set required permissions
|
|
error = TermuxFileUtils.isTermuxPrefixDirectoryAccessible(true, true);
|
|
if (error != null) {
|
|
showBootstrapErrorDialog(activity, whenDone, Error.getErrorMarkdownString(error));
|
|
return;
|
|
}
|
|
|
|
Logger.logInfo(LOG_TAG, "Extracting bootstrap zip to prefix staging directory \"" + TERMUX_STAGING_PREFIX_DIR_PATH + "\".");
|
|
|
|
final byte[] buffer = new byte[8096];
|
|
final List<Pair<String, String>> symlinks = new ArrayList<>(50);
|
|
|
|
final byte[] zipBytes = loadZipBytes();
|
|
try (ZipInputStream zipInput = new ZipInputStream(new ByteArrayInputStream(zipBytes))) {
|
|
ZipEntry zipEntry;
|
|
while ((zipEntry = zipInput.getNextEntry()) != null) {
|
|
if (zipEntry.getName().equals("SYMLINKS.txt")) {
|
|
BufferedReader symlinksReader = new BufferedReader(new InputStreamReader(zipInput));
|
|
String line;
|
|
while ((line = symlinksReader.readLine()) != null) {
|
|
String[] parts = line.split("←");
|
|
if (parts.length != 2)
|
|
throw new RuntimeException("Malformed symlink line: " + line);
|
|
String oldPath = parts[0];
|
|
String newPath = TERMUX_STAGING_PREFIX_DIR_PATH + "/" + parts[1];
|
|
symlinks.add(Pair.create(oldPath, newPath));
|
|
|
|
error = ensureDirectoryExists(new File(newPath).getParentFile());
|
|
if (error != null) {
|
|
showBootstrapErrorDialog(activity, whenDone, Error.getErrorMarkdownString(error));
|
|
return;
|
|
}
|
|
}
|
|
} else {
|
|
String zipEntryName = zipEntry.getName();
|
|
File targetFile = new File(TERMUX_STAGING_PREFIX_DIR_PATH, zipEntryName);
|
|
boolean isDirectory = zipEntry.isDirectory();
|
|
|
|
error = ensureDirectoryExists(isDirectory ? targetFile : targetFile.getParentFile());
|
|
if (error != null) {
|
|
showBootstrapErrorDialog(activity, whenDone, Error.getErrorMarkdownString(error));
|
|
return;
|
|
}
|
|
|
|
if (!isDirectory) {
|
|
try (FileOutputStream outStream = new FileOutputStream(targetFile)) {
|
|
int readBytes;
|
|
while ((readBytes = zipInput.read(buffer)) != -1)
|
|
outStream.write(buffer, 0, readBytes);
|
|
}
|
|
if (zipEntryName.startsWith("bin/") || zipEntryName.startsWith("libexec") ||
|
|
zipEntryName.startsWith("lib/apt/apt-helper") || zipEntryName.startsWith("lib/apt/methods") ||
|
|
zipEntryName.equals("etc/termux/bootstrap/termux-bootstrap-second-stage.sh")) {
|
|
//noinspection OctalInteger
|
|
Os.chmod(targetFile.getAbsolutePath(), 0700);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (symlinks.isEmpty())
|
|
throw new RuntimeException("No SYMLINKS.txt encountered");
|
|
for (Pair<String, String> symlink : symlinks) {
|
|
Os.symlink(symlink.first, symlink.second);
|
|
}
|
|
|
|
Logger.logInfo(LOG_TAG, "Moving termux prefix staging to prefix directory.");
|
|
|
|
if (!TERMUX_STAGING_PREFIX_DIR.renameTo(TERMUX_PREFIX_DIR)) {
|
|
throw new RuntimeException("Moving termux prefix staging to prefix directory failed");
|
|
}
|
|
|
|
// Run Termux bootstrap second stage
|
|
Logger.logInfo(LOG_TAG, "Running Termux bootstrap second stage.");
|
|
String termuxBootstrapSecondStageFile = TERMUX_PREFIX_DIR_PATH + "/etc/termux/bootstrap/termux-bootstrap-second-stage.sh";
|
|
if (FileUtils.fileExists(termuxBootstrapSecondStageFile, false)) {
|
|
ExecutionCommand executionCommand = new ExecutionCommand(-1,
|
|
termuxBootstrapSecondStageFile, null, null,
|
|
null, ExecutionCommand.Runner.APP_SHELL.getName(), false);
|
|
executionCommand.commandLabel = "Termux Bootstrap Second Stage Command";
|
|
executionCommand.backgroundCustomLogLevel = Logger.LOG_LEVEL_NORMAL;
|
|
AppShell appShell = AppShell.execute(activity, executionCommand, null, new TermuxShellEnvironment(), null, true);
|
|
boolean stderrSet = !executionCommand.resultData.stderr.toString().isEmpty();
|
|
if (appShell == null || !executionCommand.isSuccessful() || executionCommand.resultData.exitCode != 0 || stderrSet) {
|
|
// Delete prefix directory as otherwise when app is restarted, the broken prefix directory would be used and logged into
|
|
error = FileUtils.deleteFile("termux prefix directory", TERMUX_PREFIX_DIR_PATH, true);
|
|
if (error != null)
|
|
Logger.logErrorExtended(LOG_TAG, error.toString());
|
|
|
|
showBootstrapErrorDialog(activity, whenDone, MarkdownUtils.getMarkdownCodeForString(executionCommand.toString(), true));
|
|
return;
|
|
}
|
|
}
|
|
|
|
Logger.logInfo(LOG_TAG, "Bootstrap packages installed successfully.");
|
|
|
|
// Recreate env file since termux prefix was wiped earlier
|
|
TermuxShellEnvironment.writeEnvironmentToFile(activity);
|
|
|
|
activity.runOnUiThread(whenDone);
|
|
|
|
} catch (final Exception e) {
|
|
showBootstrapErrorDialog(activity, whenDone, Logger.getStackTracesMarkdownString(null, Logger.getStackTracesStringArray(e)));
|
|
|
|
} finally {
|
|
activity.runOnUiThread(() -> {
|
|
try {
|
|
progress.dismiss();
|
|
} catch (RuntimeException e) {
|
|
// Activity already dismissed - ignore.
|
|
}
|
|
});
|
|
}
|
|
}
|
|
}.start();
|
|
}
|
|
|
|
public static void showBootstrapErrorDialog(Activity activity, Runnable whenDone, String message) {
|
|
Logger.logErrorExtended(LOG_TAG, "Bootstrap Error:\n" + message);
|
|
|
|
// Send a notification with the exception so that the user knows why bootstrap setup failed
|
|
sendBootstrapCrashReportNotification(activity, message);
|
|
|
|
activity.runOnUiThread(() -> {
|
|
try {
|
|
new AlertDialog.Builder(activity).setTitle(R.string.bootstrap_error_title).setMessage(R.string.bootstrap_error_body)
|
|
.setNegativeButton(R.string.bootstrap_error_abort, (dialog, which) -> {
|
|
dialog.dismiss();
|
|
activity.finish();
|
|
})
|
|
.setPositiveButton(R.string.bootstrap_error_try_again, (dialog, which) -> {
|
|
dialog.dismiss();
|
|
FileUtils.deleteFile("termux prefix directory", TERMUX_PREFIX_DIR_PATH, true);
|
|
TermuxInstaller.setupBootstrapIfNeeded(activity, whenDone);
|
|
}).show();
|
|
} catch (WindowManager.BadTokenException e1) {
|
|
// Activity already dismissed - ignore.
|
|
}
|
|
});
|
|
}
|
|
|
|
private static void sendBootstrapCrashReportNotification(Activity activity, String message) {
|
|
final String title = TermuxConstants.TERMUX_APP_NAME + " Bootstrap Error";
|
|
|
|
// Add info of all install Termux plugin apps as well since their target sdk or installation
|
|
// on external/portable sd card can affect Termux app files directory access or exec.
|
|
TermuxCrashUtils.sendCrashReportNotification(activity, LOG_TAG,
|
|
title, null, "## " + title + "\n\n" + message + "\n\n" +
|
|
TermuxUtils.getTermuxDebugMarkdownString(activity),
|
|
true, false, TermuxUtils.AppInfoMode.TERMUX_AND_PLUGIN_PACKAGES, true);
|
|
}
|
|
|
|
static void setupStorageSymlinks(final Context context) {
|
|
final String LOG_TAG = "termux-storage";
|
|
final String title = TermuxConstants.TERMUX_APP_NAME + " Setup Storage Error";
|
|
|
|
Logger.logInfo(LOG_TAG, "Setting up storage symlinks.");
|
|
|
|
new Thread() {
|
|
public void run() {
|
|
try {
|
|
Error error;
|
|
File storageDir = TermuxConstants.TERMUX_STORAGE_HOME_DIR;
|
|
|
|
error = FileUtils.clearDirectory("~/storage", storageDir.getAbsolutePath());
|
|
if (error != null) {
|
|
Logger.logErrorAndShowToast(context, LOG_TAG, error.getMessage());
|
|
Logger.logErrorExtended(LOG_TAG, "Setup Storage Error\n" + error.toString());
|
|
TermuxCrashUtils.sendCrashReportNotification(context, LOG_TAG, title, null,
|
|
"## " + title + "\n\n" + Error.getErrorMarkdownString(error),
|
|
true, false, TermuxUtils.AppInfoMode.TERMUX_PACKAGE, true);
|
|
return;
|
|
}
|
|
|
|
Logger.logInfo(LOG_TAG, "Setting up storage symlinks at ~/storage/shared, ~/storage/downloads, ~/storage/dcim, ~/storage/pictures, ~/storage/music and ~/storage/movies for directories in \"" + Environment.getExternalStorageDirectory().getAbsolutePath() + "\".");
|
|
|
|
// Get primary storage root "/storage/emulated/0" symlink
|
|
File sharedDir = Environment.getExternalStorageDirectory();
|
|
Os.symlink(sharedDir.getAbsolutePath(), new File(storageDir, "shared").getAbsolutePath());
|
|
|
|
File documentsDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS);
|
|
Os.symlink(documentsDir.getAbsolutePath(), new File(storageDir, "documents").getAbsolutePath());
|
|
|
|
File downloadsDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS);
|
|
Os.symlink(downloadsDir.getAbsolutePath(), new File(storageDir, "downloads").getAbsolutePath());
|
|
|
|
File dcimDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM);
|
|
Os.symlink(dcimDir.getAbsolutePath(), new File(storageDir, "dcim").getAbsolutePath());
|
|
|
|
File picturesDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES);
|
|
Os.symlink(picturesDir.getAbsolutePath(), new File(storageDir, "pictures").getAbsolutePath());
|
|
|
|
File musicDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MUSIC);
|
|
Os.symlink(musicDir.getAbsolutePath(), new File(storageDir, "music").getAbsolutePath());
|
|
|
|
File moviesDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MOVIES);
|
|
Os.symlink(moviesDir.getAbsolutePath(), new File(storageDir, "movies").getAbsolutePath());
|
|
|
|
File podcastsDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PODCASTS);
|
|
Os.symlink(podcastsDir.getAbsolutePath(), new File(storageDir, "podcasts").getAbsolutePath());
|
|
|
|
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.Q) {
|
|
File audiobooksDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_AUDIOBOOKS);
|
|
Os.symlink(audiobooksDir.getAbsolutePath(), new File(storageDir, "audiobooks").getAbsolutePath());
|
|
}
|
|
|
|
// Dir 0 should ideally be for primary storage
|
|
// https://cs.android.com/android/platform/superproject/+/android-12.0.0_r32:frameworks/base/core/java/android/app/ContextImpl.java;l=818
|
|
// https://cs.android.com/android/platform/superproject/+/android-12.0.0_r32:frameworks/base/core/java/android/os/Environment.java;l=219
|
|
// https://cs.android.com/android/platform/superproject/+/android-12.0.0_r32:frameworks/base/core/java/android/os/Environment.java;l=181
|
|
// https://cs.android.com/android/platform/superproject/+/android-12.0.0_r32:frameworks/base/services/core/java/com/android/server/StorageManagerService.java;l=3796
|
|
// https://cs.android.com/android/platform/superproject/+/android-7.0.0_r36:frameworks/base/services/core/java/com/android/server/MountService.java;l=3053
|
|
|
|
// Create "Android/data/com.termux" symlinks
|
|
File[] dirs = context.getExternalFilesDirs(null);
|
|
if (dirs != null && dirs.length > 0) {
|
|
for (int i = 0; i < dirs.length; i++) {
|
|
File dir = dirs[i];
|
|
if (dir == null) continue;
|
|
String symlinkName = "external-" + i;
|
|
Logger.logInfo(LOG_TAG, "Setting up storage symlinks at ~/storage/" + symlinkName + " for \"" + dir.getAbsolutePath() + "\".");
|
|
Os.symlink(dir.getAbsolutePath(), new File(storageDir, symlinkName).getAbsolutePath());
|
|
}
|
|
}
|
|
|
|
// Create "Android/media/com.termux" symlinks
|
|
dirs = context.getExternalMediaDirs();
|
|
if (dirs != null && dirs.length > 0) {
|
|
for (int i = 0; i < dirs.length; i++) {
|
|
File dir = dirs[i];
|
|
if (dir == null) continue;
|
|
String symlinkName = "media-" + i;
|
|
Logger.logInfo(LOG_TAG, "Setting up storage symlinks at ~/storage/" + symlinkName + " for \"" + dir.getAbsolutePath() + "\".");
|
|
Os.symlink(dir.getAbsolutePath(), new File(storageDir, symlinkName).getAbsolutePath());
|
|
}
|
|
}
|
|
|
|
Logger.logInfo(LOG_TAG, "Storage symlinks created successfully.");
|
|
} catch (Exception e) {
|
|
Logger.logErrorAndShowToast(context, LOG_TAG, e.getMessage());
|
|
Logger.logStackTraceWithMessage(LOG_TAG, "Setup Storage Error: Error setting up link", e);
|
|
TermuxCrashUtils.sendCrashReportNotification(context, LOG_TAG, title, null,
|
|
"## " + title + "\n\n" + Logger.getStackTracesMarkdownString(null, Logger.getStackTracesStringArray(e)),
|
|
true, false, TermuxUtils.AppInfoMode.TERMUX_PACKAGE, true);
|
|
}
|
|
}
|
|
}.start();
|
|
}
|
|
|
|
private static Error ensureDirectoryExists(File directory) {
|
|
return FileUtils.createDirectoryFile(directory.getAbsolutePath());
|
|
}
|
|
|
|
public static byte[] loadZipBytes() {
|
|
// Only load the shared library when necessary to save memory usage.
|
|
System.loadLibrary("termux-bootstrap");
|
|
return getZip();
|
|
}
|
|
|
|
public static native byte[] getZip();
|
|
|
|
}
|