feat(android): harden update checks, ci, and direct-only ui
This commit is contained in:
parent
934eb345a2
commit
68a378bad9
|
|
@ -16,6 +16,9 @@ on:
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
|
|
||||||
|
env:
|
||||||
|
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build-windows:
|
build-windows:
|
||||||
runs-on: windows-latest
|
runs-on: windows-latest
|
||||||
|
|
@ -329,7 +332,7 @@ jobs:
|
||||||
working-directory: android
|
working-directory: android
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
- name: Validate Android release signing secrets
|
- name: Validate Android release signing secrets
|
||||||
env:
|
env:
|
||||||
|
|
@ -349,9 +352,14 @@ jobs:
|
||||||
distribution: temurin
|
distribution: temurin
|
||||||
java-version: "17"
|
java-version: "17"
|
||||||
cache: gradle
|
cache: gradle
|
||||||
|
cache-dependency-path: |
|
||||||
|
android/settings.gradle.kts
|
||||||
|
android/build.gradle.kts
|
||||||
|
android/gradle.properties
|
||||||
|
android/app/build.gradle.kts
|
||||||
|
|
||||||
- name: Set up Python 3.12
|
- name: Set up Python 3.12
|
||||||
uses: actions/setup-python@v5
|
uses: actions/setup-python@v6
|
||||||
with:
|
with:
|
||||||
python-version: "3.12"
|
python-version: "3.12"
|
||||||
|
|
||||||
|
|
@ -390,13 +398,19 @@ jobs:
|
||||||
cp app/build/outputs/apk/legacy32/release/app-legacy32-release.apk \
|
cp app/build/outputs/apk/legacy32/release/app-legacy32-release.apk \
|
||||||
"app/build/outputs/apk/legacy32/release/$ANDROID_APK_LEGACY32_NAME"
|
"app/build/outputs/apk/legacy32/release/$ANDROID_APK_LEGACY32_NAME"
|
||||||
|
|
||||||
|
- name: Stage Android release artifacts
|
||||||
|
run: |
|
||||||
|
mkdir -p dist
|
||||||
|
cp "app/build/outputs/apk/standard/release/$ANDROID_APK_STANDARD_NAME" "dist/$ANDROID_APK_STANDARD_NAME"
|
||||||
|
cp "app/build/outputs/apk/legacy32/release/$ANDROID_APK_LEGACY32_NAME" "dist/$ANDROID_APK_LEGACY32_NAME"
|
||||||
|
|
||||||
- name: Upload artifact
|
- name: Upload artifact
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v7
|
||||||
with:
|
with:
|
||||||
name: TgWsProxy-android-release
|
name: TgWsProxy-android-release
|
||||||
path: |
|
path: |
|
||||||
android/app/build/outputs/apk/standard/release/${{ env.ANDROID_APK_STANDARD_NAME }}
|
android/dist/${{ env.ANDROID_APK_STANDARD_NAME }}
|
||||||
android/app/build/outputs/apk/legacy32/release/${{ env.ANDROID_APK_LEGACY32_NAME }}
|
android/dist/${{ env.ANDROID_APK_LEGACY32_NAME }}
|
||||||
|
|
||||||
release:
|
release:
|
||||||
needs: [build-windows, build-win7, build-macos, build-linux, build-android]
|
needs: [build-windows, build-win7, build-macos, build-linux, build-android]
|
||||||
|
|
@ -410,7 +424,7 @@ jobs:
|
||||||
merge-multiple: true
|
merge-multiple: true
|
||||||
|
|
||||||
- name: Download Android build
|
- name: Download Android build
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v8
|
||||||
with:
|
with:
|
||||||
name: TgWsProxy-android-release
|
name: TgWsProxy-android-release
|
||||||
path: dist
|
path: dist
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,14 @@ plugins {
|
||||||
id("org.jetbrains.kotlin.android")
|
id("org.jetbrains.kotlin.android")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun loadProxyVersionName(): String {
|
||||||
|
val versionFile = rootProject.projectDir.resolve("../proxy/__init__.py")
|
||||||
|
val match = Regex("""__version__\s*=\s*"([^"]+)"""")
|
||||||
|
.find(versionFile.readText())
|
||||||
|
?: throw GradleException("Failed to parse proxy version from ${versionFile.absolutePath}")
|
||||||
|
return match.groupValues[1]
|
||||||
|
}
|
||||||
|
|
||||||
data class ReleaseSigningEnv(
|
data class ReleaseSigningEnv(
|
||||||
val keystoreFile: File,
|
val keystoreFile: File,
|
||||||
val storePassword: String,
|
val storePassword: String,
|
||||||
|
|
@ -51,12 +59,16 @@ val stagePythonSources by tasks.registering(Sync::class) {
|
||||||
from(rootProject.projectDir.resolve("../proxy")) {
|
from(rootProject.projectDir.resolve("../proxy")) {
|
||||||
into("proxy")
|
into("proxy")
|
||||||
}
|
}
|
||||||
|
from(rootProject.projectDir.resolve("../utils")) {
|
||||||
|
into("utils")
|
||||||
|
}
|
||||||
into(stagedPythonSourcesDir)
|
into(stagedPythonSourcesDir)
|
||||||
}
|
}
|
||||||
val releaseSigningRequested = gradle.startParameter.taskNames.any {
|
val releaseSigningRequested = gradle.startParameter.taskNames.any {
|
||||||
it.contains("release", ignoreCase = true)
|
it.contains("release", ignoreCase = true)
|
||||||
}
|
}
|
||||||
val releaseSigningEnv = loadReleaseSigningEnv(releaseSigningRequested)
|
val releaseSigningEnv = loadReleaseSigningEnv(releaseSigningRequested)
|
||||||
|
val appVersionName = loadProxyVersionName()
|
||||||
|
|
||||||
android {
|
android {
|
||||||
namespace = "org.flowseal.tgwsproxy"
|
namespace = "org.flowseal.tgwsproxy"
|
||||||
|
|
@ -67,7 +79,7 @@ android {
|
||||||
minSdk = 26
|
minSdk = 26
|
||||||
targetSdk = 34
|
targetSdk = 34
|
||||||
versionCode = 1
|
versionCode = 1
|
||||||
versionName = "0.1.0"
|
versionName = appVersionName
|
||||||
|
|
||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -63,7 +63,12 @@ class MainActivity : AppCompatActivity() {
|
||||||
|
|
||||||
val config = settingsStore.load()
|
val config = settingsStore.load()
|
||||||
renderConfig(config)
|
renderConfig(config)
|
||||||
refreshUpdateStatus(checkNow = config.checkUpdates)
|
if (config.checkUpdates) {
|
||||||
|
refreshUpdateStatus(checkNow = true)
|
||||||
|
} else {
|
||||||
|
currentUpdateStatus = null
|
||||||
|
renderUpdateStatus(null, false)
|
||||||
|
}
|
||||||
requestNotificationPermissionIfNeeded()
|
requestNotificationPermissionIfNeeded()
|
||||||
observeServiceState()
|
observeServiceState()
|
||||||
renderSystemStatus()
|
renderSystemStatus()
|
||||||
|
|
@ -88,7 +93,12 @@ class MainActivity : AppCompatActivity() {
|
||||||
if (showMessage) {
|
if (showMessage) {
|
||||||
Snackbar.make(binding.root, R.string.settings_saved, Snackbar.LENGTH_SHORT).show()
|
Snackbar.make(binding.root, R.string.settings_saved, Snackbar.LENGTH_SHORT).show()
|
||||||
}
|
}
|
||||||
refreshUpdateStatus(checkNow = config.checkUpdates)
|
if (config.checkUpdates) {
|
||||||
|
refreshUpdateStatus(checkNow = true)
|
||||||
|
} else {
|
||||||
|
currentUpdateStatus = null
|
||||||
|
renderUpdateStatus(null, false)
|
||||||
|
}
|
||||||
return config
|
return config
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -141,7 +151,7 @@ class MainActivity : AppCompatActivity() {
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onOpenReleasePageClicked() {
|
private fun onOpenReleasePageClicked() {
|
||||||
val url = currentUpdateStatus?.htmlUrl ?: "https://github.com/Dark-Avery/tg-ws-proxy/releases/latest"
|
val url = currentUpdateStatus?.htmlUrl ?: "https://github.com/Flowseal/tg-ws-proxy/releases/latest"
|
||||||
val opened = runCatching {
|
val opened = runCatching {
|
||||||
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(url)))
|
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(url)))
|
||||||
}.isSuccess
|
}.isSuccess
|
||||||
|
|
@ -152,16 +162,23 @@ class MainActivity : AppCompatActivity() {
|
||||||
|
|
||||||
private fun refreshUpdateStatus(checkNow: Boolean) {
|
private fun refreshUpdateStatus(checkNow: Boolean) {
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
val status = withContext(Dispatchers.IO) {
|
val status = runCatching {
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
PythonProxyBridge.getUpdateStatus(this@MainActivity, checkNow)
|
PythonProxyBridge.getUpdateStatus(this@MainActivity, checkNow)
|
||||||
}
|
}
|
||||||
|
}.getOrElse { exc ->
|
||||||
|
ProxyUpdateStatus(
|
||||||
|
currentVersion = "unknown",
|
||||||
|
error = exc.message ?: exc.javaClass.simpleName,
|
||||||
|
)
|
||||||
|
}
|
||||||
currentUpdateStatus = status
|
currentUpdateStatus = status
|
||||||
renderUpdateStatus(status, binding.checkUpdatesSwitch.isChecked)
|
renderUpdateStatus(status, binding.checkUpdatesSwitch.isChecked)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun renderUpdateStatus(status: ProxyUpdateStatus?, checkUpdatesEnabled: Boolean) {
|
private fun renderUpdateStatus(status: ProxyUpdateStatus?, checkUpdatesEnabled: Boolean) {
|
||||||
val currentVersion = status?.currentVersion ?: "unknown"
|
val currentVersion = status?.currentVersion?.takeIf { it.isNotBlank() } ?: currentAppVersionName()
|
||||||
binding.currentVersionValue.text = getString(
|
binding.currentVersionValue.text = getString(
|
||||||
R.string.updates_current_version_format,
|
R.string.updates_current_version_format,
|
||||||
currentVersion,
|
currentVersion,
|
||||||
|
|
@ -176,6 +193,9 @@ class MainActivity : AppCompatActivity() {
|
||||||
!status.error.isNullOrBlank() -> {
|
!status.error.isNullOrBlank() -> {
|
||||||
getString(R.string.updates_status_error, status.error)
|
getString(R.string.updates_status_error, status.error)
|
||||||
}
|
}
|
||||||
|
!status.checked -> {
|
||||||
|
getString(R.string.updates_status_idle)
|
||||||
|
}
|
||||||
status.hasUpdate && !status.latestVersion.isNullOrBlank() -> {
|
status.hasUpdate && !status.latestVersion.isNullOrBlank() -> {
|
||||||
getString(
|
getString(
|
||||||
R.string.updates_status_available,
|
R.string.updates_status_available,
|
||||||
|
|
@ -192,6 +212,13 @@ class MainActivity : AppCompatActivity() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun currentAppVersionName(): String {
|
||||||
|
return runCatching {
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
packageManager.getPackageInfo(packageName, 0).versionName
|
||||||
|
}.getOrNull().orEmpty().ifBlank { "unknown" }
|
||||||
|
}
|
||||||
|
|
||||||
private fun observeServiceState() {
|
private fun observeServiceState() {
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
repeatOnLifecycle(Lifecycle.State.STARTED) {
|
repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ data class ProxyConfig(
|
||||||
val logMaxMbText: String = formatDecimal(DEFAULT_LOG_MAX_MB),
|
val logMaxMbText: String = formatDecimal(DEFAULT_LOG_MAX_MB),
|
||||||
val bufferKbText: String = DEFAULT_BUFFER_KB.toString(),
|
val bufferKbText: String = DEFAULT_BUFFER_KB.toString(),
|
||||||
val poolSizeText: String = DEFAULT_POOL_SIZE.toString(),
|
val poolSizeText: String = DEFAULT_POOL_SIZE.toString(),
|
||||||
val checkUpdates: Boolean = true,
|
val checkUpdates: Boolean = false,
|
||||||
val verbose: Boolean = false,
|
val verbose: Boolean = false,
|
||||||
) {
|
) {
|
||||||
fun validate(): ValidationResult {
|
fun validate(): ValidationResult {
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@ class ProxySettingsStore(context: Context) {
|
||||||
KEY_POOL_SIZE,
|
KEY_POOL_SIZE,
|
||||||
ProxyConfig.DEFAULT_POOL_SIZE,
|
ProxyConfig.DEFAULT_POOL_SIZE,
|
||||||
).toString(),
|
).toString(),
|
||||||
checkUpdates = preferences.getBoolean(KEY_CHECK_UPDATES, true),
|
checkUpdates = preferences.getBoolean(KEY_CHECK_UPDATES, false),
|
||||||
verbose = preferences.getBoolean(KEY_VERBOSE, false),
|
verbose = preferences.getBoolean(KEY_VERBOSE, false),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import org.json.JSONObject
|
||||||
|
|
||||||
object PythonProxyBridge {
|
object PythonProxyBridge {
|
||||||
private const val MODULE_NAME = "android_proxy_bridge"
|
private const val MODULE_NAME = "android_proxy_bridge"
|
||||||
|
private val pythonStartLock = Any()
|
||||||
|
|
||||||
fun start(context: Context, config: NormalizedProxyConfig): String {
|
fun start(context: Context, config: NormalizedProxyConfig): String {
|
||||||
val module = getModule(context)
|
val module = getModule(context)
|
||||||
|
|
@ -54,6 +55,7 @@ object PythonProxyBridge {
|
||||||
latestVersion = json.optString("latest").ifBlank { null },
|
latestVersion = json.optString("latest").ifBlank { null },
|
||||||
hasUpdate = json.optBoolean("has_update", false),
|
hasUpdate = json.optBoolean("has_update", false),
|
||||||
aheadOfRelease = json.optBoolean("ahead_of_release", false),
|
aheadOfRelease = json.optBoolean("ahead_of_release", false),
|
||||||
|
checked = json.optBoolean("checked", false),
|
||||||
htmlUrl = json.optString("html_url").ifBlank { null },
|
htmlUrl = json.optString("html_url").ifBlank { null },
|
||||||
error = json.optString("error").ifBlank { null },
|
error = json.optString("error").ifBlank { null },
|
||||||
)
|
)
|
||||||
|
|
@ -63,8 +65,19 @@ object PythonProxyBridge {
|
||||||
getPython(context.applicationContext).getModule(MODULE_NAME)
|
getPython(context.applicationContext).getModule(MODULE_NAME)
|
||||||
|
|
||||||
private fun getPython(context: Context): Python {
|
private fun getPython(context: Context): Python {
|
||||||
|
if (Python.isStarted()) {
|
||||||
|
return Python.getInstance()
|
||||||
|
}
|
||||||
|
synchronized(pythonStartLock) {
|
||||||
if (!Python.isStarted()) {
|
if (!Python.isStarted()) {
|
||||||
|
try {
|
||||||
Python.start(AndroidPlatform(context))
|
Python.start(AndroidPlatform(context))
|
||||||
|
} catch (exc: IllegalStateException) {
|
||||||
|
if (!Python.isStarted()) {
|
||||||
|
throw exc
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return Python.getInstance()
|
return Python.getInstance()
|
||||||
}
|
}
|
||||||
|
|
@ -82,6 +95,7 @@ data class ProxyUpdateStatus(
|
||||||
val latestVersion: String? = null,
|
val latestVersion: String? = null,
|
||||||
val hasUpdate: Boolean = false,
|
val hasUpdate: Boolean = false,
|
||||||
val aheadOfRelease: Boolean = false,
|
val aheadOfRelease: Boolean = false,
|
||||||
|
val checked: Boolean = false,
|
||||||
val htmlUrl: String? = null,
|
val htmlUrl: String? = null,
|
||||||
val error: String? = null,
|
val error: String? = null,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,9 @@ from typing import Iterable, Optional
|
||||||
from proxy.app_runtime import ProxyAppRuntime
|
from proxy.app_runtime import ProxyAppRuntime
|
||||||
from proxy import __version__
|
from proxy import __version__
|
||||||
import proxy.tg_ws_proxy as tg_ws_proxy
|
import proxy.tg_ws_proxy as tg_ws_proxy
|
||||||
from utils.update_check import RELEASES_PAGE_URL, get_status, run_check
|
|
||||||
|
|
||||||
|
RELEASES_PAGE_URL = "https://github.com/Flowseal/tg-ws-proxy/releases/latest"
|
||||||
|
|
||||||
|
|
||||||
_RUNTIME_LOCK = threading.RLock()
|
_RUNTIME_LOCK = threading.RLock()
|
||||||
|
|
@ -129,21 +131,30 @@ def get_runtime_stats_json() -> str:
|
||||||
return json.dumps(payload)
|
return json.dumps(payload)
|
||||||
|
|
||||||
|
|
||||||
|
def _load_update_check():
|
||||||
|
from utils import update_check
|
||||||
|
return update_check
|
||||||
|
|
||||||
|
|
||||||
def get_update_status_json(check_now: bool = False) -> str:
|
def get_update_status_json(check_now: bool = False) -> str:
|
||||||
payload = {
|
payload = {
|
||||||
"current_version": __version__,
|
"current_version": __version__,
|
||||||
"latest": "",
|
"latest": "",
|
||||||
"has_update": False,
|
"has_update": False,
|
||||||
"ahead_of_release": False,
|
"ahead_of_release": False,
|
||||||
|
"checked": False,
|
||||||
"html_url": RELEASES_PAGE_URL,
|
"html_url": RELEASES_PAGE_URL,
|
||||||
"error": "",
|
"error": "",
|
||||||
}
|
}
|
||||||
try:
|
try:
|
||||||
|
update_check = _load_update_check()
|
||||||
if check_now:
|
if check_now:
|
||||||
run_check(__version__)
|
update_check.run_check(__version__)
|
||||||
payload.update(get_status())
|
payload.update(update_check.get_status())
|
||||||
payload["current_version"] = __version__
|
payload["current_version"] = __version__
|
||||||
payload["html_url"] = payload.get("html_url") or RELEASES_PAGE_URL
|
payload["latest"] = payload.get("latest") or ""
|
||||||
|
payload["html_url"] = payload.get("html_url") or update_check.RELEASES_PAGE_URL
|
||||||
|
payload["error"] = payload.get("error") or ""
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
payload["error"] = str(exc)
|
payload["error"] = str(exc)
|
||||||
return json.dumps(payload)
|
return json.dumps(payload)
|
||||||
|
|
|
||||||
|
|
@ -18,13 +18,13 @@
|
||||||
<string name="system_check_background_restricted">Background restriction: enabled, Android may block long-running work.</string>
|
<string name="system_check_background_restricted">Background restriction: enabled, Android may block long-running work.</string>
|
||||||
<string name="system_check_oem_note">Some phones also require manual vendor settings such as Autostart, Lock in recents, or Unrestricted battery mode.</string>
|
<string name="system_check_oem_note">Some phones also require manual vendor settings such as Autostart, Lock in recents, or Unrestricted battery mode.</string>
|
||||||
<string name="endpoint_section_label">Proxy endpoint</string>
|
<string name="endpoint_section_label">Proxy endpoint</string>
|
||||||
<string name="routing_section_label">Routing</string>
|
|
||||||
<string name="advanced_section_label">Advanced</string>
|
<string name="advanced_section_label">Advanced</string>
|
||||||
<string name="actions_section_label">Actions</string>
|
<string name="actions_section_label">Actions</string>
|
||||||
<string name="updates_label">Updates</string>
|
<string name="updates_label">Updates</string>
|
||||||
<string name="updates_current_version_format">v%1$s</string>
|
<string name="updates_current_version_format">v%1$s</string>
|
||||||
<string name="updates_check_label">Check for updates on launch</string>
|
<string name="updates_check_label">Check for updates on launch</string>
|
||||||
<string name="updates_status_initial">Status will appear after background check.</string>
|
<string name="updates_status_initial">Checking GitHub release…</string>
|
||||||
|
<string name="updates_status_idle">Not checked yet</string>
|
||||||
<string name="updates_status_disabled">Automatic update checks are disabled.</string>
|
<string name="updates_status_disabled">Automatic update checks are disabled.</string>
|
||||||
<string name="updates_status_latest">Installed version %1$s matches the latest GitHub release.</string>
|
<string name="updates_status_latest">Installed version %1$s matches the latest GitHub release.</string>
|
||||||
<string name="updates_status_newer">Installed version %1$s is newer than the latest GitHub release.</string>
|
<string name="updates_status_newer">Installed version %1$s is newer than the latest GitHub release.</string>
|
||||||
|
|
|
||||||
|
|
@ -121,6 +121,22 @@ prefetch_chaquopy_runtime() {
|
||||||
done
|
done
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cleanup_stale_build_state() {
|
||||||
|
local stale_dirs=(
|
||||||
|
"$ROOT_DIR/app/build/python/env"
|
||||||
|
"$ROOT_DIR/app/build/intermediates/project_dex_archive"
|
||||||
|
"$ROOT_DIR/app/build/intermediates/desugar_graph"
|
||||||
|
"$ROOT_DIR/app/build/tmp/kotlin-classes"
|
||||||
|
"$ROOT_DIR/app/build/snapshot/kotlin"
|
||||||
|
)
|
||||||
|
|
||||||
|
for stale_dir in "${stale_dirs[@]}"; do
|
||||||
|
if [[ -d "$stale_dir" ]]; then
|
||||||
|
rm -rf "$stale_dir"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
prefetch_chaquopy_runtime
|
prefetch_chaquopy_runtime
|
||||||
|
|
||||||
for attempt in $(seq 1 "$ATTEMPTS"); do
|
for attempt in $(seq 1 "$ATTEMPTS"); do
|
||||||
|
|
@ -130,6 +146,7 @@ for attempt in $(seq 1 "$ATTEMPTS"); do
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ "$attempt" -lt "$ATTEMPTS" ]]; then
|
if [[ "$attempt" -lt "$ATTEMPTS" ]]; then
|
||||||
|
cleanup_stale_build_state
|
||||||
echo "Build failed, retrying in ${SLEEP_SECONDS}s..."
|
echo "Build failed, retrying in ${SLEEP_SECONDS}s..."
|
||||||
sleep "$SLEEP_SECONDS"
|
sleep "$SLEEP_SECONDS"
|
||||||
fi
|
fi
|
||||||
|
|
|
||||||
|
|
@ -17,17 +17,10 @@ DEFAULT_CONFIG = {
|
||||||
"port": 1080,
|
"port": 1080,
|
||||||
"host": "127.0.0.1",
|
"host": "127.0.0.1",
|
||||||
"dc_ip": ["2:149.154.167.220", "4:149.154.167.220"],
|
"dc_ip": ["2:149.154.167.220", "4:149.154.167.220"],
|
||||||
"upstream_mode": "telegram_ws_direct",
|
|
||||||
"relay_url": "",
|
|
||||||
"relay_token": "",
|
|
||||||
"direct_ws_timeout_seconds": 10.0,
|
|
||||||
"log_max_mb": 5,
|
"log_max_mb": 5,
|
||||||
"buf_kb": 256,
|
"buf_kb": 256,
|
||||||
"pool_size": 4,
|
"pool_size": 4,
|
||||||
"verbose": False,
|
"verbose": False,
|
||||||
"log_max_mb": 5,
|
|
||||||
"buf_kb": 256,
|
|
||||||
"pool_size": 4,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -132,11 +125,7 @@ class ProxyAppRuntime:
|
||||||
self.on_error(text)
|
self.on_error(text)
|
||||||
|
|
||||||
def _run_proxy_thread(self, port: int, dc_opt: Dict[int, str],
|
def _run_proxy_thread(self, port: int, dc_opt: Dict[int, str],
|
||||||
host: str = "127.0.0.1",
|
host: str = "127.0.0.1"):
|
||||||
upstream_mode: str = "telegram_ws_direct",
|
|
||||||
relay_url: str = "",
|
|
||||||
relay_token: str = "",
|
|
||||||
direct_ws_timeout_seconds: float = 10.0):
|
|
||||||
loop = _asyncio.new_event_loop()
|
loop = _asyncio.new_event_loop()
|
||||||
_asyncio.set_event_loop(loop)
|
_asyncio.set_event_loop(loop)
|
||||||
stop_ev = _asyncio.Event()
|
stop_ev = _asyncio.Event()
|
||||||
|
|
@ -144,12 +133,7 @@ class ProxyAppRuntime:
|
||||||
|
|
||||||
try:
|
try:
|
||||||
loop.run_until_complete(
|
loop.run_until_complete(
|
||||||
self.run_proxy(
|
self.run_proxy(port, dc_opt, stop_event=stop_ev, host=host))
|
||||||
port, dc_opt, stop_event=stop_ev, host=host,
|
|
||||||
upstream_mode=upstream_mode,
|
|
||||||
relay_url=relay_url or None,
|
|
||||||
relay_token=relay_token,
|
|
||||||
direct_ws_timeout_seconds=direct_ws_timeout_seconds))
|
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
self.log.error("Proxy thread crashed: %s", exc)
|
self.log.error("Proxy thread crashed: %s", exc)
|
||||||
if ("10048" in str(exc) or
|
if ("10048" in str(exc) or
|
||||||
|
|
@ -173,15 +157,6 @@ class ProxyAppRuntime:
|
||||||
port = active_cfg.get("port", self.default_config["port"])
|
port = active_cfg.get("port", self.default_config["port"])
|
||||||
host = active_cfg.get("host", self.default_config["host"])
|
host = active_cfg.get("host", self.default_config["host"])
|
||||||
dc_ip_list = active_cfg.get("dc_ip", self.default_config["dc_ip"])
|
dc_ip_list = active_cfg.get("dc_ip", self.default_config["dc_ip"])
|
||||||
upstream_mode = active_cfg.get(
|
|
||||||
"upstream_mode", self.default_config["upstream_mode"])
|
|
||||||
relay_url = active_cfg.get(
|
|
||||||
"relay_url", self.default_config["relay_url"])
|
|
||||||
relay_token = active_cfg.get(
|
|
||||||
"relay_token", self.default_config["relay_token"])
|
|
||||||
direct_ws_timeout_seconds = active_cfg.get(
|
|
||||||
"direct_ws_timeout_seconds",
|
|
||||||
self.default_config["direct_ws_timeout_seconds"])
|
|
||||||
buf_kb = active_cfg.get("buf_kb", self.default_config["buf_kb"])
|
buf_kb = active_cfg.get("buf_kb", self.default_config["buf_kb"])
|
||||||
pool_size = active_cfg.get(
|
pool_size = active_cfg.get(
|
||||||
"pool_size", self.default_config["pool_size"])
|
"pool_size", self.default_config["pool_size"])
|
||||||
|
|
@ -203,10 +178,6 @@ class ProxyAppRuntime:
|
||||||
port,
|
port,
|
||||||
dc_opt,
|
dc_opt,
|
||||||
host,
|
host,
|
||||||
upstream_mode,
|
|
||||||
relay_url,
|
|
||||||
relay_token,
|
|
||||||
direct_ws_timeout_seconds,
|
|
||||||
),
|
),
|
||||||
daemon=True,
|
daemon=True,
|
||||||
name="proxy")
|
name="proxy")
|
||||||
|
|
|
||||||
|
|
@ -125,16 +125,21 @@ class AndroidProxyBridgeTests(unittest.TestCase):
|
||||||
self.assertTrue(captured["verbose"])
|
self.assertTrue(captured["verbose"])
|
||||||
|
|
||||||
def test_get_update_status_json_merges_python_update_state(self):
|
def test_get_update_status_json_merges_python_update_state(self):
|
||||||
original_run_check = android_proxy_bridge.run_check
|
original_load_update_check = android_proxy_bridge._load_update_check
|
||||||
original_get_status = android_proxy_bridge.get_status
|
|
||||||
try:
|
try:
|
||||||
captured = {}
|
captured = {}
|
||||||
|
|
||||||
def fake_run_check(version):
|
class FakeUpdateCheck:
|
||||||
|
RELEASES_PAGE_URL = "https://example.com/releases/latest"
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def run_check(version):
|
||||||
captured["run_check_version"] = version
|
captured["run_check_version"] = version
|
||||||
|
|
||||||
def fake_get_status():
|
@staticmethod
|
||||||
|
def get_status():
|
||||||
return {
|
return {
|
||||||
|
"checked": True,
|
||||||
"latest": "1.3.1",
|
"latest": "1.3.1",
|
||||||
"has_update": True,
|
"has_update": True,
|
||||||
"ahead_of_release": False,
|
"ahead_of_release": False,
|
||||||
|
|
@ -142,20 +147,83 @@ class AndroidProxyBridgeTests(unittest.TestCase):
|
||||||
"error": "",
|
"error": "",
|
||||||
}
|
}
|
||||||
|
|
||||||
android_proxy_bridge.run_check = fake_run_check
|
android_proxy_bridge._load_update_check = lambda: FakeUpdateCheck
|
||||||
android_proxy_bridge.get_status = fake_get_status
|
|
||||||
|
|
||||||
result = json.loads(android_proxy_bridge.get_update_status_json(True))
|
result = json.loads(android_proxy_bridge.get_update_status_json(True))
|
||||||
finally:
|
finally:
|
||||||
android_proxy_bridge.run_check = original_run_check
|
android_proxy_bridge._load_update_check = original_load_update_check
|
||||||
android_proxy_bridge.get_status = original_get_status
|
|
||||||
|
|
||||||
self.assertEqual(captured["run_check_version"], android_proxy_bridge.__version__)
|
self.assertEqual(captured["run_check_version"], android_proxy_bridge.__version__)
|
||||||
self.assertEqual(result["current_version"], android_proxy_bridge.__version__)
|
self.assertEqual(result["current_version"], android_proxy_bridge.__version__)
|
||||||
self.assertEqual(result["latest"], "1.3.1")
|
self.assertEqual(result["latest"], "1.3.1")
|
||||||
self.assertTrue(result["has_update"])
|
self.assertTrue(result["has_update"])
|
||||||
|
self.assertTrue(result["checked"])
|
||||||
self.assertEqual(result["html_url"], "https://example.com/release")
|
self.assertEqual(result["html_url"], "https://example.com/release")
|
||||||
|
|
||||||
|
def test_get_update_status_json_reports_unchecked_state(self):
|
||||||
|
original_load_update_check = android_proxy_bridge._load_update_check
|
||||||
|
try:
|
||||||
|
class FakeUpdateCheck:
|
||||||
|
RELEASES_PAGE_URL = "https://example.com/releases/latest"
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_status():
|
||||||
|
return {
|
||||||
|
"checked": False,
|
||||||
|
"latest": "",
|
||||||
|
"has_update": False,
|
||||||
|
"ahead_of_release": False,
|
||||||
|
"html_url": "",
|
||||||
|
"error": "",
|
||||||
|
}
|
||||||
|
|
||||||
|
android_proxy_bridge._load_update_check = lambda: FakeUpdateCheck
|
||||||
|
result = json.loads(android_proxy_bridge.get_update_status_json(False))
|
||||||
|
finally:
|
||||||
|
android_proxy_bridge._load_update_check = original_load_update_check
|
||||||
|
|
||||||
|
self.assertFalse(result["checked"])
|
||||||
|
self.assertEqual(result["current_version"], android_proxy_bridge.__version__)
|
||||||
|
|
||||||
|
def test_get_update_status_json_reports_import_error_without_breaking_bridge(self):
|
||||||
|
original_load_update_check = android_proxy_bridge._load_update_check
|
||||||
|
try:
|
||||||
|
def fail():
|
||||||
|
raise ModuleNotFoundError("No module named 'utils'")
|
||||||
|
|
||||||
|
android_proxy_bridge._load_update_check = fail
|
||||||
|
result = json.loads(android_proxy_bridge.get_update_status_json(True))
|
||||||
|
finally:
|
||||||
|
android_proxy_bridge._load_update_check = original_load_update_check
|
||||||
|
|
||||||
|
self.assertFalse(result["checked"])
|
||||||
|
self.assertIn("No module named 'utils'", result["error"])
|
||||||
|
|
||||||
|
def test_get_update_status_json_normalizes_none_fields_for_kotlin(self):
|
||||||
|
original_load_update_check = android_proxy_bridge._load_update_check
|
||||||
|
try:
|
||||||
|
class FakeUpdateCheck:
|
||||||
|
RELEASES_PAGE_URL = "https://example.com/releases/latest"
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_status():
|
||||||
|
return {
|
||||||
|
"checked": True,
|
||||||
|
"latest": None,
|
||||||
|
"has_update": False,
|
||||||
|
"ahead_of_release": True,
|
||||||
|
"html_url": None,
|
||||||
|
"error": None,
|
||||||
|
}
|
||||||
|
|
||||||
|
android_proxy_bridge._load_update_check = lambda: FakeUpdateCheck
|
||||||
|
result = json.loads(android_proxy_bridge.get_update_status_json(False))
|
||||||
|
finally:
|
||||||
|
android_proxy_bridge._load_update_check = original_load_update_check
|
||||||
|
|
||||||
|
self.assertEqual(result["latest"], "")
|
||||||
|
self.assertEqual(result["error"], "")
|
||||||
|
self.assertEqual(result["html_url"], "https://example.com/releases/latest")
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,54 @@
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
from proxy import __version__
|
||||||
|
from utils import update_check
|
||||||
|
|
||||||
|
|
||||||
|
class UpdateCheckTests(unittest.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self._orig_state = dict(update_check._state)
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
update_check._state.clear()
|
||||||
|
update_check._state.update(self._orig_state)
|
||||||
|
|
||||||
|
def test_apply_release_tag_marks_update_available(self):
|
||||||
|
update_check._apply_release_tag(
|
||||||
|
tag="v1.3.1",
|
||||||
|
html_url="https://example.com/release",
|
||||||
|
current_version=__version__,
|
||||||
|
)
|
||||||
|
|
||||||
|
status = update_check.get_status()
|
||||||
|
self.assertTrue(status["has_update"])
|
||||||
|
self.assertFalse(status["ahead_of_release"])
|
||||||
|
self.assertEqual(status["latest"], "1.3.1")
|
||||||
|
self.assertEqual(status["html_url"], "https://example.com/release")
|
||||||
|
|
||||||
|
def test_apply_release_tag_marks_ahead_of_release(self):
|
||||||
|
update_check._apply_release_tag(
|
||||||
|
tag="v1.2.1",
|
||||||
|
html_url="https://example.com/release",
|
||||||
|
current_version=__version__,
|
||||||
|
)
|
||||||
|
|
||||||
|
status = update_check.get_status()
|
||||||
|
self.assertFalse(status["has_update"])
|
||||||
|
self.assertTrue(status["ahead_of_release"])
|
||||||
|
self.assertEqual(status["latest"], "1.2.1")
|
||||||
|
|
||||||
|
def test_apply_release_tag_marks_latest_when_versions_match(self):
|
||||||
|
update_check._apply_release_tag(
|
||||||
|
tag=f"v{__version__}",
|
||||||
|
html_url="https://example.com/release",
|
||||||
|
current_version=__version__,
|
||||||
|
)
|
||||||
|
|
||||||
|
status = update_check.get_status()
|
||||||
|
self.assertFalse(status["has_update"])
|
||||||
|
self.assertFalse(status["ahead_of_release"])
|
||||||
|
self.assertEqual(status["latest"], __version__)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
|
|
@ -16,7 +16,7 @@ from typing import Any, Dict, Optional, Tuple
|
||||||
from urllib.error import HTTPError, URLError
|
from urllib.error import HTTPError, URLError
|
||||||
from urllib.request import Request, urlopen
|
from urllib.request import Request, urlopen
|
||||||
|
|
||||||
REPO = "Dark-Avery/tg-ws-proxy"
|
REPO = "Flowseal/tg-ws-proxy"
|
||||||
RELEASES_LATEST_API = f"https://api.github.com/repos/{REPO}/releases/latest"
|
RELEASES_LATEST_API = f"https://api.github.com/repos/{REPO}/releases/latest"
|
||||||
RELEASES_PAGE_URL = f"https://github.com/{REPO}/releases/latest"
|
RELEASES_PAGE_URL = f"https://github.com/{REPO}/releases/latest"
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue