termux-app

Форк
0
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

17
package com.termux.shared.shell;
18

19
import java.io.BufferedReader;
20
import java.io.IOException;
21
import java.io.InputStream;
22
import java.io.InputStreamReader;
23
import java.util.List;
24
import java.util.Locale;
25

26
import androidx.annotation.AnyThread;
27
import androidx.annotation.NonNull;
28
import androidx.annotation.Nullable;
29
import androidx.annotation.WorkerThread;
30

31
import 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"})
40
public class StreamGobbler extends Thread {
41
    private static int threadCounter = 0;
42
    private static int incThreadCounter() {
43
        synchronized (StreamGobbler.class) {
44
            int ret = threadCounter;
45
            threadCounter++;
46
            return ret;
47
        }
48
    }
49

50
    /**
51
     * Line callback interface
52
     */
53
    public 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
         */
63
        void onLine(String line);
64
    }
65

66
    /**
67
     * Stream closed callback interface
68
     */
69
    public interface OnStreamClosedListener {
70
        /**
71
         * <p>Stream closed callback</p>
72
         */
73
        void onStreamClosed();
74
    }
75

76
    @NonNull
77
    private final String shell;
78
    @NonNull
79
    private final InputStream inputStream;
80
    @NonNull
81
    private final BufferedReader reader;
82
    @Nullable
83
    private final List<String> listWriter;
84
    @Nullable
85
    private final StringBuilder stringWriter;
86
    @Nullable
87
    private final OnLineListener lineListener;
88
    @Nullable
89
    private final OnStreamClosedListener streamClosedListener;
90
    @Nullable
91
    private final Integer mLogLevel;
92
    private volatile boolean active = true;
93
    private volatile boolean calledOnClose = false;
94

95
    private 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
111
    public StreamGobbler(@NonNull String shell, @NonNull InputStream inputStream,
112
                         @Nullable List<String> outputList,
113
                         @Nullable Integer logLevel) {
114
        super("Gobbler#" + incThreadCounter());
115
        this.shell = shell;
116
        this.inputStream = inputStream;
117
        reader = new BufferedReader(new InputStreamReader(inputStream));
118
        streamClosedListener = null;
119

120
        listWriter = outputList;
121
        stringWriter = null;
122
        lineListener = null;
123

124
        mLogLevel = 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
143
    public StreamGobbler(@NonNull String shell, @NonNull InputStream inputStream,
144
                         @Nullable StringBuilder outputString,
145
                         @Nullable Integer logLevel) {
146
        super("Gobbler#" + incThreadCounter());
147
        this.shell = shell;
148
        this.inputStream = inputStream;
149
        reader = new BufferedReader(new InputStreamReader(inputStream));
150
        streamClosedListener = null;
151

152
        listWriter = null;
153
        stringWriter = outputString;
154
        lineListener = null;
155

156
        mLogLevel = 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
174
    public StreamGobbler(@NonNull String shell, @NonNull InputStream inputStream,
175
                         @Nullable OnLineListener onLineListener,
176
                         @Nullable OnStreamClosedListener onStreamClosedListener,
177
                         @Nullable Integer logLevel) {
178
        super("Gobbler#" + incThreadCounter());
179
        this.shell = shell;
180
        this.inputStream = inputStream;
181
        reader = new BufferedReader(new InputStreamReader(inputStream));
182
        streamClosedListener = onStreamClosedListener;
183

184
        listWriter = null;
185
        stringWriter = null;
186
        lineListener = onLineListener;
187

188
        mLogLevel = logLevel;
189
    }
190

191
    @Override
192
    public void run() {
193
        String defaultLogTag = Logger.getDefaultLogTag();
194
        boolean loggingEnabled = Logger.shouldEnableLoggingForCustomLogLevel(mLogLevel);
195
        if (loggingEnabled)
196
            Logger.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
200
        try {
201
            String line;
202
            while ((line = reader.readLine()) != null) {
203
                if (loggingEnabled)
204
                    Logger.logVerboseForce(defaultLogTag + "Command", String.format(Locale.ENGLISH, "[%s] %s", shell, line)); // This will get truncated by LOGGER_ENTRY_MAX_LEN, likely 4KB
205

206
                if (stringWriter != null) stringWriter.append(line).append("\n");
207
                if (listWriter != null) listWriter.add(line);
208
                if (lineListener != null) lineListener.onLine(line);
209
                while (!active) {
210
                    synchronized (this) {
211
                        try {
212
                            this.wait(128);
213
                        } catch (InterruptedException e) {
214
                            // no action
215
                        }
216
                    }
217
                }
218
            }
219
        } catch (IOException e) {
220
            // reader probably closed, expected exit condition
221
            if (streamClosedListener != null) {
222
                calledOnClose = true;
223
                streamClosedListener.onStreamClosed();
224
            }
225
        }
226

227
        // make sure our stream is closed and resources will be freed
228
        try {
229
            reader.close();
230
        } catch (IOException e) {
231
            // read already closed
232
        }
233

234
        if (!calledOnClose) {
235
            if (streamClosedListener != null) {
236
                calledOnClose = true;
237
                streamClosedListener.onStreamClosed();
238
            }
239
        }
240
    }
241

242
    /**
243
     * <p>Resume consuming the input from the stream</p>
244
     */
245
    @AnyThread
246
    public void resumeGobbling() {
247
        if (!active) {
248
            synchronized (this) {
249
                active = true;
250
                this.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
261
    public void suspendGobbling() {
262
        synchronized (this) {
263
            active = false;
264
            this.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
274
    public void waitForSuspend() {
275
        synchronized (this) {
276
            while (active) {
277
                try {
278
                    this.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
292
    public boolean isSuspended() {
293
        synchronized (this) {
294
            return !active;
295
        }
296
    }
297

298
    /**
299
     * <p>Get current source InputStream</p>
300
     *
301
     * @return source InputStream
302
     */
303
    @NonNull
304
    @AnyThread
305
    public InputStream getInputStream() {
306
        return inputStream;
307
    }
308

309
    /**
310
     * <p>Get current OnLineListener</p>
311
     *
312
     * @return OnLineListener
313
     */
314
    @Nullable
315
    @AnyThread
316
    public OnLineListener getOnLineListener() {
317
        return lineListener;
318
    }
319

320
    void conditionalJoin() throws InterruptedException {
321
        if (calledOnClose) return; // deadlock from callback, we're inside exit procedure
322
        if (Thread.currentThread() == this) return; // can't join self
323
        join();
324
    }
325
}
326

Использование cookies

Мы используем файлы cookie в соответствии с Политикой конфиденциальности и Политикой использования cookies.

Нажимая кнопку «Принимаю», Вы даете АО «СберТех» согласие на обработку Ваших персональных данных в целях совершенствования нашего веб-сайта и Сервиса GitVerse, а также повышения удобства их использования.

Запретить использование cookies Вы можете самостоятельно в настройках Вашего браузера.