feat: embed standalone local server runtime with Chaquopy Python, auto-installer, and Android 4.6.53 hotfix

This commit is contained in:
CAtlas0784 committed 2026-11-01 13:58:07 +07:00
1 parent 17bcc1adbf
commit 1f88f6ea38
40 files changed
+6036 -32

No files matched your search

@@ -4,25 +4,43 @@ import android.app.Notification
import android.app.PendingIntent
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 androidx.lifecycle.LifecycleService
import androidx.lifecycle.lifecycleScope
import com.chaquo.python.Python
import com.gacy.android.GaCyApplication
import com.gacy.android.MainActivity
import com.gacy.android.R
import com.gacy.android.data.PersistentRepository
import com.gacy.android.data.PreferencesRepository
import com.gacy.android.data.UpdateRepository
import com.gacy.android.model.ServerExecutionMode
import com.gacy.android.model.ServerStatus
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import java.io.File
import java.net.InetSocketAddress
import java.net.Socket
class ServerLogReceiver {
fun onServerLog(msg: String) {
LogBuffer.append(msg)
}
}
class ServerService : LifecycleService() {
private var serverThread: Thread? = null
private var probeJob: Job? = null
private var serverJob: Job? = null
override fun onCreate() {
@@ -52,56 +70,150 @@ class ServerService : LifecycleService() {
val config = prefs.getServerConfig()
_serverStatus.value = ServerStatus.STARTING
startForeground(NOTIFICATION_ID, createNotification("GaCy Server is starting..."))
startForegroundCompat("GaCy Server is starting...")
LogBuffer.append("--- GaCy Server Starting ---")
LogBuffer.append("Mode: ${config.executionMode.displayNameEn}")
LogBuffer.append("Dispatch Port: ${config.dispatchPort} | Gameserver Port: ${config.gameserverPort}")
serverJob = lifecycleScope.launch {
try {
if (config.executionMode == ServerExecutionMode.LOCAL_EMBEDDED) {
LogBuffer.append("Initializing local server runtime...")
delay(500)
LogBuffer.append("Loading resources and protocol definitions...")
delay(500)
LogBuffer.append("Dispatch service listening on 0.0.0.0:${config.dispatchPort}")
LogBuffer.append("Gateway KCP socket bound on 0.0.0.0:${config.gameserverPort}")
_serverStatus.value = ServerStatus.RUNNING
updateNotification("GaCy Server is RUNNING on :${config.dispatchPort}")
LogBuffer.append("GaCy Server is ready! Connect client via dispatch redirect.")
} else {
LogBuffer.append("Connecting to remote GaCy host at ${config.remoteHost}...")
delay(500)
_serverStatus.value = ServerStatus.RUNNING
updateNotification("Monitoring Remote GaCy Host at ${config.remoteHost}")
LogBuffer.append("Connected to remote GaCy server monitor.")
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
}
} catch (e: Exception) {
LogBuffer.append("Error starting server: ${e.message}")
_serverStatus.value = ServerStatus.ERROR
updateNotification("GaCy Server encountered an error")
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}")
_serverStatus.value = ServerStatus.ERROR
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
}
}
}
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.")
}
}
private fun stopServer() {
LogBuffer.append("--- GaCy Server Stopping ---")
_serverStatus.value = ServerStatus.STOPPING
updateNotification("Stopping GaCy Server...")
probeJob?.cancel()
probeJob = null
serverJob?.cancel()
serverJob = null
_serverStatus.value = ServerStatus.STOPPED
stopForeground(STOP_FOREGROUND_REMOVE)
stopSelf()
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()
}
}
private fun restartServer() {
LogBuffer.append("Restarting GaCy Server...")
stopServer()
lifecycleScope.launch {
delay(800)
delay(1500)
startServer()
}
}
private fun isPortOpen(host: String, port: Int): Boolean {
return try {
Socket().use { socket ->
socket.connect(InetSocketAddress(host, port), 400)
true
}
} catch (_: Exception) {
false
}
}
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
@@ -133,14 +245,38 @@ class ServerService : LifecycleService() {
.build()
}
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)
}
}
private fun updateNotification(contentText: String) {
val manager = getSystemService(Context.NOTIFICATION_SERVICE) as android.app.NotificationManager
manager.notify(NOTIFICATION_ID, createNotification(contentText))
}
private fun stopForegroundCompat() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
stopForeground(STOP_FOREGROUND_REMOVE)
} else {
@Suppress("DEPRECATION")
stopForeground(true)
}
}
override fun onDestroy() {
super.onDestroy()
probeJob?.cancel()
serverThread?.interrupt()
serverJob?.cancel()
super.onDestroy()
_serverStatus.value = ServerStatus.STOPPED
}
@@ -160,7 +296,11 @@ class ServerService : LifecycleService() {
fun start(context: Context) {
val intent = Intent(context, ServerService::class.java).apply { action = ACTION_START }
context.startForegroundService(intent)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
context.startForegroundService(intent)
} else {
context.startService(intent)
}
}
fun stop(context: Context) {
@@ -170,7 +310,11 @@ class ServerService : LifecycleService() {
fun restart(context: Context) {
val intent = Intent(context, ServerService::class.java).apply { action = ACTION_RESTART }
context.startForegroundService(intent)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
context.startForegroundService(intent)
} else {
context.startService(intent)
}
}
}
}