排查二次启动的问题

This commit is contained in:
asxalex 2025-07-18 06:25:27 +08:00
parent 867fb5ca72
commit 87bee05140
15 changed files with 497 additions and 174 deletions

Binary file not shown.

View File

@ -1,5 +1,6 @@
package com.jihe.punchnet package com.jihe.punchnet
import android.app.Application
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.net.VpnService import android.net.VpnService
@ -31,12 +32,21 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import androidx.navigation.compose.rememberNavController import androidx.navigation.compose.rememberNavController
import com.jihe.punchnet.data.ButtonState
import com.jihe.punchnet.data.ButtonViewModel
import com.jihe.punchnet.data.RouteViewModel import com.jihe.punchnet.data.RouteViewModel
import com.jihe.punchnet.data.ServiceViewModel import com.jihe.punchnet.data.ServiceViewModel
import com.jihe.punchnet.screen.MainApp import com.jihe.punchnet.screen.MainApp
import com.jihe.punchnet.ui.theme.PunchnetTheme import com.jihe.punchnet.ui.theme.PunchnetTheme
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
class PunchnetApp: Application() {
val buttonModel: ButtonViewModel by lazy {
ViewModelProvider.AndroidViewModelFactory.getInstance(this)
.create(ButtonViewModel::class.java)
}
}
class MainActivity : ComponentActivity() { class MainActivity : ComponentActivity() {
private val TAG = "MainActivity" private val TAG = "MainActivity"
@ -58,6 +68,18 @@ class MainActivity : ComponentActivity() {
} }
} }
/*
val buttonModel: ButtonViewModel by viewModels {
object: ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun<T: ViewModel> create(modelClass: Class<T>): T {
return ButtonViewModel(application) as T
}
}
}
*/
/* /*
fun prepareAndStartVPN() { fun prepareAndStartVPN() {
@ -97,9 +119,12 @@ class MainActivity : ComponentActivity() {
Log.d("DIR", "filesdir = ${this.filesDir}") Log.d("DIR", "filesdir = ${this.filesDir}")
val buttonModel = ButtonViewModel()
buttonModel.updateState(ButtonState.ButtonStopped)
setContent { setContent {
PunchnetTheme { PunchnetTheme {
MainApp(serviceModel, viewModel, rememberNavController()) MainApp(buttonModel, serviceModel, viewModel, rememberNavController())
/* /*
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding -> Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
StartStop( StartStop(

View File

@ -13,6 +13,9 @@ import android.util.Log
import android.widget.Toast import android.widget.Toast
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import com.jihe.punchnet.data.ButtonRepository
import com.jihe.punchnet.data.ButtonState
import com.jihe.punchnet.data.ButtonViewModel
import com.jihe.punchnet.data.RouteItem import com.jihe.punchnet.data.RouteItem
import com.jihe.punchnet.data.RouteViewModel import com.jihe.punchnet.data.RouteViewModel
import com.jihe.punchnet.sdlan.config.Arguments import com.jihe.punchnet.sdlan.config.Arguments
@ -42,6 +45,7 @@ import java.io.FileOutputStream
import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicBoolean
class PunchnetService : VpnService() , IfaceTun { class PunchnetService : VpnService() , IfaceTun {
private val TAG = "PunchnetService" private val TAG = "PunchnetService"
private var scope = CoroutineScope(Dispatchers.IO) private var scope = CoroutineScope(Dispatchers.IO)
@ -50,9 +54,7 @@ class PunchnetService : VpnService() , IfaceTun {
private var vpnDescriptor: ParcelFileDescriptor? = null private var vpnDescriptor: ParcelFileDescriptor? = null
private val serviceLock = Any() private val serviceLock = Any()
private val isRunning = AtomicBoolean(false) // private val isRunning = AtomicBoolean(false)
private val start_stop_channel = Channel<StartStopChanInfo>(100)
var input: FileInputStream? = null var input: FileInputStream? = null
var output: FileOutputStream? = null var output: FileOutputStream? = null
@ -129,17 +131,24 @@ class PunchnetService : VpnService() , IfaceTun {
} }
vpnDescriptor = tempVpnDescriptor vpnDescriptor = tempVpnDescriptor
.establish() .establish()?.apply {
input = FileInputStream(fileDescriptor)
input = FileInputStream(vpnDescriptor!!.fileDescriptor) output = FileOutputStream(fileDescriptor)
output = FileOutputStream(vpnDescriptor!!.fileDescriptor) }
ButtonRepository.updateState(ButtonState.ButtonStarted)
} }
private fun disconnect() { private fun disconnect() {
synchronized(serviceLock) { synchronized(serviceLock) {
if (ButtonRepository.buttonState.value != ButtonState.ButtonStarted) {
return
}
/*
if (!isRunning.get()) { if (!isRunning.get()) {
return return
} }
*/
ButtonRepository.updateState(ButtonState.ButtonStopping)
Toast.makeText(this, "stop vpn called", Toast.LENGTH_LONG).show() Toast.makeText(this, "stop vpn called", Toast.LENGTH_LONG).show()
input?.close() input?.close()
@ -149,15 +158,23 @@ class PunchnetService : VpnService() , IfaceTun {
stopForeground(STOP_FOREGROUND_REMOVE) stopForeground(STOP_FOREGROUND_REMOVE)
stopSelf() stopSelf()
isRunning.set(false) // isRunning.set(false)
} }
ButtonRepository.updateState(ButtonState.ButtonStopped)
} }
private fun connect(startArg: PunchnetServiceArgument?) { private fun connect(startArg: PunchnetServiceArgument?) {
synchronized(serviceLock) { synchronized(serviceLock) {
if (ButtonRepository.buttonState.value != ButtonState.ButtonStopped) {
return
}
/*
if (isRunning.get()) { if (isRunning.get()) {
return return
} }
*/
ButtonRepository.updateState(ButtonState.ButtonStarting)
val iface = this val iface = this
val server = "punchnet.aioe.tech" val server = "punchnet.aioe.tech"
@ -194,11 +211,13 @@ class PunchnetService : VpnService() , IfaceTun {
startForeground(1, notification) startForeground(1, notification)
scope.launch { scope.launch {
run_sdlan(scope, start_stop_channel, iface, argument, startArg) run_sdlan(scope, iface, argument, startArg)
} }
isRunning.set(true) // isRunning.set(true)
} }
// ButtonRepository.updateState(ButtonState.ButtonStarted)
// buttonViewModel.changeButtonState(ButtonState.ButtonStarted)
} }
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
@ -209,7 +228,6 @@ class PunchnetService : VpnService() , IfaceTun {
disconnect() disconnect()
START_STICKY START_STICKY
} else { } else {
scope = CoroutineScope(Dispatchers.IO) scope = CoroutineScope(Dispatchers.IO)
arpTable = ARPTable(scope) arpTable = ARPTable(scope)

View File

@ -0,0 +1,34 @@
package com.jihe.punchnet.data
import android.app.Application
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.ViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
object ButtonRepository {
private val _buttonState: MutableStateFlow<ButtonState> = MutableStateFlow(ButtonState.ButtonStopped)
val buttonState: StateFlow<ButtonState> = _buttonState
fun updateState(state: ButtonState) {
_buttonState.value = state
}
}
sealed class ButtonState(val text: String, val enabled: Boolean) {
object ButtonStarted: ButtonState("停止", true)
object ButtonStarting: ButtonState("启动中", false)
object ButtonStopped: ButtonState("启动", true)
object ButtonStopping: ButtonState("停止中", false)
}
class ButtonViewModel(): ViewModel() {
val buttonState = ButtonRepository.buttonState
fun updateState(state: ButtonState) {
ButtonRepository.updateState(state)
}
}

View File

@ -27,20 +27,24 @@ import kotlinx.coroutines.flow.forEach
import kotlinx.coroutines.flow.toList import kotlinx.coroutines.flow.toList
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
class ServiceViewModel(application: Application): AndroidViewModel(application) { class ServiceViewModel(application: Application): AndroidViewModel(application) {
/*
private val _isRunning = mutableStateOf(false) private val _isRunning = mutableStateOf(false)
val isRunning: State<Boolean> = _isRunning val isRunning: State<Boolean> = _isRunning
*/
fun startService(context: Context, routes: Array<RouteItem>) { fun startService(context: Context, routes: Array<RouteItem>, token: String) {
// should has the permission // should has the permission
startVpnService(context, routes) startVpnService(context, routes, token)
_isRunning.value = true // ButtonRepository.updateState(ButtonState.ButtonStarting)
// _isRunning.value = true
} }
private fun startVpnService(context: Context, routes: Array<RouteItem>) { private fun startVpnService(context: Context, routes: Array<RouteItem>, token: String) {
val intent = Intent(context, PunchnetService::class.java) val intent = Intent(context, PunchnetService::class.java)
intent.putExtra("argument", PunchnetServiceArgument( intent.putExtra("argument", PunchnetServiceArgument(
"", token,
routes, routes,
)) ))
@ -59,7 +63,7 @@ class ServiceViewModel(application: Application): AndroidViewModel(application)
PunchnetService.ACTION_DISCONNECT }) PunchnetService.ACTION_DISCONNECT })
// stopService(Intent(this, PunchnetService::class.java)) // stopService(Intent(this, PunchnetService::class.java))
Log.d("STOPPED PUNCHNET", "stopping PUNCHNET") Log.d("STOPPED PUNCHNET", "stopping PUNCHNET")
_isRunning.value = false // ButtonRepository.updateState(ButtonState.ButtonStopping)
// Toast.makeText(this, "VPN service stopped", Toast.LENGTH_SHORT).show() // Toast.makeText(this, "VPN service stopped", Toast.LENGTH_SHORT).show()
} }
} }

