Nagram/TMessagesProj/src/main/java/tw/nekomimi/nekogram/GuardedProcessPool.kt
2020-06-25 15:28:55 +00:00

119 lines
5.5 KiB
Kotlin

/*******************************************************************************
* *
* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> *
* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
* *
* This program is free software: you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation, either version 3 of the License, or *
* (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
* *
*******************************************************************************/
package tw.nekomimi.nekogram
import android.annotation.SuppressLint
import android.os.Build
import android.os.SystemClock
import android.system.ErrnoException
import android.system.Os
import android.system.OsConstants
import androidx.annotation.MainThread
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.Channel
import org.telegram.messenger.ApplicationLoader
import org.telegram.messenger.FileLog
import java.io.File
import java.io.IOException
import java.io.InputStream
import kotlin.concurrent.thread
class GuardedProcessPool(private val onFatal: suspend (IOException) -> Unit) : CoroutineScope {
companion object {
private const val TAG = "GuardedProcessPool"
}
private inner class Guard(private val cmd: List<String>) {
private lateinit var process: Process
private fun streamLogger(input: InputStream, logger: (String) -> Unit) = try {
input.bufferedReader().forEachLine(logger)
} catch (_: IOException) { } // ignore
fun start() {
process = ProcessBuilder(cmd).directory(ApplicationLoader.applicationContext.cacheDir).start()
}
suspend fun looper(onRestartCallback: (suspend () -> Unit)?) {
var running = true
val cmdName = File(cmd.first()).nameWithoutExtension
val exitChannel = Channel<Int>()
try {
while (true) {
thread(name = "stderr-$cmdName") {
streamLogger(process.errorStream) {
FileLog.e("[$cmdName]$it")
}
}
thread(name = "stdout-$cmdName") {
streamLogger(process.inputStream) {
FileLog.d("[$cmdName]$it")
}
// this thread also acts as a daemon thread for waitFor
runBlocking { exitChannel.send(process.waitFor()) }
}
val startTime = SystemClock.elapsedRealtime()
val exitCode = exitChannel.receive()
running = false
when {
SystemClock.elapsedRealtime() - startTime < 1000 -> throw IOException(
"$cmdName exits too fast (exit code: $exitCode)")
exitCode == 128 + OsConstants.SIGKILL -> FileLog.w("$cmdName was killed")
else -> FileLog.e(IOException("$cmdName unexpectedly exits with code $exitCode"))
}
start()
running = true
onRestartCallback?.invoke()
}
} catch (e: IOException) {
FileLog.w("error occurred. stop guard: " + cmd.joinToString(" "))
GlobalScope.launch(Dispatchers.Main) { onFatal(e) }
} finally {
if (running) withContext(NonCancellable) {
process.destroy() // kill the process
if (Build.VERSION.SDK_INT >= 26) {
if (withTimeoutOrNull(1000) { exitChannel.receive() } != null) return@withContext
process.destroyForcibly() // Force to kill the process if it's still alive
}
exitChannel.receive()
} // otherwise process already exited, nothing to be done
}
}
}
override val coroutineContext = Dispatchers.Main.immediate + Job()
@MainThread
fun start(cmd: List<String>, onRestartCallback: (suspend () -> Unit)? = null) {
FileLog.d("start process: " + cmd.joinToString (" "))
Guard(cmd).apply {
start() // if start fails, IOException will be thrown directly
launch { looper(onRestartCallback) }
}
}
@MainThread
fun close(scope: CoroutineScope) {
cancel()
coroutineContext[Job]!!.also { job -> scope.launch { job.join() } }
}
}