fix(server): fix werkzeug metadata lookup crash & add update channel selector (Stable/Beta)
This commit is contained in:
1 parent
2de9a13c89
commit
9ace7a2c16
6 files changed
+314
-51
No files matched your search
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in new issue
Block a user