Skip to content

Commit

Permalink
feat: add service restart functionality and improve server status han…
Browse files Browse the repository at this point in the history
…dling

- Introduced RestartServiceView for managing the restart process of OpenClash services, including user confirmation and log display.
- Enhanced ServerViewModel to improve error messages and server status updates, ensuring clarity in connection issues.
- Added a button in ContentView to trigger the restart service view, improving user interaction.
- Made minor adjustments to existing code for better readability and consistency in error handling.
  • Loading branch information
bin64 committed Dec 19, 2024
1 parent decb023 commit 755f350
Show file tree
Hide file tree
Showing 3 changed files with 206 additions and 4 deletions.
8 changes: 4 additions & 4 deletions Clash Dash/ViewModels/ServerViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ struct VersionResponse: Codable {
}

// 添加一个结构体来表示启动状态
struct StartLogResponse: Codable {
public struct StartLogResponse: Codable {
let startlog: String
}

Expand Down Expand Up @@ -188,7 +188,7 @@ class ServerViewModel: NSObject, ObservableObject, URLSessionDelegate, URLSessio
case .secureConnectionFailed:
updateServerStatus(server, status: .error, message: "SSL/TLS 连接失败")
case .serverCertificateUntrusted:
updateServerStatus(server, status: .error, message: "证书不���信任")
updateServerStatus(server, status: .error, message: "证书不信任")
case .timedOut:
updateServerStatus(server, status: .error, message: "连接超时")
case .cannotConnectToHost:
Expand Down Expand Up @@ -262,7 +262,7 @@ class ServerViewModel: NSObject, ObservableObject, URLSessionDelegate, URLSessio
servers[index].isQuickLaunch = false
}

// 然后设置选中的服务器为快速���动
// 然后设置选中的服务器为快速启动
if let index = servers.firstIndex(where: { $0.id == server.id }) {
servers[index].isQuickLaunch = true
}
Expand Down Expand Up @@ -1073,7 +1073,7 @@ class ServerViewModel: NSObject, ObservableObject, URLSessionDelegate, URLSessio
guard let username = server.openWRTUsername,
let password = server.openWRTPassword else {
print("❌ 未找到认证信息")
throw NetworkError.unauthorized(message: "未设置 OpenWRT 用户名或密码")
throw NetworkError.unauthorized(message: "未设置 OpenWRT ��户名或密码")
}

print("🔑 获取认证令牌...")
Expand Down
22 changes: 22 additions & 0 deletions Clash Dash/Views/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,13 @@ struct ContentView: View {
} label: {
Label("覆写规则", systemImage: "list.bullet.rectangle")
}

Button {
impactFeedback.impactOccurred()
showRestartServiceView(for: server)
} label: {
Label("重启服务", systemImage: "arrow.clockwise.circle")
}
}
}
}
Expand Down Expand Up @@ -295,6 +302,21 @@ struct ContentView: View {
rootViewController.present(sheet, animated: true)
}
}

private func showRestartServiceView(for server: ClashServer) {
editingServer = nil // 清除编辑状态
let restartView = RestartServiceView(viewModel: viewModel, server: server)
let sheet = UIHostingController(rootView: restartView)

sheet.modalPresentationStyle = .formSheet
sheet.sheetPresentationController?.detents = [.medium(), .large()]
sheet.sheetPresentationController?.prefersGrabberVisible = true

if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
let rootViewController = windowScene.windows.first?.rootViewController {
rootViewController.present(sheet, animated: true)
}
}
}


Expand Down
180 changes: 180 additions & 0 deletions Clash Dash/Views/RestartServiceView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
import SwiftUI

