Create termux-shared library package for all termux constants and shared utils

The termux plugins should use this library instead of hardcoding "com.termux" values in their source code.

The library can be included as a dependency by plugins and third party apps by including the following line in the build.gradle where x.xxx is the version number, once its published.

`implementation 'com.termux:termux-shared:x.xxx'`

The `TermuxConstants` class has been updated to `v0.17.0`, `TermuxPreferenceConstants` to `v0.9.0` and `TermuxPropertyConstants` to `v0.6.0`. Check their Changelog sections for info on changes.

Some typos and redundant code has also been fixed.
This commit is contained in:
agnostic-apollo
2021-04-07 11:31:30 +05:00
parent c9a476caf7
commit 682ce08314
73 changed files with 746 additions and 520 deletions

View File

@@ -0,0 +1,156 @@
package com.termux.shared.shell;
import android.content.Context;
import com.termux.shared.termux.TermuxConstants;
import com.termux.shared.file.FileUtils;
import com.termux.shared.logger.Logger;
import com.termux.shared.packages.PackageUtils;
import com.termux.shared.termux.TermuxUtils;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class ShellUtils {
public static String[] buildEnvironment(Context currentPackageContext, boolean isFailSafe, String workingDirectory) {
TermuxConstants.TERMUX_HOME_DIR.mkdirs();
if (workingDirectory == null || workingDirectory.isEmpty()) workingDirectory = TermuxConstants.TERMUX_HOME_DIR_PATH;
List<String> environment = new ArrayList<>();
// This function may be called by a different package like a plugin, so we get version for Termux package via its context
Context termuxPackageContext = TermuxUtils.getTermuxPackageContext(currentPackageContext);
if(termuxPackageContext != null) {
String termuxVersionName = PackageUtils.getVersionNameForPackage(termuxPackageContext);
if(termuxVersionName != null)
environment.add("TERMUX_VERSION=" + termuxVersionName);
}
environment.add("TERM=xterm-256color");
environment.add("COLORTERM=truecolor");
environment.add("HOME=" + TermuxConstants.TERMUX_HOME_DIR_PATH);
environment.add("PREFIX=" + TermuxConstants.TERMUX_PREFIX_DIR_PATH);
environment.add("BOOTCLASSPATH=" + System.getenv("BOOTCLASSPATH"));
environment.add("ANDROID_ROOT=" + System.getenv("ANDROID_ROOT"));
environment.add("ANDROID_DATA=" + System.getenv("ANDROID_DATA"));
// EXTERNAL_STORAGE is needed for /system/bin/am to work on at least
// Samsung S7 - see https://plus.google.com/110070148244138185604/posts/gp8Lk3aCGp3.
environment.add("EXTERNAL_STORAGE=" + System.getenv("EXTERNAL_STORAGE"));
// These variables are needed if running on Android 10 and higher.
addToEnvIfPresent(environment, "ANDROID_ART_ROOT");
addToEnvIfPresent(environment, "DEX2OATBOOTCLASSPATH");
addToEnvIfPresent(environment, "ANDROID_I18N_ROOT");
addToEnvIfPresent(environment, "ANDROID_RUNTIME_ROOT");
addToEnvIfPresent(environment, "ANDROID_TZDATA_ROOT");
if (isFailSafe) {
// Keep the default path so that system binaries can be used in the failsafe session.
environment.add("PATH= " + System.getenv("PATH"));
} else {
environment.add("LANG=en_US.UTF-8");
environment.add("PATH=" + TermuxConstants.TERMUX_BIN_PREFIX_DIR_PATH);
environment.add("PWD=" + workingDirectory);
environment.add("TMPDIR=" + TermuxConstants.TERMUX_TMP_PREFIX_DIR_PATH);
}
return environment.toArray(new String[0]);
}
public static void addToEnvIfPresent(List<String> environment, String name) {
String value = System.getenv(name);
if (value != null) {
environment.add(name + "=" + value);
}
}
public static int getPid(Process p) {
try {
Field f = p.getClass().getDeclaredField("pid");
f.setAccessible(true);
try {
return f.getInt(p);
} finally {
f.setAccessible(false);
}
} catch (Throwable e) {
return -1;
}
}
public static String[] setupProcessArgs(String fileToExecute, String[] arguments) {
// The file to execute may either be:
// - An elf file, in which we execute it directly.
// - A script file without shebang, which we execute with our standard shell $PREFIX/bin/sh instead of the
// system /system/bin/sh. The system shell may vary and may not work at all due to LD_LIBRARY_PATH.
// - A file with shebang, which we try to handle with e.g. /bin/foo -> $PREFIX/bin/foo.
String interpreter = null;
try {
File file = new File(fileToExecute);
try (FileInputStream in = new FileInputStream(file)) {
byte[] buffer = new byte[256];
int bytesRead = in.read(buffer);
if (bytesRead > 4) {
if (buffer[0] == 0x7F && buffer[1] == 'E' && buffer[2] == 'L' && buffer[3] == 'F') {
// Elf file, do nothing.
} else if (buffer[0] == '#' && buffer[1] == '!') {
// Try to parse shebang.
StringBuilder builder = new StringBuilder();
for (int i = 2; i < bytesRead; i++) {
char c = (char) buffer[i];
if (c == ' ' || c == '\n') {
if (builder.length() == 0) {
// Skip whitespace after shebang.
} else {
// End of shebang.
String executable = builder.toString();
if (executable.startsWith("/usr") || executable.startsWith("/bin")) {
String[] parts = executable.split("/");
String binary = parts[parts.length - 1];
interpreter = TermuxConstants.TERMUX_BIN_PREFIX_DIR_PATH + "/" + binary;
}
break;
}
} else {
builder.append(c);
}
}
} else {
// No shebang and no ELF, use standard shell.
interpreter = TermuxConstants.TERMUX_BIN_PREFIX_DIR_PATH + "/sh";
}
}
}
} catch (IOException e) {
// Ignore.
}
List<String> result = new ArrayList<>();
if (interpreter != null) result.add(interpreter);
result.add(fileToExecute);
if (arguments != null) Collections.addAll(result, arguments);
return result.toArray(new String[0]);
}
public static String getExecutableBasename(String executable) {
if(executable == null) return null;
int lastSlash = executable.lastIndexOf('/');
return (lastSlash == -1) ? executable : executable.substring(lastSlash + 1);
}
public static void clearTermuxTMPDIR(Context context) {
String errmsg;
errmsg = FileUtils.clearDirectory(context, "$TMPDIR", FileUtils.getCanonicalPath(TermuxConstants.TERMUX_TMP_PREFIX_DIR_PATH, null, false));
if (errmsg != null) {
Logger.logErrorAndShowToast(context, errmsg);
}
}
}

View File

@@ -0,0 +1,298 @@
/*
* Copyright (C) 2012-2019 Jorrit "Chainfire" Jongma
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.termux.shared.shell;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.List;
import java.util.Locale;
import androidx.annotation.AnyThread;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.WorkerThread;
import com.termux.shared.logger.Logger;
/**
* Thread utility class continuously reading from an InputStream
*/
@SuppressWarnings({"WeakerAccess"})
public class StreamGobbler extends Thread {
private static int threadCounter = 0;
private static int incThreadCounter() {
synchronized (StreamGobbler.class) {
int ret = threadCounter;
threadCounter++;
return ret;
}
}
/**
* Line callback interface
*/
public interface OnLineListener {
/**
* <p>Line callback</p>
*
* <p>This callback should process the line as quickly as possible.
* Delays in this callback may pause the native process or even
* result in a deadlock</p>
*
* @param line String that was gobbled
*/
void onLine(String line);
}
/**
* Stream closed callback interface
*/
public interface OnStreamClosedListener {
/**
* <p>Stream closed callback</p>
*/
void onStreamClosed();
}
@NonNull
private final String shell;
@NonNull
private final InputStream inputStream;
@NonNull
private final BufferedReader reader;
@Nullable
private final List<String> listWriter;
@Nullable
private final StringBuilder stringWriter;
@Nullable
private final OnLineListener lineListener;
@Nullable
private final OnStreamClosedListener streamClosedListener;
private volatile boolean active = true;
private volatile boolean calledOnClose = false;
private static final String LOG_TAG = "StreamGobbler";
/**
* <p>StreamGobbler constructor</p>
*
* <p>We use this class because shell STDOUT and STDERR should be read as quickly as
* possible to prevent a deadlock from occurring, or Process.waitFor() never
* returning (as the buffer is full, pausing the native process)</p>
*
* @param shell Name of the shell
* @param inputStream InputStream to read from
* @param outputList {@literal List<String>} to write to, or null
*/
@AnyThread
public StreamGobbler(@NonNull String shell, @NonNull InputStream inputStream, @Nullable List<String> outputList) {
super("Gobbler#" + incThreadCounter());
this.shell = shell;
this.inputStream = inputStream;
reader = new BufferedReader(new InputStreamReader(inputStream));
streamClosedListener = null;
listWriter = outputList;
stringWriter = null;
lineListener = null;
}
/**
* <p>StreamGobbler constructor</p>
*
* <p>We use this class because shell STDOUT and STDERR should be read as quickly as
* possible to prevent a deadlock from occurring, or Process.waitFor() never
* returning (as the buffer is full, pausing the native process)</p>
* Do not use this for concurrent reading for STDOUT and STDERR for the same StringBuilder since
* its not synchronized.
*
* @param shell Name of the shell
* @param inputStream InputStream to read from
* @param outputString {@literal List<String>} to write to, or null
*/
@AnyThread
public StreamGobbler(@NonNull String shell, @NonNull InputStream inputStream, @Nullable StringBuilder outputString) {
super("Gobbler#" + incThreadCounter());
this.shell = shell;
this.inputStream = inputStream;
reader = new BufferedReader(new InputStreamReader(inputStream));
streamClosedListener = null;
listWriter = null;
stringWriter = outputString;
lineListener = null;
}
/**
* <p>StreamGobbler constructor</p>
*
* <p>We use this class because shell STDOUT and STDERR should be read as quickly as
* possible to prevent a deadlock from occurring, or Process.waitFor() never
* returning (as the buffer is full, pausing the native process)</p>
*
* @param shell Name of the shell
* @param inputStream InputStream to read from
* @param onLineListener OnLineListener callback
* @param onStreamClosedListener OnStreamClosedListener callback
*/
@AnyThread
public StreamGobbler(@NonNull String shell, @NonNull InputStream inputStream, @Nullable OnLineListener onLineListener, @Nullable OnStreamClosedListener onStreamClosedListener) {
super("Gobbler#" + incThreadCounter());
this.shell = shell;
this.inputStream = inputStream;
reader = new BufferedReader(new InputStreamReader(inputStream));
streamClosedListener = onStreamClosedListener;
listWriter = null;
stringWriter = null;
lineListener = onLineListener;
}
@Override
public void run() {
// keep reading the InputStream until it ends (or an error occurs)
// optionally pausing when a command is executed that consumes the InputStream itself
int logLevel = Logger.getLogLevel();
try {
String line;
while ((line = reader.readLine()) != null) {
if(logLevel >= Logger.LOG_LEVEL_VERBOSE)
Logger.logVerbose(LOG_TAG, String.format(Locale.ENGLISH, "[%s] %s", shell, line)); // This will get truncated by LOGGER_ENTRY_MAX_LEN, likely 4KB
if (stringWriter != null) stringWriter.append(line).append("\n");
if (listWriter != null) listWriter.add(line);
if (lineListener != null) lineListener.onLine(line);
while (!active) {
synchronized (this) {
try {
this.wait(128);
} catch (InterruptedException e) {
// no action
}
}
}
}
} catch (IOException e) {
// reader probably closed, expected exit condition
if (streamClosedListener != null) {
calledOnClose = true;
streamClosedListener.onStreamClosed();
}
}
// make sure our stream is closed and resources will be freed
try {
reader.close();
} catch (IOException e) {
// read already closed
}
if (!calledOnClose) {
if (streamClosedListener != null) {
calledOnClose = true;
streamClosedListener.onStreamClosed();
}
}
}
/**
* <p>Resume consuming the input from the stream</p>
*/
@AnyThread
public void resumeGobbling() {
if (!active) {
synchronized (this) {
active = true;
this.notifyAll();
}
}
}
/**
* <p>Suspend gobbling, so other code may read from the InputStream instead</p>
*
* <p>This should <i>only</i> be called from the OnLineListener callback!</p>
*/
@AnyThread
public void suspendGobbling() {
synchronized (this) {
active = false;
this.notifyAll();
}
}
/**
* <p>Wait for gobbling to be suspended</p>
*
* <p>Obviously this cannot be called from the same thread as {@link #suspendGobbling()}</p>
*/
@WorkerThread
public void waitForSuspend() {
synchronized (this) {
while (active) {
try {
this.wait(32);
} catch (InterruptedException e) {
// no action
}
}
}
}
/**
* <p>Is gobbling suspended ?</p>
*
* @return is gobbling suspended?
*/
@AnyThread
public boolean isSuspended() {
synchronized (this) {
return !active;
}
}
/**
* <p>Get current source InputStream</p>
*
* @return source InputStream
*/
@NonNull
@AnyThread
public InputStream getInputStream() {
return inputStream;
}
/**
* <p>Get current OnLineListener</p>
*
* @return OnLineListener
*/
@Nullable
@AnyThread
public OnLineListener getOnLineListener() {
return lineListener;
}
void conditionalJoin() throws InterruptedException {
if (calledOnClose) return; // deadlock from callback, we're inside exit procedure
if (Thread.currentThread() == this) return; // can't join self
join();
}
}