feat: embed standalone local server runtime with Chaquopy Python, auto-installer, and Android 4.6.53 hotfix
This commit is contained in:
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user