struct RestartServiceView: View {
@Environment(\.dismiss) private var dismiss
@StateObject private var viewModel: ServerViewModel
let server: ClashServer

@State private var logs: [String] = []
@State private var isRestarting = false
@State private var error: Error?
@State private var showConfirmation = true
@State private var isRestartSuccessful = false

init(viewModel: ServerViewModel, server: ClashServer) {
_viewModel = StateObject(wrappedValue: viewModel)
self.server = server
}

private func logColor(_ log: String) -> Color {
if log.contains("警告") {
return .orange
} else if log.contains("错误") {
return .red
}else if log.contains("提示") {
return .yellow
} else if log.contains("成功") {
return .green
}
return .secondary
}

var body: some View {
NavigationStack {
ScrollViewReader { proxy in
ScrollView {
LazyVStack(alignment: .leading, spacing: 8) {
ForEach(logs.reversed(), id: \.self) { log in
Text(log)
.font(.system(.body, design: .monospaced))
.foregroundColor(logColor(log))
.textSelection(.enabled)
.padding(.horizontal)
.transition(.asymmetric(
insertion: .move(edge: .top).combined(with: .opacity),
removal: .opacity
))
}
}
.frame(maxWidth: .infinity, alignment: .leading)
.onChange(of: logs) { _ in
withAnimation(.easeInOut(duration: 0.3)) {
proxy.scrollTo(logs.first, anchor: .top)
}
}
}
}
.navigationTitle("重启服务")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .cancellationAction) {
Button("关闭") {
dismiss()
}
}

ToolbarItem(placement: .principal) {
if isRestartSuccessful {
Label("重启成功", systemImage: "checkmark.circle.fill")
.foregroundColor(.green)
}
}
}
}
.alert("确认重启", isPresented: $showConfirmation) {
Button("取消", role: .cancel) {
dismiss()
}
Button("确认重启", role: .destructive) {
Task {
try? await Task.sleep(nanoseconds: 1_000_000_000)
await restartService()
}
}
} message: {
Text("重启 OpenClash 服务将导致:\n\n1. 所有当前连接会被中断\n2. 服务在重启期间不可用\n\n是否确认重启?")
}
.alert("错误", isPresented: .constant(error != nil)) {
Button("确定") {
error = nil
}
} message: {
if let error = error {
Text(error.localizedDescription)
}
}
}

private func restartService() async {
isRestarting = true
isRestartSuccessful = false
logs.removeAll()

do {
// 1. 先发送重启命令
let stream = try await viewModel.restartOpenClash(server)

// 2. 开始轮询日志
let scheme = server.useSSL ? "https" : "http"
let baseURL = "\(scheme)://\(server.url):\(server.openWRTPort ?? "80")"

guard let username = server.openWRTUsername,
let password = server.openWRTPassword else {
throw NetworkError.unauthorized(message: "未设置 OpenWRT 用户名或密码")
}

// 获取认证令牌
let token = try await viewModel.getAuthToken(server, username: username, password: password)

// 3. 持续获取日志,直到服务完全启动或超时
var retryCount = 0
let maxRetries = 300 // 最多尝试300次,每次0.1秒

while retryCount < maxRetries {
let random = Int.random(in: 1...1000000000)
guard let logURL = URL(string: "\(baseURL)/cgi-bin/luci/admin/services/openclash/startlog?\(random)") else {
throw NetworkError.invalidURL
}

var logRequest = URLRequest(url: logURL)
logRequest.setValue("sysauth_http=\(token)", forHTTPHeaderField: "Cookie")

let (logData, _) = try await URLSession.shared.data(for: logRequest)
let logResponse = try JSONDecoder().decode(StartLogResponse.self, from: logData)

if !logResponse.startlog.isEmpty {
let newLogs = logResponse.startlog
.components(separatedBy: "\n")
.filter { !$0.isEmpty }

for log in newLogs {
if !logs.contains(log) {
withAnimation {
logs.append(log)
}

// 检查重启成功标记
if log.contains("第九步") || log.contains("第八步") || log.contains("启动成功") {
// 等待2秒后标记成功
try await Task.sleep(nanoseconds: 2_000_000_000)
isRestartSuccessful = true
isRestarting = false

// 再等待1秒后关闭sheet
try await Task.sleep(nanoseconds: 1_000_000_000)
await MainActor.run {
dismiss()
}
return
}
}
}
}

retryCount += 1
try await Task.sleep(nanoseconds: 100_000_000) // 等待0.1秒
}

// 如果超时,添加提示信息
withAnimation {
logs.append("⚠️ 获取日志超时,请检查服务状态")
}

} catch {
self.error = error
}

isRestarting = false
}
}

0 comments on commit 755f350

Please sign in to comment.