termux-app
325 строк · 10.6 Кб
1/*
2* Copyright (C) 2012-2019 Jorrit "Chainfire" Jongma
3*
4* Licensed under the Apache License, Version 2.0 (the "License");
5* you may not use this file except in compliance with the License.
6* You may obtain a copy of the License at
7*
8* http://www.apache.org/licenses/LICENSE-2.0
9*
10* Unless required by applicable law or agreed to in writing, software
11* distributed under the License is distributed on an "AS IS" BASIS,
12* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13* See the License for the specific language governing permissions and
14* limitations under the License.
15*/
16
17package com.termux.shared.shell;
18
19import java.io.BufferedReader;
20import java.io.IOException;
21import java.io.InputStream;
22import java.io.InputStreamReader;
23import java.util.List;
24import java.util.Locale;
25
26import androidx.annotation.AnyThread;
27import androidx.annotation.NonNull;
28import androidx.annotation.Nullable;
29import androidx.annotation.WorkerThread;
30
31import com.termux.shared.logger.Logger;
32
33/**
34* Thread utility class continuously reading from an InputStream
35*
36* https://github.com/Chainfire/libsuperuser/blob/1.1.0.201907261845/libsuperuser/src/eu/chainfire/libsuperuser/Shell.java#L141
37* https://github.com/Chainfire/libsuperuser/blob/1.1.0.201907261845/libsuperuser/src/eu/chainfire/libsuperuser/StreamGobbler.java
38*/
39@SuppressWarnings({"WeakerAccess"})
40public class StreamGobbler extends Thread {
41private static int threadCounter = 0;
42private static int incThreadCounter() {
43synchronized (StreamGobbler.class) {
44int ret = threadCounter;
45threadCounter++;
46return ret;
47}
48}
49
50/**
51* Line callback interface
52*/
53public interface OnLineListener {
54/**
55* <p>Line callback</p>
56*
57* <p>This callback should process the line as quickly as possible.
58* Delays in this callback may pause the native process or even
59* result in a deadlock</p>
60*
61* @param line String that was gobbled
62*/
63void onLine(String line);
64}
65
66/**
67* Stream closed callback interface
68*/
69public interface OnStreamClosedListener {
70/**
71* <p>Stream closed callback</p>
72*/
73void onStreamClosed();
74}
75
76@NonNull
77private final String shell;
78@NonNull
79private final InputStream inputStream;
80@NonNull
81private final BufferedReader reader;
82@Nullable
83private final List<String> listWriter;
84@Nullable
85private final StringBuilder stringWriter;
86@Nullable
87private final OnLineListener lineListener;
88@Nullable
89private final OnStreamClosedListener streamClosedListener;
90@Nullable
91private final Integer mLogLevel;
92private volatile boolean active = true;
93private volatile boolean calledOnClose = false;
94
95private static final String LOG_TAG = "StreamGobbler";
96
97/**
98* <p>StreamGobbler constructor</p>
99*
100* <p>We use this class because shell STDOUT and STDERR should be read as quickly as
101* possible to prevent a deadlock from occurring, or Process.waitFor() never
102* returning (as the buffer is full, pausing the native process)</p>
103*
104* @param shell Name of the shell
105* @param inputStream InputStream to read from
106* @param outputList {@literal List<String>} to write to, or null
107* @param logLevel The custom log level to use for logging the command output. If set to
108* {@code null}, then {@link Logger#LOG_LEVEL_VERBOSE} will be used.
109*/
110@AnyThread
111public StreamGobbler(@NonNull String shell, @NonNull InputStream inputStream,
112@Nullable List<String> outputList,
113@Nullable Integer logLevel) {
114super("Gobbler#" + incThreadCounter());
115this.shell = shell;
116this.inputStream = inputStream;
117reader = new BufferedReader(new InputStreamReader(inputStream));
118streamClosedListener = null;
119
120listWriter = outputList;
121stringWriter = null;
122lineListener = null;
123
124mLogLevel = logLevel;
125}
126
127/**
128* <p>StreamGobbler constructor</p>
129*
130* <p>We use this class because shell STDOUT and STDERR should be read as quickly as
131* possible to prevent a deadlock from occurring, or Process.waitFor() never
132* returning (as the buffer is full, pausing the native process)</p>
133* Do not use this for concurrent reading for STDOUT and STDERR for the same StringBuilder since
134* its not synchronized.
135*
136* @param shell Name of the shell
137* @param inputStream InputStream to read from
138* @param outputString {@literal List<String>} to write to, or null
139* @param logLevel The custom log level to use for logging the command output. If set to
140* {@code null}, then {@link Logger#LOG_LEVEL_VERBOSE} will be used.
141*/
142@AnyThread
143public StreamGobbler(@NonNull String shell, @NonNull InputStream inputStream,
144@Nullable StringBuilder outputString,
145@Nullable Integer logLevel) {
146super("Gobbler#" + incThreadCounter());
147this.shell = shell;
148this.inputStream = inputStream;
149reader = new BufferedReader(new InputStreamReader(inputStream));
150streamClosedListener = null;
151
152listWriter = null;
153stringWriter = outputString;
154lineListener = null;
155
156mLogLevel = logLevel;
157}
158
159/**
160* <p>StreamGobbler constructor</p>
161*
162* <p>We use this class because shell STDOUT and STDERR should be read as quickly as
163* possible to prevent a deadlock from occurring, or Process.waitFor() never
164* returning (as the buffer is full, pausing the native process)</p>
165*
166* @param shell Name of the shell
167* @param inputStream InputStream to read from
168* @param onLineListener OnLineListener callback
169* @param onStreamClosedListener OnStreamClosedListener callback
170* @param logLevel The custom log level to use for logging the command output. If set to
171* {@code null}, then {@link Logger#LOG_LEVEL_VERBOSE} will be used.
172*/
173@AnyThread
174public StreamGobbler(@NonNull String shell, @NonNull InputStream inputStream,
175@Nullable OnLineListener onLineListener,
176@Nullable OnStreamClosedListener onStreamClosedListener,
177@Nullable Integer logLevel) {
178super("Gobbler#" + incThreadCounter());
179this.shell = shell;
180this.inputStream = inputStream;
181reader = new BufferedReader(new InputStreamReader(inputStream));
182streamClosedListener = onStreamClosedListener;
183
184listWriter = null;
185stringWriter = null;
186lineListener = onLineListener;
187
188mLogLevel = logLevel;
189}
190
191@Override
192public void run() {
193String defaultLogTag = Logger.getDefaultLogTag();
194boolean loggingEnabled = Logger.shouldEnableLoggingForCustomLogLevel(mLogLevel);
195if (loggingEnabled)
196Logger.logVerbose(LOG_TAG, "Using custom log level: " + mLogLevel + ", current log level: " + Logger.getLogLevel());
197
198// keep reading the InputStream until it ends (or an error occurs)
199// optionally pausing when a command is executed that consumes the InputStream itself
200try {
201String line;
202while ((line = reader.readLine()) != null) {
203if (loggingEnabled)
204Logger.logVerboseForce(defaultLogTag + "Command", String.format(Locale.ENGLISH, "[%s] %s", shell, line)); // This will get truncated by LOGGER_ENTRY_MAX_LEN, likely 4KB
205
206if (stringWriter != null) stringWriter.append(line).append("\n");
207if (listWriter != null) listWriter.add(line);
208if (lineListener != null) lineListener.onLine(line);
209while (!active) {
210synchronized (this) {
211try {
212this.wait(128);
213} catch (InterruptedException e) {
214// no action
215}
216}
217}
218}
219} catch (IOException e) {
220// reader probably closed, expected exit condition
221if (streamClosedListener != null) {
222calledOnClose = true;
223streamClosedListener.onStreamClosed();
224}
225}
226
227// make sure our stream is closed and resources will be freed
228try {
229reader.close();
230} catch (IOException e) {
231// read already closed
232}
233
234if (!calledOnClose) {
235if (streamClosedListener != null) {
236calledOnClose = true;
237streamClosedListener.onStreamClosed();
238}
239}
240}
241
242/**
243* <p>Resume consuming the input from the stream</p>
244*/
245@AnyThread
246public void resumeGobbling() {
247if (!active) {
248synchronized (this) {
249active = true;
250this.notifyAll();
251}
252}
253}
254
255/**
256* <p>Suspend gobbling, so other code may read from the InputStream instead</p>
257*
258* <p>This should <i>only</i> be called from the OnLineListener callback!</p>
259*/
260@AnyThread
261public void suspendGobbling() {
262synchronized (this) {
263active = false;
264this.notifyAll();
265}
266}
267
268/**
269* <p>Wait for gobbling to be suspended</p>
270*
271* <p>Obviously this cannot be called from the same thread as {@link #suspendGobbling()}</p>
272*/
273@WorkerThread
274public void waitForSuspend() {
275synchronized (this) {
276while (active) {
277try {
278this.wait(32);
279} catch (InterruptedException e) {
280// no action
281}
282}
283}
284}
285
286/**
287* <p>Is gobbling suspended ?</p>
288*
289* @return is gobbling suspended?
290*/
291@AnyThread
292public boolean isSuspended() {
293synchronized (this) {
294return !active;
295}
296}
297
298/**
299* <p>Get current source InputStream</p>
300*
301* @return source InputStream
302*/
303@NonNull
304@AnyThread
305public InputStream getInputStream() {
306return inputStream;
307}
308
309/**
310* <p>Get current OnLineListener</p>
311*
312* @return OnLineListener
313*/
314@Nullable
315@AnyThread
316public OnLineListener getOnLineListener() {
317return lineListener;
318}
319
320void conditionalJoin() throws InterruptedException {
321if (calledOnClose) return; // deadlock from callback, we're inside exit procedure
322if (Thread.currentThread() == this) return; // can't join self
323join();
324}
325}
326