2026-10-31 17:36:42 +07:00
|
|
|
package com.gacy.android.service
|
|
|
|
|
|
|
|
|
|
import android.app.Notification
|
|
|
|
|
import android.app.PendingIntent
|
|
|
|
|
import android.content.Context
|
|
|
|
|
import android.content.Intent
|
2026-11-01 13:58:07 +07:00
|
|
|
import android.content.pm.ServiceInfo
|
|
|
|
|
import android.os.Build
|
2026-10-31 17:36:42 +07:00
|
|
|
import android.os.IBinder
|
|
|
|
|
import androidx.core.app.NotificationCompat
|
|
|
|
|
import androidx.lifecycle.LifecycleService
|
|
|
|
|
import androidx.lifecycle.lifecycleScope
|
2026-11-01 13:58:07 +07:00
|
|
|
import com.chaquo.python.Python
|
2026-10-31 17:36:42 +07:00
|
|
|
import com.gacy.android.GaCyApplication
|
|
|
|
|
import com.gacy.android.MainActivity
|
|
|
|
|
import com.gacy.android.R
|
2026-11-01 13:58:07 +07:00
|
|
|
import com.gacy.android.data.PersistentRepository
|
2026-10-31 17:36:42 +07:00
|
|
|
import com.gacy.android.data.PreferencesRepository
|
2026-11-01 13:58:07 +07:00
|
|
|
import com.gacy.android.data.UpdateRepository
|
2026-10-31 17:36:42 +07:00
|
|
|
import com.gacy.android.model.ServerExecutionMode
|
|
|
|
|
import com.gacy.android.model.ServerStatus
|
2026-11-01 13:58:07 +07:00
|
|
|
import kotlinx.coroutines.Dispatchers
|
2026-10-31 17:36:42 +07:00
|
|
|
import kotlinx.coroutines.Job
|
|
|
|
|
import kotlinx.coroutines.delay
|
|
|
|
|
import kotlinx.coroutines.flow.MutableStateFlow
|
|
|
|
|
import kotlinx.coroutines.flow.StateFlow
|
|
|
|
|
import kotlinx.coroutines.flow.asStateFlow
|
2026-11-01 13:58:07 +07:00
|
|
|
import kotlinx.coroutines.isActive
|
2026-10-31 17:36:42 +07:00
|
|
|
import kotlinx.coroutines.launch
|
2026-11-01 13:58:07 +07:00
|
|
|
import java.io.File
|
|
|
|
|
import java.net.InetSocketAddress
|
|
|
|
|
import java.net.Socket
|
|
|
|
|
|
|
|
|
|
class ServerLogReceiver {
|
|
|
|
|
fun onServerLog(msg: String) {
|
|
|
|
|
LogBuffer.append(msg)
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-10-31 17:36:42 +07:00
|
|
|
|
|
|
|
|
class ServerService : LifecycleService() {
|
|
|
|
|
|
2026-11-01 13:58:07 +07:00
|
|
|
private var serverThread: Thread? = null
|
|
|
|
|
private var probeJob: Job? = null
|
2026-10-31 17:36:42 +07:00
|
|
|
private var serverJob: Job? = null
|
|
|
|
|
|
|
|
|
|
override fun onCreate() {
|
|
|
|
|
super.onCreate()
|
|
|
|
|
_serverStatus.value = ServerStatus.STOPPED
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
|
|
|
|
super.onStartCommand(intent, flags, startId)
|
|
|
|
|
val action = intent?.action ?: ACTION_START
|
|
|
|
|
|
|
|
|
|
when (action) {
|
|
|
|
|
ACTION_START -> startServer()
|
|
|
|
|
ACTION_STOP -> stopServer()
|
|
|
|
|
ACTION_RESTART -> restartServer()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return START_NOT_STICKY
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private fun startServer() {
|
|
|
|
|
if (_serverStatus.value == ServerStatus.RUNNING || _serverStatus.value == ServerStatus.STARTING) {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
val prefs = PreferencesRepository(this)
|
|
|
|
|
val config = prefs.getServerConfig()
|
|
|
|
|
|
|
|
|
|
_serverStatus.value = ServerStatus.STARTING
|
2026-11-01 13:58:07 +07:00
|
|
|
startForegroundCompat("GaCy Server is starting...")
|
2026-10-31 17:36:42 +07:00
|
|
|
LogBuffer.append("--- GaCy Server Starting ---")
|
|
|
|
|
LogBuffer.append("Mode: ${config.executionMode.displayNameEn}")
|
|
|
|
|
LogBuffer.append("Dispatch Port: ${config.dispatchPort} | Gameserver Port: ${config.gameserverPort}")
|
|
|
|
|
|
2026-11-01 13:58:07 +07:00
|
|
|
if (config.executionMode == ServerExecutionMode.LOCAL_EMBEDDED) {
|
|
|
|
|
startLocalServerExecution(config.dispatchPort)
|
|
|
|
|
} else {
|
|
|
|
|
startRemoteMonitor(config.remoteHost, config.dispatchPort)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private fun startLocalServerExecution(dispatchPort: Int) {
|
|
|
|
|
serverJob = lifecycleScope.launch(Dispatchers.IO) {
|
|
|
|
|
val updateRepo = UpdateRepository(this@ServerService)
|
|
|
|
|
if (!updateRepo.isServerInstalled()) {
|
|
|
|
|
LogBuffer.append("[Setup] Installing GaCy embedded server binaries from assets...")
|
|
|
|
|
val success = updateRepo.ensureServerInstalled()
|
|
|
|
|
if (!success) {
|
|
|
|
|
LogBuffer.append("[Error] Failed to install server binaries from assets.")
|
|
|
|
|
_serverStatus.value = ServerStatus.ERROR
|
|
|
|
|
stopForegroundCompat()
|
|
|
|
|
stopSelf()
|
|
|
|
|
return@launch
|
2026-10-31 17:36:42 +07:00
|
|
|
}
|
2026-11-01 13:58:07 +07:00
|
|
|
LogBuffer.append("[Setup] Server binaries installed successfully.")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
val ppsrFile = File(filesDir, "server/build/build/ppsr.pyc")
|
|
|
|
|
if (!ppsrFile.exists()) {
|
|
|
|
|
LogBuffer.append("[Error] Server bytecode missing at: ${ppsrFile.absolutePath}")
|
2026-10-31 17:36:42 +07:00
|
|
|
_serverStatus.value = ServerStatus.ERROR
|
2026-11-01 13:58:07 +07:00
|
|
|
stopForegroundCompat()
|
|
|
|
|
stopSelf()
|
|
|
|
|
return@launch
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
val persistentRepo = PersistentRepository(this@ServerService)
|
|
|
|
|
persistentRepo.ensureValidPersistentJsonSync()
|
|
|
|
|
} catch (e: Exception) {
|
|
|
|
|
LogBuffer.append("[Warning] Persistent validation: ${e.message}")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
LogBuffer.append("[System] Launching embedded GaCy Python server...")
|
|
|
|
|
|
|
|
|
|
// Background TCP port readiness probe
|
|
|
|
|
probeJob?.cancel()
|
|
|
|
|
probeJob = lifecycleScope.launch(Dispatchers.IO) {
|
|
|
|
|
var attempts = 0
|
|
|
|
|
while (isActive && _serverStatus.value == ServerStatus.STARTING && attempts < 35) {
|
|
|
|
|
delay(1000)
|
|
|
|
|
attempts++
|
|
|
|
|
if (isPortOpen("127.0.0.1", dispatchPort)) {
|
|
|
|
|
_serverStatus.value = ServerStatus.RUNNING
|
|
|
|
|
updateNotification("GaCy Server is RUNNING on :$dispatchPort")
|
|
|
|
|
LogBuffer.append("[System] GaCy Dispatch server is ready on 127.0.0.1:$dispatchPort")
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-10-31 17:36:42 +07:00
|
|
|
}
|
2026-11-01 13:58:07 +07:00
|
|
|
|
|
|
|
|
serverThread = Thread({
|
|
|
|
|
try {
|
|
|
|
|
val py = Python.getInstance()
|
|
|
|
|
val runPpsr = py.getModule("run_ppsr")
|
|
|
|
|
val logReceiver = ServerLogReceiver()
|
|
|
|
|
runPpsr.callAttr("run_server", ppsrFile.absolutePath, logReceiver)
|
|
|
|
|
} catch (e: Exception) {
|
|
|
|
|
LogBuffer.append("[Python Error] ${e.localizedMessage ?: e.message}")
|
|
|
|
|
} finally {
|
|
|
|
|
probeJob?.cancel()
|
|
|
|
|
_serverStatus.value = ServerStatus.STOPPED
|
|
|
|
|
LogBuffer.append("[System] Server stopped.")
|
|
|
|
|
stopForegroundCompat()
|
|
|
|
|
stopSelf()
|
|
|
|
|
}
|
|
|
|
|
}, "GaCy-PythonServer").apply {
|
|
|
|
|
start()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private fun startRemoteMonitor(remoteHost: String, dispatchPort: Int) {
|
|
|
|
|
serverJob = lifecycleScope.launch(Dispatchers.IO) {
|
|
|
|
|
LogBuffer.append("[Remote] Connecting to remote GaCy host at $remoteHost:$dispatchPort...")
|
|
|
|
|
delay(600)
|
|
|
|
|
_serverStatus.value = ServerStatus.RUNNING
|
|
|
|
|
updateNotification("Monitoring Remote GaCy Host at $remoteHost")
|
|
|
|
|
LogBuffer.append("[Remote] Connected to remote GaCy server monitor.")
|
2026-10-31 17:36:42 +07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private fun stopServer() {
|
|
|
|
|
LogBuffer.append("--- GaCy Server Stopping ---")
|
2026-11-01 13:58:07 +07:00
|
|
|
_serverStatus.value = ServerStatus.STOPPING
|
|
|
|
|
updateNotification("Stopping GaCy Server...")
|
|
|
|
|
|
|
|
|
|
probeJob?.cancel()
|
|
|
|
|
probeJob = null
|
|
|
|
|
|
2026-10-31 17:36:42 +07:00
|
|
|
serverJob?.cancel()
|
|
|
|
|
serverJob = null
|
2026-11-01 13:58:07 +07:00
|
|
|
|
|
|
|
|
lifecycleScope.launch(Dispatchers.IO) {
|
|
|
|
|
try {
|
|
|
|
|
if (Python.isStarted()) {
|
|
|
|
|
val py = Python.getInstance()
|
|
|
|
|
val runPpsr = py.getModule("run_ppsr")
|
|
|
|
|
runPpsr.callAttr("kill_server_process")
|
|
|
|
|
}
|
|
|
|
|
} catch (e: Exception) {
|
|
|
|
|
LogBuffer.append("[Stop Error] ${e.localizedMessage ?: e.message}")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
delay(1200)
|
|
|
|
|
serverThread?.interrupt()
|
|
|
|
|
serverThread = null
|
|
|
|
|
_serverStatus.value = ServerStatus.STOPPED
|
|
|
|
|
stopForegroundCompat()
|
|
|
|
|
stopSelf()
|
|
|
|
|
}
|
2026-10-31 17:36:42 +07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private fun restartServer() {
|
|
|
|
|
LogBuffer.append("Restarting GaCy Server...")
|
|
|
|
|
stopServer()
|
|
|
|
|
lifecycleScope.launch {
|
2026-11-01 13:58:07 +07:00
|
|
|
delay(1500)
|
2026-10-31 17:36:42 +07:00
|
|
|
startServer()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-11-01 13:58:07 +07:00
|
|
|
private fun isPortOpen(host: String, port: Int): Boolean {
|
|
|
|
|
return try {
|
|
|
|
|
Socket().use { socket ->
|
|
|
|
|
socket.connect(InetSocketAddress(host, port), 400)
|
|
|
|
|
true
|
|
|
|
|
}
|
|
|
|
|
} catch (_: Exception) {
|
|
|
|
|
false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-31 17:36:42 +07:00
|
|
|
private fun createNotification(contentText: String): Notification {
|
|
|
|
|
val launchIntent = Intent(this, MainActivity::class.java).apply {
|
|
|
|
|
flags = Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP
|
|
|
|
|
}
|
|
|
|
|
val pendingIntent = PendingIntent.getActivity(
|
|
|
|
|
this,
|
|
|
|
|
0,
|
|
|
|
|
launchIntent,
|
|
|
|
|
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
val stopIntent = Intent(this, ServerService::class.java).apply {
|
|
|
|
|
action = ACTION_STOP
|
|
|
|
|
}
|
|
|
|
|
val stopPendingIntent = PendingIntent.getService(
|
|
|
|
|
this,
|
|
|
|
|
1,
|
|
|
|
|
stopIntent,
|
|
|
|
|
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
return NotificationCompat.Builder(this, GaCyApplication.CHANNEL_ID)
|
|
|
|
|
.setContentTitle("GaCy Android")
|
|
|
|
|
.setContentText(contentText)
|
|
|
|
|
.setSmallIcon(R.mipmap.ic_launcher)
|
|
|
|
|
.setOngoing(true)
|
|
|
|
|
.setContentIntent(pendingIntent)
|
|
|
|
|
.addAction(android.R.drawable.ic_menu_close_clear_cancel, "Stop", stopPendingIntent)
|
|
|
|
|
.build()
|
|
|
|
|
}
|
|
|
|
|
|
2026-11-01 13:58:07 +07:00
|
|
|
private fun startForegroundCompat(text: String) {
|
|
|
|
|
val notification = createNotification(text)
|
|
|
|
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
|
|
|
|
startForeground(
|
|
|
|
|
NOTIFICATION_ID,
|
|
|
|
|
notification,
|
|
|
|
|
ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE
|
|
|
|
|
)
|
|
|
|
|
} else {
|
|
|
|
|
startForeground(NOTIFICATION_ID, notification)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-31 17:36:42 +07:00
|
|
|
private fun updateNotification(contentText: String) {
|
|
|
|
|
val manager = getSystemService(Context.NOTIFICATION_SERVICE) as android.app.NotificationManager
|
|
|
|
|
manager.notify(NOTIFICATION_ID, createNotification(contentText))
|
|
|
|
|
}
|
|
|
|
|
|
2026-11-01 13:58:07 +07:00
|
|
|
private fun stopForegroundCompat() {
|
|
|
|
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
|
|
|
|
stopForeground(STOP_FOREGROUND_REMOVE)
|
|
|
|
|
} else {
|
|
|
|
|
@Suppress("DEPRECATION")
|
|
|
|
|
stopForeground(true)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-31 17:36:42 +07:00
|
|
|
override fun onDestroy() {
|
2026-11-01 13:58:07 +07:00
|
|
|
probeJob?.cancel()
|
|
|
|
|
serverThread?.interrupt()
|
2026-10-31 17:36:42 +07:00
|
|
|
serverJob?.cancel()
|
2026-11-01 13:58:07 +07:00
|
|
|
super.onDestroy()
|
2026-10-31 17:36:42 +07:00
|
|
|
_serverStatus.value = ServerStatus.STOPPED
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
override fun onBind(intent: Intent): IBinder? {
|
|
|
|
|
super.onBind(intent)
|
|
|
|
|
return null
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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> = _serverStatus.asStateFlow()
|
|
|
|
|
|
|
|
|
|
fun start(context: Context) {
|
|
|
|
|
val intent = Intent(context, ServerService::class.java).apply { action = ACTION_START }
|
2026-11-01 13:58:07 +07:00
|
|
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
|
|
|
context.startForegroundService(intent)
|
|
|
|
|
} else {
|
|
|
|
|
context.startService(intent)
|
|
|
|
|
}
|
2026-10-31 17:36:42 +07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fun stop(context: Context) {
|
|
|
|
|
val intent = Intent(context, ServerService::class.java).apply { action = ACTION_STOP }
|
|
|
|
|
context.startService(intent)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fun restart(context: Context) {
|
|
|
|
|
val intent = Intent(context, ServerService::class.java).apply { action = ACTION_RESTART }
|
2026-11-01 13:58:07 +07:00
|
|
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
|
|
|
context.startForegroundService(intent)
|
|
|
|
|
} else {
|
|
|
|
|
context.startService(intent)
|
|
|
|
|
}
|
2026-10-31 17:36:42 +07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|