fix(server): fix werkzeug metadata lookup crash & add update channel selector (Stable/Beta)

This commit is contained in:
CAtlas0784 committed 2026-10-10 15:47:45 +07:00
1 parent 2de9a13c89
commit 9ace7a2c16
6 files changed
+314 -51

No files matched your search

+2
View File
@@ -11,3 +11,5 @@ local.properties
/app/build
*.apk
__pycache__/
*.pyc
Binary file not shown.
Binary file not shown.
@@ -10,6 +10,7 @@ import androidx.core.content.FileProvider
import com.catlas.escapepenacony.BuildConfig
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.json.JSONArray
import org.json.JSONObject
import java.io.File
import java.io.FileOutputStream
@@ -21,40 +22,79 @@ data class AppReleaseInfo(
val releaseTitle: String,
val changelog: String,
val apkDownloadUrl: String,
val isNewer: Boolean
val isNewer: Boolean,
val isPrerelease: Boolean = false
)
object AppUpdateManager {
private val GITHUB_ENDPOINTS = listOf(
"https://api.github.com/repos/CAtlas0784/Escape-penacony-standalone/releases/latest",
"https://api.github.com/repos/CAtlas0784/Escape-penacony/releases/latest"
private val GITHUB_REPOS = listOf(
"CAtlas0784/Escape-penacony-standalone",
"CAtlas0784/Escape-penacony"
)
suspend fun checkForAppUpdate(currentVersion: String = BuildConfig.VERSION_NAME): Result<AppReleaseInfo?> =
withContext(Dispatchers.IO) {
for (endpoint in GITHUB_ENDPOINTS) {
try {
val url = URL(endpoint)
val conn = (url.openConnection() as HttpURLConnection).apply {
connectTimeout = 8000
readTimeout = 8000
setRequestProperty("Accept", "application/vnd.github.v3+json")
setRequestProperty("User-Agent", "Escape-Penacony-Android-App")
private const val PREFS_NAME = "app_updater_prefs"
private const val KEY_INCLUDE_PRERELEASE = "include_prerelease"
fun isIncludePrerelease(context: Context): Boolean {
return context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
.getBoolean(KEY_INCLUDE_PRERELEASE, true)
}
fun setIncludePrerelease(context: Context, value: Boolean) {
context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
.edit()
.putBoolean(KEY_INCLUDE_PRERELEASE, value)
.apply()
}
suspend fun checkForAppUpdate(
includePrerelease: Boolean,
currentVersion: String = BuildConfig.VERSION_NAME
): Result<AppReleaseInfo?> = withContext(Dispatchers.IO) {
for (repo in GITHUB_REPOS) {
try {
val url = URL("https://api.github.com/repos/$repo/releases")
val conn = (url.openConnection() as HttpURLConnection).apply {
connectTimeout = 8000
readTimeout = 8000
setRequestProperty("Accept", "application/vnd.github.v3+json")
setRequestProperty("User-Agent", "Escape-Penacony-Android-App")
}
if (conn.responseCode == 200) {
val body = conn.inputStream.bufferedReader().use { it.readText() }
val array = JSONArray(body)
if (array.length() == 0) continue
var chosenRelease: JSONObject? = null
for (i in 0 until array.length()) {
val rel = array.getJSONObject(i)
if (rel.optBoolean("draft", false)) continue
val isPre = rel.optBoolean("prerelease", false)
if (includePrerelease) {
chosenRelease = rel
break
} else {
if (!isPre) {
chosenRelease = rel
break
}
}
}
if (conn.responseCode == 200) {
val body = conn.inputStream.bufferedReader().use { it.readText() }
val json = JSONObject(body)
val tagName = json.getString("tag_name")
val title = json.optString("name", tagName)
val changelog = json.optString("body", "")
if (chosenRelease != null) {
val tagName = chosenRelease.getString("tag_name")
val title = chosenRelease.optString("name", tagName)
val changelog = chosenRelease.optString("body", "")
val isPre = chosenRelease.optBoolean("prerelease", false)
var downloadUrl = ""
val assets = json.optJSONArray("assets")
val assets = chosenRelease.optJSONArray("assets")
if (assets != null) {
for (i in 0 until assets.length()) {
val asset = assets.getJSONObject(i)
for (j in 0 until assets.length()) {
val asset = assets.getJSONObject(j)
val name = asset.optString("name", "")
if (name.endsWith(".apk", ignoreCase = true)) {
downloadUrl = asset.getString("browser_download_url")
@@ -70,16 +110,18 @@ object AppUpdateManager {
releaseTitle = title,
changelog = changelog,
apkDownloadUrl = downloadUrl,
isNewer = isNewer
isNewer = isNewer,
isPrerelease = isPre
)
)
}
} catch (e: Exception) {
// Try next endpoint
}
} catch (e: Exception) {
// Try next repo
}
Result.success(null)
}
Result.success(null)
}
suspend fun downloadApk(
context: Context,
@@ -148,16 +190,38 @@ object AppUpdateManager {
context.startActivity(installIntent)
}
private fun isNewerVersion(current: String, latest: String): Boolean {
val c = current.removePrefix("v").split(".").map { it.toIntOrNull() ?: 0 }
val l = latest.removePrefix("v").split(".").map { it.toIntOrNull() ?: 0 }
val maxLen = maxOf(c.size, l.size)
fun isNewerVersion(current: String, latest: String): Boolean {
if (current.equals(latest, ignoreCase = true)) return false
fun parseParts(ver: String): Pair<List<Int>, String> {
val clean = ver.trim().removePrefix("v").removePrefix("V")
val parts = clean.split("-", limit = 2)
val numbers = parts[0].split(".").map { it.toIntOrNull() ?: 0 }
val suffix = if (parts.size > 1) parts[1].lowercase() else ""
return Pair(numbers, suffix)
}
val (cNums, cSuffix) = parseParts(current)
val (lNums, lSuffix) = parseParts(latest)
val maxLen = maxOf(cNums.size, lNums.size)
for (i in 0 until maxLen) {
val cv = c.getOrElse(i) { 0 }
val lv = l.getOrElse(i) { 0 }
val cv = cNums.getOrElse(i) { 0 }
val lv = lNums.getOrElse(i) { 0 }
if (lv > cv) return true
if (lv < cv) return false
}
if (cSuffix.isNotBlank() && lSuffix.isBlank()) {
return true
}
if (cSuffix.isBlank() && lSuffix.isNotBlank()) {
return false
}
if (cSuffix.isNotBlank() && lSuffix.isNotBlank()) {
return lSuffix > cSuffix
}
return false
}
}
@@ -114,6 +114,7 @@ fun MainScreen() {
var installStatusMessage by remember { mutableStateOf("") }
// App Update States (APK)
var includePrerelease by remember { mutableStateOf(AppUpdateManager.isIncludePrerelease(context)) }
var appReleaseInfo by remember { mutableStateOf<AppReleaseInfo?>(null) }
var showAppUpdateDialog by remember { mutableStateOf(false) }
var isCheckingAppUpdate by remember { mutableStateOf(false) }
@@ -139,7 +140,7 @@ fun MainScreen() {
// Silent background check for APK updates on launch
scope.launch {
val res = AppUpdateManager.checkForAppUpdate()
val res = AppUpdateManager.checkForAppUpdate(includePrerelease)
res.onSuccess { info ->
if (info != null) {
appReleaseInfo = info
@@ -412,10 +413,23 @@ fun MainScreen() {
}
}
},
includePrerelease = includePrerelease,
onTogglePrerelease = { newVal ->
includePrerelease = newVal
AppUpdateManager.setIncludePrerelease(context, newVal)
scope.launch {
isCheckingAppUpdate = true
val res = AppUpdateManager.checkForAppUpdate(newVal)
isCheckingAppUpdate = false
res.onSuccess { info ->
appReleaseInfo = info
}
}
},
onCheckAppUpdate = {
scope.launch {
isCheckingAppUpdate = true
val res = AppUpdateManager.checkForAppUpdate()
val res = AppUpdateManager.checkForAppUpdate(includePrerelease)
isCheckingAppUpdate = false
res.onSuccess { info ->
appReleaseInfo = info
@@ -736,11 +750,41 @@ fun MainScreen() {
AlertDialog(
onDismissRequest = { if (!isDownloadingApk) showAppUpdateDialog = false },
title = {
Text(
tr(currentLanguage, "New App Update: ${release.tagName}", "พบอัปเดตแอปใหม่: ${release.tagName}", "发现新版本应用: ${release.tagName}"),
fontWeight = FontWeight.Bold,
color = Color.White
)
Row(verticalAlignment = Alignment.CenterVertically) {
Text(
tr(currentLanguage, "New App Update", "พบอัปเดตแอปใหม่", "发现新版本应用"),
fontWeight = FontWeight.Bold,
color = Color.White
)
Spacer(modifier = Modifier.width(8.dp))
if (release.isPrerelease) {
Surface(
shape = RoundedCornerShape(4.dp),
color = ExploreAmber.copy(alpha = 0.2f)
) {
Text(
text = "BETA",
color = ExploreAmber,
fontSize = 11.sp,
fontWeight = FontWeight.Bold,
modifier = Modifier.padding(horizontal = 6.dp, vertical = 2.dp)
)
}
} else {
Surface(
shape = RoundedCornerShape(4.dp),
color = CombatGreen.copy(alpha = 0.2f)
) {
Text(
text = "STABLE",
color = CombatGreen,
fontSize = 11.sp,
fontWeight = FontWeight.Bold,
modifier = Modifier.padding(horizontal = 6.dp, vertical = 2.dp)
)
}
}
}
},
text = {
Column {
@@ -1427,6 +1471,8 @@ fun UpdaterTab(
isCheckingApp: Boolean,
isDownloadingApk: Boolean,
apkProgress: Int,
includePrerelease: Boolean,
onTogglePrerelease: (Boolean) -> Unit,
onCheckUpdate: () -> Unit,
onOpenVersionSelector: () -> Unit,
onInstall: (String) -> Unit,
@@ -1463,11 +1509,63 @@ fun UpdaterTab(
color = Color.Gray
)
Spacer(modifier = Modifier.height(14.dp))
Spacer(modifier = Modifier.height(12.dp))
// Update Channel Selector (Stable vs Pre-release / Beta)
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalAlignment = Alignment.CenterVertically
) {
Text(
tr(currentLanguage, "Channel:", "ช่องทาง:", "渠道:"),
fontSize = 12.sp,
color = Color.Gray
)
FilterChip(
selected = !includePrerelease,
onClick = {
if (includePrerelease) {
onTogglePrerelease(false)
}
},
label = {
Text(
tr(currentLanguage, "Latest (Stable)", "เสถียร (ล่าสุด)", "稳定版 (最新)"),
fontSize = 11.sp
)
},
colors = FilterChipDefaults.filterChipColors(
selectedContainerColor = PurplePrimary,
selectedLabelColor = Color.White
)
)
FilterChip(
selected = includePrerelease,
onClick = {
if (!includePrerelease) {
onTogglePrerelease(true)
}
},
label = {
Text(
tr(currentLanguage, "Pre-release (Beta)", "ทดสอบ (เบต้า)", "预发布 (测试版)"),
fontSize = 11.sp
)
},
colors = FilterChipDefaults.filterChipColors(
selectedContainerColor = ExploreAmber,
selectedLabelColor = Color.Black
)
)
}
Spacer(modifier = Modifier.height(10.dp))
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Column {
Text(
@@ -1489,12 +1587,45 @@ fun UpdaterTab(
fontSize = 12.sp,
color = Color.Gray
)
Text(
text = appReleaseInfo?.tagName ?: tr(currentLanguage, "Tap check to query", "แตะตรวจหาเพื่อดูเวอร์ชัน", "点击检查获取"),
fontWeight = FontWeight.Bold,
fontSize = 15.sp,
color = PurplePrimary
)
Row(verticalAlignment = Alignment.CenterVertically) {
if (appReleaseInfo != null) {
if (appReleaseInfo.isPrerelease) {
Surface(
shape = RoundedCornerShape(4.dp),
color = ExploreAmber.copy(alpha = 0.2f),
modifier = Modifier.padding(end = 4.dp)
) {
Text(
text = "BETA",
color = ExploreAmber,
fontSize = 9.sp,
fontWeight = FontWeight.Bold,
modifier = Modifier.padding(horizontal = 4.dp, vertical = 1.dp)
)
}
} else {
Surface(
shape = RoundedCornerShape(4.dp),
color = CombatGreen.copy(alpha = 0.2f),
modifier = Modifier.padding(end = 4.dp)
) {
Text(
text = "STABLE",
color = CombatGreen,
fontSize = 9.sp,
fontWeight = FontWeight.Bold,
modifier = Modifier.padding(horizontal = 4.dp, vertical = 1.dp)
)
}
}
}
Text(
text = appReleaseInfo?.tagName ?: tr(currentLanguage, "Tap check to query", "แตะตรวจหาเพื่อดูเวอร์ชัน", "点击检查获取"),
fontWeight = FontWeight.Bold,
fontSize = 15.sp,
color = PurplePrimary
)
}
}
}
+69 -3
View File
@@ -8,12 +8,62 @@ import socket
import struct
import warnings
# === GLOBAL WARNING SUPPRESSION (very early) ===
# === GLOBAL WARNING SUPPRESSION & METADATA SHIM (very early) ===
try:
import importlib.metadata
_orig_metadata_version = importlib.metadata.version
_orig_metadata_distribution = importlib.metadata.distribution
_KNOWN_VERSIONS = {
"werkzeug": "3.1.9",
"flask": "3.1.3",
"marshmallow": "3.20.0",
"requests": "2.34.2",
"urllib3": "2.8.0",
"protobuf": "7.36.2",
"click": "8.5.0",
"jinja2": "3.1.6",
"itsdangerous": "2.2.0",
"blinker": "1.9.0",
"markupsafe": "3.0.4",
"dataclasses-json": "0.6.7",
"dataclasses_json": "0.6.7",
"packaging": "26.3"
}
class _ShimedDistribution:
def __init__(self, name, version):
self.name = name
self.version = version
self.metadata = {"Name": name, "Version": version}
def _safe_metadata_version(name):
try:
return _orig_metadata_version(name)
except Exception:
clean = name.lower().replace("-", "_")
return _KNOWN_VERSIONS.get(name.lower(), _KNOWN_VERSIONS.get(clean, "1.0.0"))
def _safe_metadata_distribution(name):
try:
return _orig_metadata_distribution(name)
except Exception:
ver = _safe_metadata_version(name)
return _ShimedDistribution(name, ver)
importlib.metadata.version = _safe_metadata_version
importlib.metadata.distribution = _safe_metadata_distribution
except Exception:
pass
warnings.filterwarnings("ignore", message=".*default.*deprecated.*")
warnings.filterwarnings("ignore", message=".*metadata.*deprecated.*")
warnings.filterwarnings("ignore", module="marshmallow.*")
warnings.filterwarnings("ignore", module="dataclasses_json.*")
try:
from marshmallow.warnings import RemovedInMarshmallow4Warning
warnings.filterwarnings("ignore", category=RemovedInMarshmallow4Warning)
warnings.filterwarnings("ignore", message=".*default.*deprecated.*")
warnings.filterwarnings("ignore", message=".*metadata.*deprecated.*")
warnings.simplefilter("ignore", category=RemovedInMarshmallow4Warning)
print("[Shim] Global warning suppression activated")
except Exception:
@@ -160,6 +210,11 @@ def run_server(ppsr_path, activity_context=None):
if os.path.exists(path) and path not in sys.path:
sys.path.insert(0, path)
warnings.filterwarnings("ignore", message=".*default.*deprecated.*")
warnings.filterwarnings("ignore", message=".*metadata.*deprecated.*")
warnings.filterwarnings("ignore", module="marshmallow.*")
warnings.filterwarnings("ignore", module="dataclasses_json.*")
# Verify and auto-recover critical packages if needed
for pkg_name in ["requests", "werkzeug", "flask"]:
try:
@@ -175,6 +230,12 @@ def run_server(ppsr_path, activity_context=None):
# --- Werkzeug Patch ---
try:
import werkzeug.serving
if hasattr(werkzeug.serving, "ThreadedWSGIServer"):
try:
werkzeug.serving.ThreadedWSGIServer._server_version = "Werkzeug/3.1.9"
except Exception:
pass
_orig_make_server = werkzeug.serving.make_server
def _patched_make_server(*args, **kwargs):
@@ -182,6 +243,11 @@ def run_server(ppsr_path, activity_context=None):
kwargs.pop('options', None)
server = _orig_make_server(*args, **kwargs)
try:
server._server_version = "Werkzeug/3.1.9"
except Exception:
pass
try:
if hasattr(server, 'socket') and server.socket:
server.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)