package com.gacy.android.service import android.app.Notification import android.app.NotificationChannel import android.app.NotificationManager import android.app.PendingIntent import android.app.Service import android.content.Context import android.content.Intent import android.content.pm.ServiceInfo import android.os.Build import android.os.IBinder import androidx.core.app.NotificationCompat import com.gacy.android.GaCyApplication import com.gacy.android.MainActivity import com.gacy.android.R import com.gacy.android.data.GaCyApiRepository import com.gacy.android.data.GoRuntimeRepository import com.gacy.android.data.PreferencesRepository import com.gacy.android.model.ServerConfig import com.gacy.android.model.ServerExecutionMode import com.gacy.android.model.ServerStatus import kotlinx.coroutines.* import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import java.io.File import java.util.concurrent.TimeUnit class ServerService : Service() { private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO) private var job: Job? = null private val processes = mutableListOf() private val lock = Any() @Volatile private var generation = 0 override fun onBind(intent: Intent?): IBinder? = null override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { when (intent?.action ?: ACTION_START) { ACTION_START -> startServer() ACTION_STOP -> stopServer() ACTION_RESTART -> { stopServer(false); startServer() } } return START_NOT_STICKY } private fun startServer() { if (_serverStatus.value == ServerStatus.STARTING || _serverStatus.value == ServerStatus.RUNNING) return val run = ++generation val config = PreferencesRepository(this).getServerConfig() _serverStatus.value = ServerStatus.STARTING startForegroundCompat("Starting GaCy Go server") job = scope.launch { try { if (config.executionMode == ServerExecutionMode.LOCAL_EMBEDDED) { val runtime = GoRuntimeRepository(applicationContext) LogBuffer.append("[Setup] Preparing GaCy Go runtime") runtime.prepare() ensureActive() launchProcess("gameserver", runtime.directory, config, run) launchProcess("sdkserver", runtime.directory, config, run) } val api = GaCyApiRepository() var ready = false repeat(40) { if (!ready) { ensureActive() if (config.executionMode == ServerExecutionMode.LOCAL_EMBEDDED) checkProcesses() ready = api.checkServerHealth(config).first if (!ready) delay(500) } } if (!ready) error("SDK server did not respond") if (run != generation) return@launch _serverStatus.value = ServerStatus.RUNNING updateNotification("GaCy Go server is running") LogBuffer.append("[System] SDK server is ready at ${config.dispatchUrl}") if (config.executionMode == ServerExecutionMode.LOCAL_EMBEDDED) { while (isActive && run == generation) { delay(1000) checkProcesses() } } else { var failures = 0 while (isActive && run == generation) { delay(5000) val healthy = api.checkServerHealth(config).first ensureActive() failures = if (healthy) 0 else failures + 1 if (failures >= 3) error("Remote SDK server stopped responding") } } } catch (_: CancellationException) { } catch (e: Exception) { if (run == generation) { LogBuffer.append("[Server error] ${e.message}") terminateProcesses() _serverStatus.value = ServerStatus.ERROR stopForeground(STOP_FOREGROUND_REMOVE) stopSelf() } } } } private fun checkProcesses() { val failed = synchronized(lock) { processes.firstOrNull { !it.isAlive } } if (failed != null) error("A Go server process exited with code ${failed.exitValue()}; see the log") } private fun launchProcess(name: String, directory: File, config: ServerConfig, run: Int) { val executable = File(applicationInfo.nativeLibraryDir, "libgacy_${name}.so") check(executable.isFile) { "Missing Go runtime: ${executable.name}" } val builder = ProcessBuilder(executable.absolutePath).directory(directory).redirectErrorStream(true) builder.environment().apply { put("GAC_SDK_PORT", config.dispatchPort.toString()) put("GAC_GAME_PORT", config.gameserverPort.toString()) put("GAC_PUBLIC_GAME_HOST", config.remoteHost) put("GAC_PUBLIC_SDK_URL", "http://${config.remoteHost}:${config.dispatchPort}") } val process = builder.start() synchronized(lock) { if (run != generation) { process.destroy(); throw CancellationException() } processes.add(process) } scope.launch { try { process.inputStream.bufferedReader().useLines { lines -> lines.forEach { LogBuffer.append("[$name] $it") } } } catch (e: java.io.IOException) { if (process.isAlive && run == generation) LogBuffer.append("[$name] Log stream error: ${e.message}") } } } private fun terminateProcesses() { val old = synchronized(lock) { processes.toList().also { processes.clear() } } old.forEach { it.destroy() } old.forEach { if (!it.waitFor(2, TimeUnit.SECONDS)) it.destroyForcibly() } } private fun stopServer(stopService: Boolean = true) { ++generation job?.cancel() job = null _serverStatus.value = ServerStatus.STOPPING terminateProcesses() _serverStatus.value = ServerStatus.STOPPED LogBuffer.append("[System] GaCy server stopped") if (stopService) { stopForeground(STOP_FOREGROUND_REMOVE); stopSelf() } } private fun notification(text: String): Notification { val channel = NotificationChannel(GaCyApplication.CHANNEL_ID, GaCyApplication.CHANNEL_NAME, NotificationManager.IMPORTANCE_LOW) getSystemService(NotificationManager::class.java).createNotificationChannel(channel) val pending = PendingIntent.getActivity(this, 0, Intent(this, MainActivity::class.java), PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT) return NotificationCompat.Builder(this, GaCyApplication.CHANNEL_ID).setContentTitle("GaCy Android").setContentText(text) .setSmallIcon(R.drawable.app_icon).setContentIntent(pending).setOngoing(true).build() } private fun startForegroundCompat(text: String) { val n = notification(text) if (Build.VERSION.SDK_INT >= 34) startForeground(NOTIFICATION_ID, n, ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE) else startForeground(NOTIFICATION_ID, n) } private fun updateNotification(text: String) { getSystemService(NotificationManager::class.java).notify(NOTIFICATION_ID, notification(text)) } override fun onDestroy() { ++generation job?.cancel() terminateProcesses() scope.cancel() if (_serverStatus.value != ServerStatus.ERROR) _serverStatus.value = ServerStatus.STOPPED super.onDestroy() } companion object { const val ACTION_START = "com.gacy.android.ACTION_START" const val ACTION_STOP = "com.gacy.android.ACTION_STOP" const val ACTION_RESTART = "com.gacy.android.ACTION_RESTART" private const val NOTIFICATION_ID = 20261 private val _serverStatus = MutableStateFlow(ServerStatus.STOPPED) val serverStatus: StateFlow = _serverStatus.asStateFlow() fun start(context: Context) { androidx.core.content.ContextCompat.startForegroundService(context, Intent(context, ServerService::class.java).setAction(ACTION_START)) } fun stop(context: Context) { context.startService(Intent(context, ServerService::class.java).setAction(ACTION_STOP)) } fun restart(context: Context) { androidx.core.content.ContextCompat.startForegroundService(context, Intent(context, ServerService::class.java).setAction(ACTION_RESTART)) } } }