/* app_process + APK: настоящий JNI/worker, удержанный в диагностическом callback до/после create instance. * Аргумент — новый /data/local/tmp/utun-shutdown-audit-/profile; компилировать как friend module. * CoreShutdown работает на Android main; JNI stop ждёт на IO. Frontend media/Activity здесь не создаются. */ import android.os.Handler import android.os.Looper import com.utun.chat.data.ChatEventCallback import com.utun.chat.data.NativeLib import com.utun.chat.headless.CoreShutdown import java.io.File import java.util.concurrent.CountDownLatch import java.util.concurrent.TimeUnit import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicInteger import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineStart import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.async import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import kotlinx.coroutines.withTimeout import kotlin.system.exitProcess class ShutdownProbeLog(private val gate: String?) { val entered = CompletableDeferred() val stopping = CompletableDeferred() val release = CountDownLatch(1) private val armed = AtomicBoolean(gate != null) fun onNativeLog(level: Int, category: String, message: String) { if (message.contains("Android worker stop requested")) stopping.complete(Unit) if (gate != null && message.contains(gate) && armed.compareAndSet(true, false)) { println("worker held at $gate") entered.complete(Unit) check(release.await(10, TimeUnit.SECONDS)) { "test did not release worker at $gate" } } } } private fun nativeStop(name: String = "nativeStop"): Boolean { val method = NativeLib::class.java.getDeclaredMethod(name) method.isAccessible = true return method.invoke(NativeLib) == true } private fun nativeInit(directory: File, logger: ShutdownProbeLog) { val database = File(directory, "chats.db") val existed = database.exists() val method = NativeLib::class.java.getDeclaredMethod("nativeInit", String::class.java, Int::class.javaPrimitiveType, Any::class.java, Any::class.java) method.isAccessible = true check(method.invoke(NativeLib, directory.path, 0, null, logger) == true) check(database.exists() == existed) { "callback initialization created a database before worker startup" } } fun main(args: Array) { check(args.size == 1) val directory = File(args.single()).canonicalFile check(directory.path.startsWith("/data/local/tmp/utun-shutdown-audit-") && !directory.exists()) check(directory.mkdirs()) if (Looper.getMainLooper() == null) Looper.prepareMainLooper() val handler = Handler(Looper.getMainLooper()) val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate) val started = AtomicInteger() val readiness = Channel(Channel.UNLIMITED) val gates = mutableListOf() NativeLib.setEventCallback(object : ChatEventCallback { override fun onEvent(type: Int, data: ByteArray?) { if (type == 12) { started.incrementAndGet(); check(readiness.trySend(Unit).isSuccess) } } }) scope.launch { try { val config = "[global]\ndb_path=${directory.path}\nauto_sockets=no\n" + "[ntp]\nenabled=no\n[stun]\nenabled=no\n[chatserver]\nstorage_autoload=0\n" + "local_invites_enabled=0\nlocal_broadcasts_enabled=0\n" for (phase in listOf("Android identity saved", "core started")) { val logger = ShutdownProbeLog(phase).also { gates.add(it) } nativeInit(directory, logger) val previousStarts = started.get() check(NativeLib.start(config)) withTimeout(10_000) { logger.entered.await() } check(!NativeLib.isReady()) { "worker unexpectedly ready while held at $phase" } val shutdown = CoreShutdown(scope, stop = { check(withContext(Dispatchers.IO) { nativeStop() }) { "native stop rejected" } }, log = { level, message -> println("$level $message") }) val cleanup = shutdown.request() val retired = async(start = CoroutineStart.UNDISPATCHED) { shutdown.await() } retired.cancel() var replacementInitialized = false val replacement = async(start = CoroutineStart.UNDISPATCHED) { shutdown.await() check(started.get() == previousStarts && !NativeLib.isReady()) { "cancelled worker announced ready" } nativeInit(directory, ShutdownProbeLog(null)) replacementInitialized = true check(NativeLib.start(config)) } withTimeout(10_000) { logger.stopping.await() } check(!cleanup.isCompleted && !replacementInitialized) val heartbeat = CompletableDeferred() handler.post { heartbeat.complete(Unit) } withTimeout(1_000) { heartbeat.await() } check(!cleanup.isCompleted) { "stop returned while worker was held at $phase" } logger.release.countDown() withTimeout(10_000) { cleanup.await(); replacement.await(); readiness.receive() } check(replacementInitialized && NativeLib.isReady() && started.get() == previousStarts + 1) withTimeout(10_000) { shutdown.request().await() } check(!NativeLib.isReady()) println("PASS: cancellation at $phase, responsive main, retained JNI callbacks, replacement starts after join") } } catch (error: Throwable) { error.printStackTrace(System.err) throw error } finally { gates.forEach { it.release.countDown() } check(withContext(Dispatchers.IO) { nativeStop("nativeDestroy") }) } exitProcess(0) } Looper.loop() }