View File

@ -1,8 +1,48 @@
package com.jihe.punchnet.helper package com.jihe.punchnet.helper
import android.app.Activity
import android.content.Context
import com.jihe.punchnet.data.RouteItem import com.jihe.punchnet.data.RouteItem
import com.jihe.punchnet.sdlan.network.maskDigitToInt import com.jihe.punchnet.sdlan.network.maskDigitToInt
const val PreferenceRepositoryName = "pref"
sealed class PreferenceName(val name: String) {
object PreferenceToken: PreferenceName("token")
}
fun getPreferenceString(context: Context, name: PreferenceName): String? {
val preference = context.getSharedPreferences(PreferenceRepositoryName, Activity.MODE_PRIVATE)
try {
return preference.getString(name.name, "")
} catch (e: Exception) {
return null
}
}
fun getPreferenceInt(context: Context, name: PreferenceName): Int? {
val preference = context.getSharedPreferences(PreferenceRepositoryName, Activity.MODE_PRIVATE)
try {
return preference.getInt(name.name, 0)
} catch (e: Exception) {
return null
}
}
fun setPreferenceString(context: Context, name: PreferenceName, value: String) {
val preference = context.getSharedPreferences(PreferenceRepositoryName, Activity.MODE_PRIVATE)
val editor = preference.edit()
editor.putString(name.name, value)
editor.apply()
}
fun setPreferenceInt(context: Context, name: PreferenceName, value: Int) {
val preference = context.getSharedPreferences(PreferenceRepositoryName, Activity.MODE_PRIVATE)
val editor = preference.edit()
editor.putInt(name.name, value)
editor.apply()
}
sealed class Screen(val route: String) { sealed class Screen(val route: String) {
object ScreenRoutes: Screen("routes") object ScreenRoutes: Screen("routes")
object ScreenMain: Screen("main") object ScreenMain: Screen("main")

View File

@ -17,6 +17,7 @@ import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
@ -34,29 +35,34 @@ import androidx.navigation.compose.composable
import androidx.navigation.navArgument import androidx.navigation.navArgument
import com.jihe.punchnet.R import com.jihe.punchnet.R
import com.jihe.punchnet.RouteInfo import com.jihe.punchnet.RouteInfo
import com.jihe.punchnet.data.ButtonRepository
import com.jihe.punchnet.data.ButtonState
import com.jihe.punchnet.data.ButtonViewModel
import com.jihe.punchnet.data.RouteViewModel import com.jihe.punchnet.data.RouteViewModel
import com.jihe.punchnet.data.ServiceViewModel import com.jihe.punchnet.data.ServiceViewModel
@Composable @Composable
fun AppNavHost2( fun AppNavHost2(
buttonViewModel: ButtonViewModel,
serviceViewModel: ServiceViewModel, serviceViewModel: ServiceViewModel,
routeViewModel: RouteViewModel = viewModel(), routeViewModel: RouteViewModel = viewModel(),
navController: NavHostController, navController: NavHostController,
paddingValues: PaddingValues paddingValues: PaddingValues
) { ) {
NavHost( NavHost(
navController = navController, navController = navController,
startDestination = Screen.MainScreen.route, startDestination = Screen.MainScreen.route,
modifier = Modifier.padding(paddingValues) modifier = Modifier.padding(paddingValues)
) { ) {
composable(Screen.MainScreen.route) { composable(Screen.MainScreen.route) {
HomeScreen(serviceViewModel, routeViewModel) HomeScreen(buttonViewModel, serviceViewModel, routeViewModel)
} }
composable(Screen.RouteScreen.route) { composable(Screen.RouteScreen.route) {
CustomHeaderScreen( CustomHeaderScreen(
"路由设置" "路由设置"
) { ) {
RouteScreen(!serviceViewModel.isRunning.value, routeViewModel) RouteScreen(ButtonRepository.buttonState.collectAsState().value != ButtonState.ButtonStarted, routeViewModel)
// LightScreen() // LightScreen()
} }
} }

View File

@ -1,5 +1,6 @@
package com.jihe.punchnet.screen package com.jihe.punchnet.screen
import android.app.Activity.MODE_PRIVATE
import android.app.Activity.RESULT_CANCELED import android.app.Activity.RESULT_CANCELED
import android.app.Activity.RESULT_OK import android.app.Activity.RESULT_OK
import android.content.Context import android.content.Context
@ -9,8 +10,15 @@ import android.os.Build
import android.widget.Toast import android.widget.Toast
import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.DrawableRes
import androidx.compose.foundation.Image import androidx.compose.foundation.Image
import androidx.compose.foundation.clickable
import androidx.compose.foundation.indication
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
@ -20,13 +28,22 @@ import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Settings import androidx.compose.material.icons.filled.Settings
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState import androidx.compose.runtime.MutableState
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
@ -38,17 +55,116 @@ import com.jihe.punchnet.PunchnetService
import com.jihe.punchnet.PunchnetServiceArgument import com.jihe.punchnet.PunchnetServiceArgument
import com.jihe.punchnet.R import com.jihe.punchnet.R
import com.jihe.punchnet.RouteInfo import com.jihe.punchnet.RouteInfo
import com.jihe.punchnet.data.ButtonRepository
import com.jihe.punchnet.data.ButtonState
import com.jihe.punchnet.data.ButtonViewModel
import com.jihe.punchnet.data.RouteViewModel import com.jihe.punchnet.data.RouteViewModel
import com.jihe.punchnet.data.ServiceViewModel import com.jihe.punchnet.data.ServiceViewModel
import com.jihe.punchnet.helper.PreferenceName
import com.jihe.punchnet.helper.getPreferenceString
import com.jihe.punchnet.helper.setPreferenceString
import kotlin.math.exp
data class HomeDropDownInfo(
@DrawableRes val icon: Int,
val name: String,
val callback: (()->Unit)? = null,
)
@Composable
fun HomeDropdownMenu(
showTokenDialog: MutableState<Boolean>,
) {
var expanded by remember { mutableStateOf(false) }
// var showTokenDialog by remember { mutableStateOf(false) }
// val showSettingDialog by remember { mutableStateOf(false) }
val context = LocalContext.current
val homeItems = arrayOf(
HomeDropDownInfo(
R.drawable.tag,
"修改token",
{
showTokenDialog.value = true
}
),
/*
HomeDropDownInfo(
R.drawable.preferences,
"修改云端配置"
)
*/
)
Box() {
IconButton(
interactionSource = remember { MutableInteractionSource() },
modifier = Modifier.indication(
interactionSource = remember { MutableInteractionSource() },
indication = null,
),
onClick = {
expanded = true
}
) {
Icon(
Icons.Default.Settings,
contentDescription = "settings",
)
}
DropdownMenu(
expanded = expanded,
onDismissRequest = {expanded = !expanded}
) {
homeItems.forEach { item ->
DropdownMenuItem(
onClick = {
expanded = false
item.callback?.invoke()
},
text = {
Row (
verticalAlignment = Alignment.CenterVertically
){
Icon(
painter = painterResource(item.icon),
modifier = Modifier.size(24.dp),
contentDescription = null,
tint = MaterialTheme.colorScheme.onBackground
)
Spacer(
modifier = Modifier.padding(start = 8.dp)
)
Text(text=item.name)
}
}
)
}
}
}
}
@Composable @Composable
fun HomeScreen( fun HomeScreen(
buttonViewModel: ButtonViewModel,
serviceViewModel: ServiceViewModel, serviceViewModel: ServiceViewModel,
routeViewModel: RouteViewModel, routeViewModel: RouteViewModel,
// started: MutableState<Boolean>, // started: MutableState<Boolean>,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
val context = LocalContext.current val context = LocalContext.current
val tkPref = getPreferenceString(context, PreferenceName.PreferenceToken)
var tokenvalue by remember {mutableStateOf(tkPref?:"")}
val allroutes = routeViewModel.allRoutes.collectAsState(emptyList()) val allroutes = routeViewModel.allRoutes.collectAsState(emptyList())
val vpnLauncher = rememberLauncherForActivityResult( val vpnLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.StartActivityForResult() contract = ActivityResultContracts.StartActivityForResult()
@ -57,7 +173,9 @@ fun HomeScreen(
RESULT_OK -> { RESULT_OK -> {
// serviceViewModel.startService(context) // serviceViewModel.startService(context)
// serviceViewModel.setVPNPermission(true) // serviceViewModel.setVPNPermission(true)
serviceViewModel.startService(context, allroutes.value.toTypedArray()) val tk = getPreferenceString(context, PreferenceName.PreferenceToken)?:""
println("now token value2 = '${tokenvalue}'")
serviceViewModel.startService(context, allroutes.value.toTypedArray(), tk)
} }
RESULT_CANCELED -> { RESULT_CANCELED -> {
Toast.makeText(context, "vpn permission denied", Toast.LENGTH_SHORT).show() Toast.makeText(context, "vpn permission denied", Toast.LENGTH_SHORT).show()
@ -65,27 +183,54 @@ fun HomeScreen(
} }
} }
val buttonState = buttonViewModel.buttonState.collectAsState()
val showTokenDialog = remember { mutableStateOf(false) }
CustomHeaderScreen( CustomHeaderScreen(
"", "",
onBack = null, onBack = null,
onMenu = { onMenu = {
Icon( HomeDropdownMenu(showTokenDialog)
Icons.Default.Settings,
contentDescription = "settings",
modifier = Modifier.fillMaxHeight()
)
// NavListMenu(navController, dbdao)
/*
navController.navigate(
Screen.ActionDetail.route.replace(
"{action_name}",
""
)
)
*/
} }
) { ) {
if (showTokenDialog.value) {
AlertDialog(
onDismissRequest = {showTokenDialog.value=false},
confirmButton = {
Button(
onClick = {
setPreferenceString(context, PreferenceName.PreferenceToken, tokenvalue)
println("now token value = '${tokenvalue}'")
showTokenDialog.value = false
}
) {
Text("确定")
}
},
title = {
Text("修改token")
},
text = {
TextField(
value = tokenvalue,
onValueChange = { newValue ->
tokenvalue = newValue.trim()
},
leadingIcon = {
Icon(
painter = painterResource(R.drawable.tag),
contentDescription = "token",
modifier = Modifier.size(24.dp)
)
},
)
}
)
}
Column( Column(
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth()
.padding(top=50.dp), .padding(top=50.dp),
@ -114,30 +259,38 @@ fun HomeScreen(
Button( Button(
onClick = { onClick = {
if (serviceViewModel.isRunning.value) { if (buttonViewModel.buttonState.value == ButtonState.ButtonStarted) {
// if (serviceViewModel.isRunning.value) {
// if is running, should stop service // if is running, should stop service
serviceViewModel.stopVpnService(context) serviceViewModel.stopVpnService(context)
} else { } else if (buttonViewModel.buttonState.value == ButtonState.ButtonStopped){
val intent = VpnService.prepare(context) val intent = VpnService.prepare(context)
if (intent != null) { if (intent != null) {
vpnLauncher.launch(intent) vpnLauncher.launch(intent)
} else { } else {
serviceViewModel.startService(context, allroutes.value.toTypedArray()) val tk = getPreferenceString(context, PreferenceName.PreferenceToken)?:""
println("now token value1 = '${tokenvalue}'")
serviceViewModel.startService(context, allroutes.value.toTypedArray(), tk)
} }
} }
// started.value = !started.value // started.value = !started.value
}, },
enabled = buttonState.value.enabled,
shape = RoundedCornerShape(10.dp), shape = RoundedCornerShape(10.dp),
modifier = Modifier.padding(top=48.dp) modifier = Modifier.padding(top=48.dp)
.width(120.dp) .width(120.dp)
.height(40.dp) .height(40.dp)
) { ) {
Text( Text(
buttonState.value.text
// buttonViewModel.buttonText.value
/*
if (serviceViewModel.isRunning.value) { if (serviceViewModel.isRunning.value) {
"停止" "停止"
} else { } else {
"启动" "启动"
} }
*/
) )
} }
} }

View File

@ -20,6 +20,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavHostController import androidx.navigation.NavHostController
import com.jihe.punchnet.data.ButtonViewModel
import com.jihe.punchnet.data.RouteViewModel import com.jihe.punchnet.data.RouteViewModel
import com.jihe.punchnet.data.ServiceViewModel import com.jihe.punchnet.data.ServiceViewModel
@ -37,6 +38,7 @@ val bottomNavItems = listOf(
@Composable @Composable
fun MainApp( fun MainApp(
buttonViewModel: ButtonViewModel = viewModel(),
serviceViewModel: ServiceViewModel = viewModel(), serviceViewModel: ServiceViewModel = viewModel(),
routeModel: RouteViewModel = viewModel(), routeModel: RouteViewModel = viewModel(),
navController: NavHostController, navController: NavHostController,
@ -54,7 +56,7 @@ fun MainApp(
modifier = Modifier.padding(paddingValues).size(200.dp) modifier = Modifier.padding(paddingValues).size(200.dp)
) )
*/ */
AppNavHost2(serviceViewModel, routeModel, navController, paddingValues) AppNavHost2(buttonViewModel, serviceViewModel, routeModel, navController, paddingValues)
} }
} }

View File

@ -2,6 +2,8 @@ package com.jihe.punchnet.sdlan.network
import android.util.Log import android.util.Log
import com.jihe.punchnet.PunchnetServiceArgument import com.jihe.punchnet.PunchnetServiceArgument
import com.jihe.punchnet.data.ButtonRepository
import com.jihe.punchnet.data.ButtonState
import com.jihe.punchnet.protobuf.PunchProto.SDLDevAddr import com.jihe.punchnet.protobuf.PunchProto.SDLDevAddr
import com.jihe.punchnet.protobuf.PunchProto.SDLRegisterSuper import com.jihe.punchnet.protobuf.PunchProto.SDLRegisterSuper
import com.jihe.punchnet.protobuf.PunchProto.SDLRegisterSuperAck import com.jihe.punchnet.protobuf.PunchProto.SDLRegisterSuperAck
@ -18,9 +20,12 @@ import com.jihe.punchnet.sdlan.utils.UniqueNodeID
import com.jihe.punchnet.sdlan.utils.ipToString import com.jihe.punchnet.sdlan.utils.ipToString
import com.jihe.punchnet.sdlan.utils.macToString import com.jihe.punchnet.sdlan.utils.macToString
import com.jihe.punchnet.sdlan.utils.parseScoketAddressV4FromString import com.jihe.punchnet.sdlan.utils.parseScoketAddressV4FromString
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.async import kotlinx.coroutines.async
import kotlinx.coroutines.cancel
import kotlinx.coroutines.cancelAndJoin import kotlinx.coroutines.cancelAndJoin
import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.channels.ReceiveChannel import kotlinx.coroutines.channels.ReceiveChannel
@ -91,9 +96,11 @@ suspend fun onMessage(scope: CoroutineScope, data: SDLanTCP) {
when(nakcode) { when(nakcode) {
NakMsgCode.InvalidToken, NakMsgCode.NodeDisabled-> { NakMsgCode.InvalidToken, NakMsgCode.NodeDisabled-> {
node.startStopChannel.send(StartStopChanInfo(StartStopFlag.IsStop, null)) node.startStopChannel.send(StartStopChanInfo(StartStopFlag.IsStop, null))
exitProcess(-1) ButtonRepository.updateState(ButtonState.ButtonStopped)
scope.cancel()
} }
else -> { else -> {
ButtonRepository.updateState(ButtonState.ButtonStopped)
node.startStopChannel.send(StartStopChanInfo(StartStopFlag.IsStop, null)) node.startStopChannel.send(StartStopChanInfo(StartStopFlag.IsStop, null))
} }
} }
@ -131,10 +138,9 @@ suspend fun onMessage(scope: CoroutineScope, data: SDLanTCP) {
println("error packet type: ${data.packetType.toUByte()}") println("error packet type: ${data.packetType.toUByte()}")
} }
} }
} }
suspend fun run_sdlan(scope: CoroutineScope, start_stop_channel: Channel<StartStopChanInfo>, iface: Iface, argument: Arguments, routeinfo: PunchnetServiceArgument?) { suspend fun run_sdlan(scope: CoroutineScope, iface: Iface, argument: Arguments, routeinfo: PunchnetServiceArgument?) {
UniqueNodeID.setBaseDir(argument.baseDir) UniqueNodeID.setBaseDir(argument.baseDir)
val edgeUUID = UniqueNodeID.getUUID() val edgeUUID = UniqueNodeID.getUUID()
val config = parseConfig(edgeUUID, argument) val config = parseConfig(edgeUUID, argument)
@ -147,7 +153,7 @@ suspend fun run_sdlan(scope: CoroutineScope, start_stop_channel: Channel<StartSt
val toSocket = Channel<ByteArray>(100) val toSocket = Channel<ByteArray>(100)
// val start_stop_channel = Channel<StartStopChanInfo>(100) val start_stop_channel = Channel<StartStopChanInfo>(100)
initEdge(scope, iface, argument.token, config, toSocket, start_stop_channel) initEdge(scope, iface, argument.token, config, toSocket, start_stop_channel)
val tcp = argument.tcp.split(":") val tcp = argument.tcp.split(":")
@ -156,7 +162,6 @@ suspend fun run_sdlan(scope: CoroutineScope, start_stop_channel: Channel<StartSt
val node = Node.getInstance() val node = Node.getInstance()
val before = ubyteArrayOf(126u,162u,25u,63u,148u,147u,198u,41u,69u,165u,149u,101u,153u,82u,190u,21u,48u,120u,26u,64u,142u,103u,159u,60u,47u,129u,176u,17u,232u,210u,36u,56u) val before = ubyteArrayOf(126u,162u,25u,63u,148u,147u,198u,41u,69u,165u,149u,101u,153u,82u,190u,21u,48u,120u,26u,64u,142u,103u,159u,60u,47u,129u,176u,17u,232u,210u,36u,56u)
val encrypted = node.rsa.encrypt(before.toByteArray(), use_private_key = false) val encrypted = node.rsa.encrypt(before.toByteArray(), use_private_key = false)
// println("encrypted: ${encrypted.contentToString()}") // println("encrypted: ${encrypted.contentToString()}")
@ -227,6 +232,8 @@ suspend fun run_sdlan(scope: CoroutineScope, start_stop_channel: Channel<StartSt
} catch (e: Exception) { } catch (e: Exception) {
println("iface read is cancelled") println("iface read is cancelled")
node.iface?.close() node.iface?.close()
node.udpSockV4.close()
node.udpSockMulticast?.close()
} }
} }
/* /*
@ -253,28 +260,26 @@ suspend fun run_sdlan(scope: CoroutineScope, start_stop_channel: Channel<StartSt
// CoroutineScope(Dispatchers.IO).async { // CoroutineScope(Dispatchers.IO).async {
scope.launch { scope.launch {
try { initTCPConn(
initTCPConn( scope,
scope, tcp[0], tcp[1].toInt(),
tcp[0], tcp[1].toInt(), start_stop_channel,
start_stop_channel, AtomicLong(now),
AtomicLong(now), AtomicBoolean(false),
AtomicBoolean(false), toSocket as ReceiveChannel<ByteArray>,
toSocket as ReceiveChannel<ByteArray>, onConnected,
onConnected, ::onMessage,
::onMessage, suspend {
suspend { node.aes.setSecret(null)
node.aes.setSecret(null) },
}, null,
null, )
)
} finally {
println("initTCPConn is cancelled")
}
} }
println("sending start")
start_stop_channel.send(StartStopChanInfo(StartStopFlag.IsStart, null)) start_stop_channel.send(StartStopChanInfo(StartStopFlag.IsStart, null))
println("sent start")
val cancel = Channel<Boolean>(100) val cancel = Channel<Boolean>(100)
runEdgeLoop(scope, node, cancel) runEdgeLoop(scope, node, cancel)
@ -295,15 +300,22 @@ suspend fun runEdgeLoop(scope: CoroutineScope, node: Node, cancel: ReceiveChanne
suspend fun loopSocketV4(scope: CoroutineScope, node: Node, sock: SDLanSocket, cancel: ReceiveChannel<Boolean>) { suspend fun loopSocketV4(scope: CoroutineScope, node: Node, sock: SDLanSocket, cancel: ReceiveChannel<Boolean>) {
val job_stun_request = scope.async { val job_stun_request = scope.async {
while(true) { try {
delay(10_000) while(true) {
node.sendStunRequest() delay(10_000)
node.sendStunRequest()
}
} catch (e: Exception) {
} }
} }
val job_handle_packet = scope.async { val job_handle_packet = scope.async {
while(true) { try {
readAndParsePacket(scope, node, sock) while(true) {
readAndParsePacket(scope, node, sock)
}
} catch (e: Exception) {
sock.close()
} }
} }
@ -435,15 +447,17 @@ suspend fun initTCPConn(
onDisconnected: suspend ()->Unit, onDisconnected: suspend ()->Unit,
connectingChan: SendChannel<ConnectingState>? connectingChan: SendChannel<ConnectingState>?
) { ) {
val started = AtomicBoolean(false) val started = AtomicBoolean(false)
// var started: Boolean = false // var started: Boolean = false
var startPktID: Int? = null var startPktID: Int? = null
val node = Node.getInstance()
while(true) { while (true) {
connectingChan?.send(ConnectingState.NotConnected) connectingChan?.send(ConnectingState.NotConnected)
if (!started.get()) { if (!started.get()) {
while(true) { while (true) {
println("waiting for start_stop") println("waiting for start_stop")
val startStopInfo = start_stop.receive() val startStopInfo = start_stop.receive()
if (startStopInfo.flag == StartStopFlag.IsStart) { if (startStopInfo.flag == StartStopFlag.IsStart) {
@ -459,7 +473,7 @@ suspend fun initTCPConn(
} }
connectingChan?.send(ConnectingState.Connecting) connectingChan?.send(ConnectingState.Connecting)
TerminalLogger.debugf {"try connecting tcp..."} TerminalLogger.debugf { "try connecting tcp..." }
val socket: Socket val socket: Socket
try { try {
@ -467,118 +481,130 @@ suspend fun initTCPConn(
TerminalLogger.debugf { "connecting to $tcpHost:$tcpPort" } TerminalLogger.debugf { "connecting to $tcpHost:$tcpPort" }
socket = Socket(tcpHost, tcpPort) socket = Socket(tcpHost, tcpPort)
} }
}catch(e: CancellationException) {
withContext(NonCancellable) {
node.iface?.close()
}
return
} catch (e: Exception) { } catch (e: Exception) {
TerminalLogger.errorf { "failed to connect to ${tcpHost}:${tcpPort}: $e"} TerminalLogger.errorf { "failed to connect to ${tcpHost}:${tcpPort}: $e" }
delay(3000) delay(3000)
continue continue
} }
val node = Node.getInstance() try {
val outIP = ByteBuffer.wrap(socket.localAddress.address).getInt() val node = Node.getInstance()
node.outerIPV4.set(outIP) val outIP = ByteBuffer.wrap(socket.localAddress.address).getInt()
node.outerIPV4.set(outIP)
// val job_read_packet = CoroutineScope(Dispatchers.IO).async { // val job_read_packet = CoroutineScope(Dispatchers.IO).async {
val job_read_packet = scope.async { val job_read_packet = scope.async {
val input = DataInputStream(socket.getInputStream()) val input = DataInputStream(socket.getInputStream())
try {
println("job read packet starts")
while(true) {
val tcpPacket = readPacket(input)
if (tcpPacket == null) {
TerminalLogger.errorf {"tcp Packet is null"}
break
}
onMessage(scope, tcpPacket)
}
} finally {
TerminalLogger.errorf {"input closing"}
input.close()
}
}
// val job_write_to_packet = CoroutineScope(Dispatchers.IO).async {
val job_write_to_packet = scope.async {
val output = DataOutputStream(socket.getOutputStream())
try {
TerminalLogger.debugf {"job write to packet starts"}
while(true) {
try {
val msg = toSocket.receive()
TerminalLogger.debugf{"received message"}
output.write(msg)
} catch (e: Exception) {
output.close()
TerminalLogger.errorf {"failed to receive message to tcp: $e"}
break
}
}
} finally {
output.close()
}
}
// val job_check_pong = CoroutineScope(Dispatchers.IO).async {
val job_check_pong = scope.async {
println("job check pong starts")
while(true) {
delay(10_000)
val now = System.currentTimeMillis()/1000
if (connected.get() && (now-pong_time.get()>SDLanConfig.TCP_PING_TIME*2)) {
TerminalLogger.errorf {"tcp pong check expired"}
break
}
}
}
// val job_check_stop = CoroutineScope(Dispatchers.IO).async {
val job_check_stop = scope.async {
println("job check stop starts")
while(true) {
try { try {
val v = start_stop.receive() println("job read packet starts")
if (v.flag == StartStopFlag.IsStop) { while (true) {
started.set(false) val tcpPacket = readPacket(input)
if (tcpPacket == null) {
TerminalLogger.errorf { "tcp Packet is null" }
break
}
onMessage(scope, tcpPacket)
}
} finally {
input.close()
TerminalLogger.errorf { "input closing" }
}
}
// val job_write_to_packet = CoroutineScope(Dispatchers.IO).async {
val job_write_to_packet = scope.async {
val output = DataOutputStream(socket.getOutputStream())
try {
TerminalLogger.debugf { "job write to packet starts" }
while (true) {
try {
val msg = toSocket.receive()
TerminalLogger.debugf { "received message" }
output.write(msg)
} catch (e: Exception) {
output.close()
TerminalLogger.errorf { "failed to receive message to tcp: $e" }
break
}
}
} finally {
output.close()
}
}
// val job_check_pong = CoroutineScope(Dispatchers.IO).async {
val job_check_pong = scope.async {
println("job check pong starts")
while (true) {
delay(10_000)
val now = System.currentTimeMillis() / 1000
if (connected.get() && (now - pong_time.get() > SDLanConfig.TCP_PING_TIME * 2)) {
TerminalLogger.errorf { "tcp pong check expired" }
break break
} }
} catch(e: Exception) {
started.set(false)
break
} }
}
// val job_check_stop = CoroutineScope(Dispatchers.IO).async {
val job_check_stop = scope.async {
println("job check stop starts")
while (true) {
try {
val v = start_stop.receive()
if (v.flag == StartStopFlag.IsStop) {
started.set(false)
break
}
} catch (e: Exception) {
// started.set(false)
break
}
}
}
TerminalLogger.debugf { "connected" }
onConnected(socket, startPktID)
connectingChan?.send(ConnectingState.Connected)
var cancelled: Boolean = false
select<Unit> {
job_read_packet.onAwait() {
println("job read packet exited")
}
job_write_to_packet.onAwait() {
println("job write to packet exited")
}
job_check_pong.onAwait() {
println("job check pong exited")
}
job_check_stop.onAwait() {
println("job check stop exited")
}
}
println("m1")
job_read_packet.cancelAndJoin()
println("m2")
job_write_to_packet.cancelAndJoin()
println("m3")
job_check_pong.cancelAndJoin()
println("m4")
job_check_stop.cancelAndJoin()
println("m5")
delay(1000)
} catch (e: Exception) {
withContext(NonCancellable) {
node.iface?.close()
socket.close()
} }
} }
TerminalLogger.debugf { "connected" }
onConnected(socket, startPktID)
connectingChan?.send(ConnectingState.Connected)
var cancelled: Boolean = false
select<Unit> {
job_read_packet.onAwait() {
println("job read packet exited")
}
job_write_to_packet.onAwait() {
println("job write to packet exited")
}
job_check_pong.onAwait() {
println("job check pong exited")
}
job_check_stop.onAwait() {
println("job check stop exited")
}
}
println("m1")
job_read_packet.cancelAndJoin()
println("m2")
job_write_to_packet.cancelAndJoin()
println("m3")
job_check_pong.cancelAndJoin()
println("m4")
job_check_stop.cancelAndJoin()
println("m5")
delay(1000)
} }
} }

View File

@ -23,8 +23,6 @@ class SDLanSocket(val scope: CoroutineScope, val addr: String, val port: Int, va
sock sock
} }
private var job: Job? = null
fun loop(): ReceiveChannel<DatagramPacket> { fun loop(): ReceiveChannel<DatagramPacket> {
val channel = Channel<DatagramPacket>(100) val channel = Channel<DatagramPacket>(100)
scope.launch { scope.launch {
@ -44,7 +42,7 @@ class SDLanSocket(val scope: CoroutineScope, val addr: String, val port: Int, va
connection.send(packet) connection.send(packet)
} }
} catch(e: Exception) { } catch(e: Exception) {
TerminalLogger.errorf {"Failed to send to: $e"} TerminalLogger.errorf {"Failed to send to: $e, localport: ${connection.localPort}"}
} }
} }
@ -59,7 +57,6 @@ class SDLanSocket(val scope: CoroutineScope, val addr: String, val port: Int, va
} }
suspend fun close() { suspend fun close() {
job?.cancelAndJoin()
connection.close() connection.close()
} }
} }

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="200dp"
android:height="200dp"
android:viewportWidth="1024"
android:viewportHeight="1024">
<path
android:fillColor="#FF000000"
android:pathData="M802.1,102.4L222.2,102.4C156.3,102.4 102.4,156 102.4,221.9v579.9A120.1,120.1 0,0 0,222.2 921.6L802.1,921.6c65.9,0 119.5,-53.9 119.5,-119.8L921.6,221.9C921.6,156 868,102.4 802.1,102.4zM751.3,767.7h-297v11.6c0,19.1 -15.4,34.1 -34.1,34.1s-34.1,-15 -34.1,-34.1v-11.6h-112.6c-19.1,0 -34.1,-15.4 -34.1,-34.1s15,-34.1 34.1,-34.1h112.6v-11.6c0,-18.8 15.4,-34.1 34.1,-34.1s34.1,15.4 34.1,34.1v11.6h297c18.8,0 34.1,15.4 34.1,34.1s-15.4,34.1 -34.1,34.1zM751.3,543.4h-123.9v11.6c0,18.8 -15.4,34.1 -34.1,34.1s-34.1,-15.4 -34.1,-34.1v-11.6L273.4,543.4c-19.1,0 -34.1,-15.4 -34.1,-34.1 0,-19.1 15,-34.1 34.1,-34.1h285.7v-11.9c0,-18.8 15.4,-34.1 34.1,-34.1s34.1,15.4 34.1,34.1v11.9h123.9c18.8,0 34.1,15 34.1,34.1 0,18.8 -15.4,34.1 -34.1,34.1zM751.3,323.9L405.2,323.9v6.8c0,18.8 -15.4,34.1 -34.1,34.1s-34.1,-15.4 -34.1,-34.1v-6.8L273.4,323.9c-19.1,0 -34.1,-15.4 -34.1,-34.1s15,-34.1 34.1,-34.1h63.5L336.9,238.9c0,-18.8 15.4,-34.1 34.1,-34.1s34.1,15.4 34.1,34.1v16.7h346.1c18.8,0 34.1,15.4 34.1,34.1s-15.4,34.1 -34.1,34.1z"/>
</vector>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="200dp"
android:height="200dp"
android:viewportWidth="1024"
android:viewportHeight="1024">
<path
android:pathData="M818.2,268.3c0.2,-34.9 -28.7,-64.3 -63.7,-64.6 -34.2,-0.3 -64.3,29.5 -64.3,64 0,34.3 28.9,63.6 63.3,64C788.2,332.1 818,302.9 818.2,268.3M128,640 L128,595.1c0,-0.4 1,-0.8 1,-1.1C132.3,570.2 144.4,551.5 161.2,534.8c125.6,-125.3 251,-250.8 376.5,-376.2C542.1,154.2 546.8,150 551.7,146.2 564,136.3 578.8,131.8 593.7,128l220.8,0c0.6,0.4 1.2,1 1.8,1.2 43.5,6.2 80.3,46.9 79.6,96.1 -0.8,63.2 -0.3,126.6 -0.1,189.9 0.1,26.6 -9.3,49.2 -28,67.9 -127.9,128 -255.7,256.1 -383.8,383.8 -39.2,39.1 -97.5,38.8 -136.8,-0.1 -63.6,-63.2 -126.7,-126.7 -190.1,-190 -11.9,-11.9 -20.6,-25.5 -25.3,-41.9C130.4,630.1 128,625.3 128,620.6"
android:fillColor="#2c2c2c"/>
</vector>