owenclave / app/src/main/java/io/nekohasekai/sagernet/bg/GuardedProcessPool.kt

github - dev - back to tree - upstream
/****************************************************************************** * * * Copyright (C) 2021 by nekohasekai * * Copyright (C) 2021 by Max Lv * * Copyright (C) 2021 by Mygod Studio * * * * 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 . * * * ******************************************************************************/ package io.nekohasekai.sagernet.bg import android.os.Build import android.os.SystemClock import android.system.ErrnoException import android.system.Os import android.system.OsConstants import android.util.Log import androidx.annotation.MainThread import io.nekohasekai.sagernet.SagerNet import io.nekohasekai.sagernet.ktx.Logs import io.nekohasekai.sagernet.utils.Commandline import kotlinx.coroutines.* import kotlinx.coroutines.channels.Channel 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 val pid by lazy { Class.forName("java.lang.ProcessManager\$ProcessImpl").getDeclaredField("pid") .apply { isAccessible = true } } } private inner class Guard(private val cmd: List, private val env: Map = mapOf()) { 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(SagerNet.deviceStorage.noBackupFilesDir).apply { environment().putAll(env) }.start() } @DelicateCoroutinesApi suspend fun looper(onRestartCallback: (suspend () -> Unit)?) { var running = true val cmdName = File(cmd.first()).nameWithoutExtension val exitChannel = Channel() try { while (true) { thread(name = "stderr-$cmdName") { streamLogger(process.errorStream) { Log.e(cmdName, it) } } thread(name = "stdout-$cmdName") { streamLogger(process.inputStream) { Log.i(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 -> Logs.w("$cmdName was killed") else -> Logs.w(IOException("$cmdName unexpectedly exits with code $exitCode")) } Logs.i("restart process: ${Commandline.toString(cmd)} (last exit code: $exitCode)") start() running = true onRestartCallback?.invoke() } } catch (e: IOException) { Logs.w("error occurred. stop guard: ${Commandline.toString(cmd)}") GlobalScope.launch(Dispatchers.Default) { onFatal(e) } } finally { if (running) withContext(NonCancellable) { // clean-up cannot be cancelled if (Build.VERSION.SDK_INT < 24) { try { Os.kill(pid.get(process) as Int, OsConstants.SIGTERM) } catch (e: ErrnoException) { if (e.errno != OsConstants.ESRCH) Logs.w(e) } catch (e: ReflectiveOperationException) { Logs.w(e) } if (withTimeoutOrNull(500) { exitChannel.receive() } != null) return@withContext } 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,env: Map = mapOf(), onRestartCallback: (suspend () -> Unit)? = null) { Logs.i("start process: ${Commandline.toString(cmd)}") Guard(cmd, env).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.cancelAndJoin() } } } }