feat(android): embed python runtime and boot proxy service inside foreground service
This commit is contained in:
parent
47e5c6241d
commit
ec6de3afb3
|
|
@ -19,6 +19,7 @@ build/
|
||||||
.gradle/
|
.gradle/
|
||||||
.gradle-local/
|
.gradle-local/
|
||||||
android/.gradle-local/
|
android/.gradle-local/
|
||||||
|
android/.m2-chaquopy*/
|
||||||
local.properties
|
local.properties
|
||||||
android/.idea/
|
android/.idea/
|
||||||
android/build/
|
android/build/
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,19 @@
|
||||||
|
import org.gradle.api.tasks.Sync
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
id("com.android.application")
|
id("com.android.application")
|
||||||
|
id("com.chaquo.python")
|
||||||
id("org.jetbrains.kotlin.android")
|
id("org.jetbrains.kotlin.android")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val stagedPythonSourcesDir = layout.buildDirectory.dir("generated/chaquopy/python")
|
||||||
|
val stagePythonSources by tasks.registering(Sync::class) {
|
||||||
|
from(rootProject.projectDir.resolve("../proxy")) {
|
||||||
|
into("proxy")
|
||||||
|
}
|
||||||
|
into(stagedPythonSourcesDir)
|
||||||
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
namespace = "org.flowseal.tgwsproxy"
|
namespace = "org.flowseal.tgwsproxy"
|
||||||
compileSdk = 34
|
compileSdk = 34
|
||||||
|
|
@ -15,6 +26,10 @@ android {
|
||||||
versionName = "0.1.0"
|
versionName = "0.1.0"
|
||||||
|
|
||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
|
|
||||||
|
ndk {
|
||||||
|
abiFilters += listOf("arm64-v8a", "x86_64")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
|
|
@ -41,6 +56,18 @@ android {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
chaquopy {
|
||||||
|
defaultConfig {
|
||||||
|
version = "3.12"
|
||||||
|
}
|
||||||
|
sourceSets {
|
||||||
|
getByName("main") {
|
||||||
|
srcDir("src/main/python")
|
||||||
|
srcDir(stagePythonSources)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation("androidx.core:core-ktx:1.13.1")
|
implementation("androidx.core:core-ktx:1.13.1")
|
||||||
implementation("androidx.appcompat:appcompat:1.7.0")
|
implementation("androidx.appcompat:appcompat:1.7.0")
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ import androidx.lifecycle.Lifecycle
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.lifecycle.repeatOnLifecycle
|
import androidx.lifecycle.repeatOnLifecycle
|
||||||
import com.google.android.material.snackbar.Snackbar
|
import com.google.android.material.snackbar.Snackbar
|
||||||
|
import kotlinx.coroutines.flow.combine
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.flowseal.tgwsproxy.databinding.ActivityMainBinding
|
import org.flowseal.tgwsproxy.databinding.ActivityMainBinding
|
||||||
|
|
||||||
|
|
@ -89,21 +90,41 @@ class MainActivity : AppCompatActivity() {
|
||||||
private fun observeServiceState() {
|
private fun observeServiceState() {
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
repeatOnLifecycle(Lifecycle.State.STARTED) {
|
repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||||
ProxyServiceState.isRunning.collect { isRunning ->
|
combine(
|
||||||
|
ProxyServiceState.isStarting,
|
||||||
|
ProxyServiceState.isRunning,
|
||||||
|
) { isStarting, isRunning ->
|
||||||
|
isStarting to isRunning
|
||||||
|
}.collect { (isStarting, isRunning) ->
|
||||||
binding.statusValue.text = getString(
|
binding.statusValue.text = getString(
|
||||||
if (isRunning) R.string.status_running else R.string.status_stopped,
|
when {
|
||||||
|
isStarting -> R.string.status_starting
|
||||||
|
isRunning -> R.string.status_running
|
||||||
|
else -> R.string.status_stopped
|
||||||
|
},
|
||||||
)
|
)
|
||||||
binding.startButton.isEnabled = !isRunning
|
binding.startButton.isEnabled = !isStarting && !isRunning
|
||||||
binding.stopButton.isEnabled = isRunning
|
binding.stopButton.isEnabled = isStarting || isRunning
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
repeatOnLifecycle(Lifecycle.State.STARTED) {
|
repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||||
ProxyServiceState.activeConfig.collect { config ->
|
combine(
|
||||||
|
ProxyServiceState.activeConfig,
|
||||||
|
ProxyServiceState.isStarting,
|
||||||
|
) { config, isStarting ->
|
||||||
|
config to isStarting
|
||||||
|
}.collect { (config, isStarting) ->
|
||||||
binding.serviceHint.text = if (config == null) {
|
binding.serviceHint.text = if (config == null) {
|
||||||
getString(R.string.service_hint_idle)
|
getString(R.string.service_hint_idle)
|
||||||
|
} else if (isStarting) {
|
||||||
|
getString(
|
||||||
|
R.string.service_hint_starting,
|
||||||
|
config.host,
|
||||||
|
config.port,
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
getString(
|
getString(
|
||||||
R.string.service_hint_running,
|
R.string.service_hint_running,
|
||||||
|
|
@ -114,6 +135,22 @@ class MainActivity : AppCompatActivity() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
lifecycleScope.launch {
|
||||||
|
repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||||
|
ProxyServiceState.lastError.collect { error ->
|
||||||
|
if (error.isNullOrBlank()) {
|
||||||
|
if (!binding.errorText.isVisible) {
|
||||||
|
return@collect
|
||||||
|
}
|
||||||
|
binding.errorText.isVisible = false
|
||||||
|
} else {
|
||||||
|
binding.errorText.text = error
|
||||||
|
binding.errorText.isVisible = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun requestNotificationPermissionIfNeeded() {
|
private fun requestNotificationPermissionIfNeeded() {
|
||||||
|
|
|
||||||
|
|
@ -9,9 +9,15 @@ import android.content.Intent
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
import androidx.core.app.NotificationCompat
|
import androidx.core.app.NotificationCompat
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.SupervisorJob
|
||||||
|
import kotlinx.coroutines.cancel
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
class ProxyForegroundService : Service() {
|
class ProxyForegroundService : Service() {
|
||||||
private lateinit var settingsStore: ProxySettingsStore
|
private lateinit var settingsStore: ProxySettingsStore
|
||||||
|
private val serviceScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
|
|
@ -22,19 +28,31 @@ class ProxyForegroundService : Service() {
|
||||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||||
return when (intent?.action) {
|
return when (intent?.action) {
|
||||||
ACTION_STOP -> {
|
ACTION_STOP -> {
|
||||||
stopForeground(STOP_FOREGROUND_REMOVE)
|
ProxyServiceState.clearError()
|
||||||
stopSelf()
|
serviceScope.launch {
|
||||||
|
stopProxyRuntime(removeNotification = true, stopService = true)
|
||||||
|
}
|
||||||
START_NOT_STICKY
|
START_NOT_STICKY
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
val config = settingsStore.load().validate().normalized
|
val config = settingsStore.load().validate().normalized
|
||||||
if (config == null) {
|
if (config == null) {
|
||||||
|
ProxyServiceState.markFailed(getString(R.string.saved_config_invalid))
|
||||||
|
stopForeground(STOP_FOREGROUND_REMOVE)
|
||||||
stopSelf()
|
stopSelf()
|
||||||
START_NOT_STICKY
|
START_NOT_STICKY
|
||||||
} else {
|
} else {
|
||||||
ProxyServiceState.markStarted(config)
|
ProxyServiceState.markStarting(config)
|
||||||
startForeground(NOTIFICATION_ID, buildNotification(config))
|
startForeground(
|
||||||
|
NOTIFICATION_ID,
|
||||||
|
buildNotification(
|
||||||
|
getString(R.string.notification_starting, config.host, config.port),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
serviceScope.launch {
|
||||||
|
startProxyRuntime(config)
|
||||||
|
}
|
||||||
START_STICKY
|
START_STICKY
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -42,14 +60,15 @@ class ProxyForegroundService : Service() {
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
|
serviceScope.cancel()
|
||||||
|
runCatching { PythonProxyBridge.stop(this) }
|
||||||
ProxyServiceState.markStopped()
|
ProxyServiceState.markStopped()
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onBind(intent: Intent?): IBinder? = null
|
override fun onBind(intent: Intent?): IBinder? = null
|
||||||
|
|
||||||
private fun buildNotification(config: NormalizedProxyConfig): Notification {
|
private fun buildNotification(contentText: String): Notification {
|
||||||
val contentText = "SOCKS5 ${config.host}:${config.port} • service shell active"
|
|
||||||
return NotificationCompat.Builder(this, CHANNEL_ID)
|
return NotificationCompat.Builder(this, CHANNEL_ID)
|
||||||
.setContentTitle(getString(R.string.notification_title))
|
.setContentTitle(getString(R.string.notification_title))
|
||||||
.setContentText(contentText)
|
.setContentText(contentText)
|
||||||
|
|
@ -59,6 +78,40 @@ class ProxyForegroundService : Service() {
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private suspend fun startProxyRuntime(config: NormalizedProxyConfig) {
|
||||||
|
val result = runCatching {
|
||||||
|
PythonProxyBridge.start(this, config)
|
||||||
|
}
|
||||||
|
|
||||||
|
result.onSuccess {
|
||||||
|
ProxyServiceState.markStarted(config)
|
||||||
|
updateNotification(getString(R.string.notification_running, config.host, config.port))
|
||||||
|
}.onFailure { error ->
|
||||||
|
ProxyServiceState.markFailed(
|
||||||
|
error.message ?: getString(R.string.proxy_start_failed_generic),
|
||||||
|
)
|
||||||
|
stopForeground(STOP_FOREGROUND_REMOVE)
|
||||||
|
stopSelf()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun stopProxyRuntime(removeNotification: Boolean, stopService: Boolean) {
|
||||||
|
runCatching { PythonProxyBridge.stop(this) }
|
||||||
|
ProxyServiceState.markStopped()
|
||||||
|
|
||||||
|
if (removeNotification) {
|
||||||
|
stopForeground(STOP_FOREGROUND_REMOVE)
|
||||||
|
}
|
||||||
|
if (stopService) {
|
||||||
|
stopSelf()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateNotification(contentText: String) {
|
||||||
|
val manager = getSystemService(NotificationManager::class.java)
|
||||||
|
manager.notify(NOTIFICATION_ID, buildNotification(contentText))
|
||||||
|
}
|
||||||
|
|
||||||
private fun createNotificationChannel() {
|
private fun createNotificationChannel() {
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
|
||||||
return
|
return
|
||||||
|
|
|
||||||
|
|
@ -7,16 +7,43 @@ object ProxyServiceState {
|
||||||
private val _isRunning = MutableStateFlow(false)
|
private val _isRunning = MutableStateFlow(false)
|
||||||
val isRunning: StateFlow<Boolean> = _isRunning
|
val isRunning: StateFlow<Boolean> = _isRunning
|
||||||
|
|
||||||
|
private val _isStarting = MutableStateFlow(false)
|
||||||
|
val isStarting: StateFlow<Boolean> = _isStarting
|
||||||
|
|
||||||
private val _activeConfig = MutableStateFlow<NormalizedProxyConfig?>(null)
|
private val _activeConfig = MutableStateFlow<NormalizedProxyConfig?>(null)
|
||||||
val activeConfig: StateFlow<NormalizedProxyConfig?> = _activeConfig
|
val activeConfig: StateFlow<NormalizedProxyConfig?> = _activeConfig
|
||||||
|
|
||||||
|
private val _lastError = MutableStateFlow<String?>(null)
|
||||||
|
val lastError: StateFlow<String?> = _lastError
|
||||||
|
|
||||||
|
fun markStarting(config: NormalizedProxyConfig) {
|
||||||
|
_activeConfig.value = config
|
||||||
|
_isStarting.value = true
|
||||||
|
_isRunning.value = false
|
||||||
|
_lastError.value = null
|
||||||
|
}
|
||||||
|
|
||||||
fun markStarted(config: NormalizedProxyConfig) {
|
fun markStarted(config: NormalizedProxyConfig) {
|
||||||
_activeConfig.value = config
|
_activeConfig.value = config
|
||||||
|
_isStarting.value = false
|
||||||
_isRunning.value = true
|
_isRunning.value = true
|
||||||
|
_lastError.value = null
|
||||||
|
}
|
||||||
|
|
||||||
|
fun markFailed(message: String) {
|
||||||
|
_activeConfig.value = null
|
||||||
|
_isStarting.value = false
|
||||||
|
_isRunning.value = false
|
||||||
|
_lastError.value = message
|
||||||
}
|
}
|
||||||
|
|
||||||
fun markStopped() {
|
fun markStopped() {
|
||||||
_activeConfig.value = null
|
_activeConfig.value = null
|
||||||
|
_isStarting.value = false
|
||||||
_isRunning.value = false
|
_isRunning.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun clearError() {
|
||||||
|
_lastError.value = null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,39 @@
|
||||||
|
package org.flowseal.tgwsproxy
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import com.chaquo.python.Python
|
||||||
|
import com.chaquo.python.android.AndroidPlatform
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
object PythonProxyBridge {
|
||||||
|
private const val MODULE_NAME = "android_proxy_bridge"
|
||||||
|
|
||||||
|
fun start(context: Context, config: NormalizedProxyConfig): String {
|
||||||
|
val module = getModule(context)
|
||||||
|
return module.callAttr(
|
||||||
|
"start_proxy",
|
||||||
|
File(context.filesDir, "tg-ws-proxy").absolutePath,
|
||||||
|
config.host,
|
||||||
|
config.port,
|
||||||
|
config.dcIpList,
|
||||||
|
config.verbose,
|
||||||
|
).toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun stop(context: Context) {
|
||||||
|
if (!Python.isStarted()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
getModule(context).callAttr("stop_proxy")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getModule(context: Context) =
|
||||||
|
getPython(context.applicationContext).getModule(MODULE_NAME)
|
||||||
|
|
||||||
|
private fun getPython(context: Context): Python {
|
||||||
|
if (!Python.isStarted()) {
|
||||||
|
Python.start(AndroidPlatform(context))
|
||||||
|
}
|
||||||
|
return Python.getInstance()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,92 @@
|
||||||
|
import os
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Iterable, Optional
|
||||||
|
|
||||||
|
from proxy.app_runtime import ProxyAppRuntime
|
||||||
|
|
||||||
|
|
||||||
|
_RUNTIME_LOCK = threading.RLock()
|
||||||
|
_RUNTIME: Optional[ProxyAppRuntime] = None
|
||||||
|
_LAST_ERROR: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
def _remember_error(message: str) -> None:
|
||||||
|
global _LAST_ERROR
|
||||||
|
_LAST_ERROR = message
|
||||||
|
|
||||||
|
|
||||||
|
def _normalize_dc_ip_list(dc_ip_list: Iterable[object]) -> list[str]:
|
||||||
|
return [str(item).strip() for item in dc_ip_list if str(item).strip()]
|
||||||
|
|
||||||
|
|
||||||
|
def start_proxy(app_dir: str, host: str, port: int,
|
||||||
|
dc_ip_list: Iterable[object], verbose: bool = False) -> str:
|
||||||
|
global _RUNTIME, _LAST_ERROR
|
||||||
|
|
||||||
|
with _RUNTIME_LOCK:
|
||||||
|
if _RUNTIME is not None:
|
||||||
|
_RUNTIME.stop_proxy()
|
||||||
|
_RUNTIME = None
|
||||||
|
|
||||||
|
_LAST_ERROR = None
|
||||||
|
os.environ["TG_WS_PROXY_CRYPTO_BACKEND"] = "python"
|
||||||
|
|
||||||
|
runtime = ProxyAppRuntime(
|
||||||
|
Path(app_dir),
|
||||||
|
logger_name="tg-ws-android",
|
||||||
|
on_error=_remember_error,
|
||||||
|
)
|
||||||
|
runtime.reset_log_file()
|
||||||
|
runtime.setup_logging(verbose=verbose)
|
||||||
|
|
||||||
|
config = {
|
||||||
|
"host": host,
|
||||||
|
"port": int(port),
|
||||||
|
"dc_ip": _normalize_dc_ip_list(dc_ip_list),
|
||||||
|
"verbose": bool(verbose),
|
||||||
|
}
|
||||||
|
runtime.save_config(config)
|
||||||
|
|
||||||
|
if not runtime.start_proxy(config):
|
||||||
|
_RUNTIME = None
|
||||||
|
raise RuntimeError(_LAST_ERROR or "Failed to start proxy runtime.")
|
||||||
|
|
||||||
|
_RUNTIME = runtime
|
||||||
|
|
||||||
|
# Give the proxy thread a short warm-up window so immediate bind failures
|
||||||
|
# surface before Kotlin reports the service as running.
|
||||||
|
for _ in range(10):
|
||||||
|
time.sleep(0.1)
|
||||||
|
with _RUNTIME_LOCK:
|
||||||
|
if _LAST_ERROR:
|
||||||
|
runtime.stop_proxy()
|
||||||
|
_RUNTIME = None
|
||||||
|
raise RuntimeError(_LAST_ERROR)
|
||||||
|
if runtime.is_proxy_running():
|
||||||
|
return str(runtime.log_file)
|
||||||
|
|
||||||
|
with _RUNTIME_LOCK:
|
||||||
|
runtime.stop_proxy()
|
||||||
|
_RUNTIME = None
|
||||||
|
raise RuntimeError("Proxy runtime did not become ready in time.")
|
||||||
|
|
||||||
|
|
||||||
|
def stop_proxy() -> None:
|
||||||
|
global _RUNTIME, _LAST_ERROR
|
||||||
|
|
||||||
|
with _RUNTIME_LOCK:
|
||||||
|
_LAST_ERROR = None
|
||||||
|
if _RUNTIME is not None:
|
||||||
|
_RUNTIME.stop_proxy()
|
||||||
|
_RUNTIME = None
|
||||||
|
|
||||||
|
|
||||||
|
def is_running() -> bool:
|
||||||
|
with _RUNTIME_LOCK:
|
||||||
|
return bool(_RUNTIME and _RUNTIME.is_proxy_running())
|
||||||
|
|
||||||
|
|
||||||
|
def get_last_error() -> Optional[str]:
|
||||||
|
return _LAST_ERROR
|
||||||
|
|
@ -1,12 +1,14 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<resources>
|
||||||
<string name="app_name">TG WS Proxy</string>
|
<string name="app_name">TG WS Proxy</string>
|
||||||
<string name="subtitle">Android shell for the local Telegram SOCKS5 proxy. The embedded Python runtime will be wired in the next commit.</string>
|
<string name="subtitle">Android app for the local Telegram SOCKS5 proxy.</string>
|
||||||
<string name="status_label">Foreground service</string>
|
<string name="status_label">Foreground service</string>
|
||||||
|
<string name="status_starting">Starting</string>
|
||||||
<string name="status_running">Running</string>
|
<string name="status_running">Running</string>
|
||||||
<string name="status_stopped">Stopped</string>
|
<string name="status_stopped">Stopped</string>
|
||||||
<string name="service_hint_idle">The Android service shell is ready. Save settings, then start the service.</string>
|
<string name="service_hint_idle">Configure the proxy settings, then start the foreground service.</string>
|
||||||
<string name="service_hint_running">Foreground service active for %1$s:%2$d. Python proxy bootstrap will be connected next.</string>
|
<string name="service_hint_starting">Starting embedded Python proxy for %1$s:%2$d.</string>
|
||||||
|
<string name="service_hint_running">Foreground service active for %1$s:%2$d.</string>
|
||||||
<string name="host_hint">Proxy host</string>
|
<string name="host_hint">Proxy host</string>
|
||||||
<string name="port_hint">Proxy port</string>
|
<string name="port_hint">Proxy port</string>
|
||||||
<string name="dc_ip_hint">DC to IP mappings (one DC:IP per line)</string>
|
<string name="dc_ip_hint">DC to IP mappings (one DC:IP per line)</string>
|
||||||
|
|
@ -19,4 +21,8 @@
|
||||||
<string name="notification_title">TG WS Proxy</string>
|
<string name="notification_title">TG WS Proxy</string>
|
||||||
<string name="notification_channel_name">Proxy service</string>
|
<string name="notification_channel_name">Proxy service</string>
|
||||||
<string name="notification_channel_description">Keeps the Telegram proxy service alive in the foreground.</string>
|
<string name="notification_channel_description">Keeps the Telegram proxy service alive in the foreground.</string>
|
||||||
|
<string name="notification_starting">SOCKS5 %1$s:%2$d • starting embedded Python</string>
|
||||||
|
<string name="notification_running">SOCKS5 %1$s:%2$d • proxy active</string>
|
||||||
|
<string name="saved_config_invalid">Saved proxy settings are invalid.</string>
|
||||||
|
<string name="proxy_start_failed_generic">Failed to start embedded Python proxy.</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
||||||
|
|
@ -44,6 +44,51 @@ fi
|
||||||
ATTEMPTS="${ATTEMPTS:-5}"
|
ATTEMPTS="${ATTEMPTS:-5}"
|
||||||
SLEEP_SECONDS="${SLEEP_SECONDS:-15}"
|
SLEEP_SECONDS="${SLEEP_SECONDS:-15}"
|
||||||
TASK="${1:-assembleDebug}"
|
TASK="${1:-assembleDebug}"
|
||||||
|
LOCAL_CHAQUOPY_REPO="${LOCAL_CHAQUOPY_REPO:-$ROOT_DIR/.m2-chaquopy}"
|
||||||
|
CHAQUOPY_MAVEN_BASE="${CHAQUOPY_MAVEN_BASE:-https://repo.maven.apache.org/maven2}"
|
||||||
|
|
||||||
|
prefetch_artifact() {
|
||||||
|
local relative_path="$1"
|
||||||
|
local destination="$LOCAL_CHAQUOPY_REPO/$relative_path"
|
||||||
|
|
||||||
|
if [[ -f "$destination" ]]; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
mkdir -p "$(dirname "$destination")"
|
||||||
|
echo "Prefetching $relative_path"
|
||||||
|
curl \
|
||||||
|
--fail \
|
||||||
|
--location \
|
||||||
|
--retry 8 \
|
||||||
|
--retry-all-errors \
|
||||||
|
--continue-at - \
|
||||||
|
--connect-timeout 15 \
|
||||||
|
--speed-limit 1024 \
|
||||||
|
--speed-time 20 \
|
||||||
|
--max-time 90 \
|
||||||
|
--output "$destination" \
|
||||||
|
"$CHAQUOPY_MAVEN_BASE/$relative_path"
|
||||||
|
}
|
||||||
|
|
||||||
|
prefetch_chaquopy_runtime() {
|
||||||
|
local artifacts=(
|
||||||
|
"com/chaquo/python/runtime/chaquopy_java/17.0.0/chaquopy_java-17.0.0.pom"
|
||||||
|
"com/chaquo/python/runtime/chaquopy_java/17.0.0/chaquopy_java-17.0.0.jar"
|
||||||
|
"com/chaquo/python/runtime/libchaquopy_java/17.0.0/libchaquopy_java-17.0.0.pom"
|
||||||
|
"com/chaquo/python/runtime/libchaquopy_java/17.0.0/libchaquopy_java-17.0.0-3.12-arm64-v8a.so"
|
||||||
|
"com/chaquo/python/runtime/libchaquopy_java/17.0.0/libchaquopy_java-17.0.0-3.12-x86_64.so"
|
||||||
|
"com/chaquo/python/target/3.12.12-0/target-3.12.12-0.pom"
|
||||||
|
"com/chaquo/python/target/3.12.12-0/target-3.12.12-0-arm64-v8a.zip"
|
||||||
|
"com/chaquo/python/target/3.12.12-0/target-3.12.12-0-x86_64.zip"
|
||||||
|
)
|
||||||
|
|
||||||
|
for artifact in "${artifacts[@]}"; do
|
||||||
|
prefetch_artifact "$artifact"
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
prefetch_chaquopy_runtime
|
||||||
|
|
||||||
for attempt in $(seq 1 "$ATTEMPTS"); do
|
for attempt in $(seq 1 "$ATTEMPTS"); do
|
||||||
echo "==> Android build attempt $attempt/$ATTEMPTS ($TASK)"
|
echo "==> Android build attempt $attempt/$ATTEMPTS ($TASK)"
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
plugins {
|
plugins {
|
||||||
id("com.android.application") version "8.5.2" apply false
|
id("com.android.application") version "8.5.2" apply false
|
||||||
|
id("com.chaquo.python") version "17.0.0" apply false
|
||||||
id("org.jetbrains.kotlin.android") version "1.9.24" apply false
|
id("org.jetbrains.kotlin.android") version "1.9.24" apply false
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,10 @@
|
||||||
pluginManagement {
|
pluginManagement {
|
||||||
repositories {
|
repositories {
|
||||||
|
val localChaquopyRepo = file(System.getenv("LOCAL_CHAQUOPY_REPO") ?: ".m2-chaquopy")
|
||||||
|
if (localChaquopyRepo.isDirectory) {
|
||||||
|
maven(url = localChaquopyRepo.toURI())
|
||||||
|
}
|
||||||
|
maven("https://chaquo.com/maven")
|
||||||
gradlePluginPortal()
|
gradlePluginPortal()
|
||||||
google()
|
google()
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
|
|
@ -9,6 +14,11 @@ pluginManagement {
|
||||||
dependencyResolutionManagement {
|
dependencyResolutionManagement {
|
||||||
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
|
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
|
||||||
repositories {
|
repositories {
|
||||||
|
val localChaquopyRepo = file(System.getenv("LOCAL_CHAQUOPY_REPO") ?: ".m2-chaquopy")
|
||||||
|
if (localChaquopyRepo.isDirectory) {
|
||||||
|
maven(url = localChaquopyRepo.toURI())
|
||||||
|
}
|
||||||
|
maven("https://chaquo.com/maven")
|
||||||
google()
|
google()
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
"""TG WS Proxy core package."""
|
||||||
|
|
@ -80,11 +80,20 @@ class ProxyAppRuntime:
|
||||||
root = logging.getLogger()
|
root = logging.getLogger()
|
||||||
root.setLevel(logging.DEBUG if verbose else logging.INFO)
|
root.setLevel(logging.DEBUG if verbose else logging.INFO)
|
||||||
|
|
||||||
|
for handler in list(root.handlers):
|
||||||
|
if getattr(handler, "_tg_ws_proxy_runtime_handler", False):
|
||||||
|
root.removeHandler(handler)
|
||||||
|
try:
|
||||||
|
handler.close()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
fh = logging.FileHandler(str(self.log_file), encoding="utf-8")
|
fh = logging.FileHandler(str(self.log_file), encoding="utf-8")
|
||||||
fh.setLevel(logging.DEBUG)
|
fh.setLevel(logging.DEBUG)
|
||||||
fh.setFormatter(logging.Formatter(
|
fh.setFormatter(logging.Formatter(
|
||||||
"%(asctime)s %(levelname)-5s %(name)s %(message)s",
|
"%(asctime)s %(levelname)-5s %(name)s %(message)s",
|
||||||
datefmt="%Y-%m-%d %H:%M:%S"))
|
datefmt="%Y-%m-%d %H:%M:%S"))
|
||||||
|
fh._tg_ws_proxy_runtime_handler = True
|
||||||
root.addHandler(fh)
|
root.addHandler(fh)
|
||||||
|
|
||||||
if not getattr(sys, "frozen", False):
|
if not getattr(sys, "frozen", False):
|
||||||
|
|
@ -93,6 +102,7 @@ class ProxyAppRuntime:
|
||||||
ch.setFormatter(logging.Formatter(
|
ch.setFormatter(logging.Formatter(
|
||||||
"%(asctime)s %(levelname)-5s %(message)s",
|
"%(asctime)s %(levelname)-5s %(message)s",
|
||||||
datefmt="%H:%M:%S"))
|
datefmt="%H:%M:%S"))
|
||||||
|
ch._tg_ws_proxy_runtime_handler = True
|
||||||
root.addHandler(ch)
|
root.addHandler(ch)
|
||||||
|
|
||||||
def prepare(self) -> dict:
|
def prepare(self) -> dict:
|
||||||
|
|
@ -168,3 +178,6 @@ class ProxyAppRuntime:
|
||||||
self.stop_proxy()
|
self.stop_proxy()
|
||||||
time.sleep(delay_seconds)
|
time.sleep(delay_seconds)
|
||||||
return self.start_proxy()
|
return self.start_proxy()
|
||||||
|
|
||||||
|
def is_proxy_running(self) -> bool:
|
||||||
|
return bool(self._proxy_thread and self._proxy_thread.is_alive())
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue