When `Logger.CURRENT_LOG_LEVEL` set by user is `Logger.LOG_VERBOSE`, then background (not foreground sessions) command output was being logged to logcat, however, if command outputted too much data to logcat, then logcat clients like in Android Studio would crash. Also if a logcat dump is being taken inside termux, then duplicate lines would occur, first one due to of original entry, and second one due to StreamGobbler logging output at verbose level for logcat command. This would be a concern for plugins as well like `RUN_COMMAND` intent or Termux:Tasker, etc if they ran commands with lot of data and user had set log level to verbose. For plugins, TermuxService now supports `com.termux.execute.background_custom_log_level` `String` extra for custom log level. Termux:Tasker, etc will have to be updated with support. For `RUN_COMMAND` intent, the `com.termux.RUN_COMMAND_BACKGROUND_CUSTOM_LOG_LEVEL` `String` extra is now provided to set custom log level for only the command output. Check `TermuxConstants`. So one can pass a custom log level that is `>=` to the log level set it termux settings where (OFF=0, NORMAL=1, DEBUG=2, VERBOSE=3). If you pass `0`, it will completely disable logging. If you pass `1`, logging will only be enabled if log level in termux settings is `NORMAL` or higher. If custom log level is not passed, then old behaviour will remain and log level in termux settings must be `VERBOSE` or higher for logging to be enabled. Note that the log entries will still be logged with priority `Log.VERBOSE` regardless of log level, i.e `logcat` will have `V/`. The entries logcat component has now changed from `StreamGobbler` to `TermuxCommand`. For output at `stdout`, the entry format is `[<pid>-stdout] ...` and for the output at `stderr`, the entry format is `[<pid>-stderr] ...`. The `<pid>` will be process id as an integer that was started by termux. For example: `V/TermuxCommand: [66666-stdout] ...`. While doing this I realize that instead of using `am` command to send messages back to tasker, you can use tasker `Logcat Entry` profile event to listen to messages from termux at both `stdout` and `stderr`. This might be faster than `am` command intent systems or at least possibly more convenient in some use cases. So setup a profile with the `Component` value set to `TermuxCommand` and `Filter` value set to `-E 'TermuxCommand: \[[0-9]+-((stdout)|(stderr))\] message_tag: .*'` and enable the `Grep Filter` toggle so that entry matching is done in native code. Check https://github.com/joaomgcd/TaskerDocumentation/blob/master/en/help/logcat%20info.md for details. Also enable `Enforce Task Order` in profile settings and set collision handling to `Run Both Together` so that if two or more entries are sent quickly, entry task is run for all. Tasker currently (v5.13.16) is not maintaining order of entry tasks despite the setting. Then you can send an intent from tasker via `Run Shell` action with `root` (since `am` command won't work without it on android >=8) or normally in termux from a script, you should be able to receive the entries as `@lc_text` in entry task of tasker `Logcat Entry` profile. The following just passes two `echo` commands to `bash` as a script via `stdin`. If you don't have root, then you can call a wrapper script with `TermuxCommand` function in `Tasker Function` action that sends another `RUN_COMMAND` intent with termux provide `am` command which will work without root. ``` am startservice --user 0 -n com.termux/com.termux.app.RunCommandService -a com.termux.RUN_COMMAND --es com.termux.RUN_COMMAND_PATH '/data/data/com.termux/files/usr/bin/bash' --es com.termux.RUN_COMMAND_STDIN 'echo "message_tag: Sending message from tasker to termux"' --ez com.termux.RUN_COMMAND_BACKGROUND true --es com.termux.RUN_COMMAND_BACKGROUND_CUSTOM_LOG_LEVEL '1' ```
332 lines
11 KiB
Java
332 lines
11 KiB
Java
/*
|
|
* 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
|
|
*
|
|
* https://github.com/Chainfire/libsuperuser/blob/1.1.0.201907261845/libsuperuser/src/eu/chainfire/libsuperuser/Shell.java#L141
|
|
* https://github.com/Chainfire/libsuperuser/blob/1.1.0.201907261845/libsuperuser/src/eu/chainfire/libsuperuser/StreamGobbler.java
|
|
*/
|
|
@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;
|
|
@Nullable
|
|
private final Integer mLlogLevel;
|
|
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
|
|
* @param logLevel The custom log level to use for logging by command output. If set to
|
|
* {@code null}, then {@link Logger#LOG_LEVEL_VERBOSE} will be used.
|
|
*/
|
|
@AnyThread
|
|
public StreamGobbler(@NonNull String shell, @NonNull InputStream inputStream,
|
|
@Nullable List<String> outputList,
|
|
@Nullable Integer logLevel) {
|
|
super("Gobbler#" + incThreadCounter());
|
|
this.shell = shell;
|
|
this.inputStream = inputStream;
|
|
reader = new BufferedReader(new InputStreamReader(inputStream));
|
|
streamClosedListener = null;
|
|
|
|
listWriter = outputList;
|
|
stringWriter = null;
|
|
lineListener = null;
|
|
|
|
mLlogLevel = logLevel;
|
|
}
|
|
|
|
/**
|
|
* <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
|
|
* @param logLevel The custom log level to use for logging by command output. If set to
|
|
* {@code null}, then {@link Logger#LOG_LEVEL_VERBOSE} will be used.
|
|
*/
|
|
@AnyThread
|
|
public StreamGobbler(@NonNull String shell, @NonNull InputStream inputStream,
|
|
@Nullable StringBuilder outputString,
|
|
@Nullable Integer logLevel) {
|
|
super("Gobbler#" + incThreadCounter());
|
|
this.shell = shell;
|
|
this.inputStream = inputStream;
|
|
reader = new BufferedReader(new InputStreamReader(inputStream));
|
|
streamClosedListener = null;
|
|
|
|
listWriter = null;
|
|
stringWriter = outputString;
|
|
lineListener = null;
|
|
|
|
mLlogLevel = logLevel;
|
|
}
|
|
|
|
/**
|
|
* <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
|
|
* @param logLevel The custom log level to use for logging by command output. If set to
|
|
* {@code null}, then {@link Logger#LOG_LEVEL_VERBOSE} will be used.
|
|
*/
|
|
@AnyThread
|
|
public StreamGobbler(@NonNull String shell, @NonNull InputStream inputStream,
|
|
@Nullable OnLineListener onLineListener,
|
|
@Nullable OnStreamClosedListener onStreamClosedListener,
|
|
@Nullable Integer logLevel) {
|
|
super("Gobbler#" + incThreadCounter());
|
|
this.shell = shell;
|
|
this.inputStream = inputStream;
|
|
reader = new BufferedReader(new InputStreamReader(inputStream));
|
|
streamClosedListener = onStreamClosedListener;
|
|
|
|
listWriter = null;
|
|
stringWriter = null;
|
|
lineListener = onLineListener;
|
|
|
|
mLlogLevel = logLevel;
|
|
}
|
|
|
|
@Override
|
|
public void run() {
|
|
String defaultLogTag = Logger.DEFAULT_LOG_TAG;
|
|
int currentLogLevel = Logger.getLogLevel();
|
|
|
|
int customLogLevel;
|
|
if (mLlogLevel != null && mLlogLevel >= Logger.LOG_LEVEL_OFF) {
|
|
customLogLevel = mLlogLevel;
|
|
Logger.logVerbose(LOG_TAG, "Using custom log level: " + customLogLevel + ", current log level: " + currentLogLevel);
|
|
} else {
|
|
customLogLevel = Logger.LOG_LEVEL_VERBOSE;
|
|
}
|
|
|
|
// keep reading the InputStream until it ends (or an error occurs)
|
|
// optionally pausing when a command is executed that consumes the InputStream itself
|
|
try {
|
|
String line;
|
|
while ((line = reader.readLine()) != null) {
|
|
if (customLogLevel >= currentLogLevel)
|
|
Logger.logVerboseForce(defaultLogTag + "Command", 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();
|
|
}
|
|
}
|