Tools View & Crash Report & OOM Report
This commit is contained in:
@@ -23,20 +23,21 @@ public struct AppView: View {
|
||||
|
||||
@State private var isLoading = true
|
||||
@State private var selectedLanguage: String?
|
||||
@State private var cacheSize: Int64 = 0
|
||||
@State private var cacheSizeText = ""
|
||||
|
||||
#if os(macOS)
|
||||
@State private var startAtLogin = false
|
||||
@Environment(\.showMenuBarExtra) private var showMenuBarExtra
|
||||
@Environment(\.menuBarExtraSpeedMode) private var menuBarExtraSpeedMode
|
||||
@State private var menuBarExtraInBackground = false
|
||||
@State private var systemExtensionInstalled = false
|
||||
@State private var helperStatusLoaded = false
|
||||
@State private var rootHelperRegistrationStatus: SMAppService.Status = .notRegistered
|
||||
@EnvironmentObject private var environments: ExtensionEnvironments
|
||||
@EnvironmentObject private var updateManager: UpdateManager
|
||||
@State private var updateTrack: UpdateTrack = .stable
|
||||
@State private var checkUpdateEnabled = false
|
||||
@State private var cacheSize: Int64 = 0
|
||||
@State private var cacheSizeText = ""
|
||||
#endif
|
||||
|
||||
@State private var alert: AlertState?
|
||||
@@ -96,34 +97,37 @@ public struct AppView: View {
|
||||
}
|
||||
}
|
||||
|
||||
if Variant.useSystemExtension {
|
||||
FormTextItem("Cache Size", cacheSizeText)
|
||||
if cacheSize > 0 {
|
||||
// Safe: System Extension's working directory is in its own container
|
||||
// (/var/root/Library/Containers/…), not under the app's cacheDirectory.
|
||||
FormButton(role: .destructive) {
|
||||
Task.detached {
|
||||
let cacheDir = FilePath.cacheDirectory
|
||||
if let contents = try? FileManager.default.contentsOfDirectory(
|
||||
at: cacheDir,
|
||||
includingPropertiesForKeys: nil
|
||||
) {
|
||||
for item in contents {
|
||||
try? FileManager.default.removeItem(at: item)
|
||||
}
|
||||
}
|
||||
await MainActor.run {
|
||||
cacheSize = 0
|
||||
cacheSizeText = ByteCountFormatter.string(fromByteCount: 0, countStyle: .file)
|
||||
#endif
|
||||
|
||||
FormTextItem("Cache Size", cacheSizeText)
|
||||
if cacheSize > 0 {
|
||||
FormButton(role: .destructive) {
|
||||
Task.detached {
|
||||
let cacheDir = FilePath.cacheDirectory
|
||||
let workingDir = FilePath.workingDirectory
|
||||
if let contents = try? FileManager.default.contentsOfDirectory(
|
||||
at: cacheDir,
|
||||
includingPropertiesForKeys: nil
|
||||
) {
|
||||
for item in contents {
|
||||
if item.lastPathComponent == workingDir.lastPathComponent {
|
||||
continue
|
||||
}
|
||||
try? FileManager.default.removeItem(at: item)
|
||||
}
|
||||
} label: {
|
||||
Label("Clear Cache", systemImage: "trash")
|
||||
.foregroundColor(.red)
|
||||
}
|
||||
await MainActor.run {
|
||||
cacheSize = 0
|
||||
cacheSizeText = ByteCountFormatter.string(fromByteCount: 0, countStyle: .file)
|
||||
}
|
||||
}
|
||||
} label: {
|
||||
Label("Clear Cache", systemImage: "trash")
|
||||
.foregroundColor(.red)
|
||||
}
|
||||
}
|
||||
|
||||
#if os(macOS)
|
||||
if Variant.useSystemExtension {
|
||||
Section("Update Settings") {
|
||||
Picker("Update Track", selection: $updateTrack) {
|
||||
@@ -203,19 +207,29 @@ public struct AppView: View {
|
||||
}
|
||||
|
||||
Section("System Extension") {
|
||||
FormButton {
|
||||
Task {
|
||||
await updateSystemExtension()
|
||||
if systemExtensionInstalled {
|
||||
FormButton {
|
||||
Task {
|
||||
await updateSystemExtension()
|
||||
}
|
||||
} label: {
|
||||
Label("Update", systemImage: "arrow.down.doc.fill")
|
||||
}
|
||||
} label: {
|
||||
Label("Update", systemImage: "arrow.down.doc.fill")
|
||||
}
|
||||
FormButton(role: .destructive) {
|
||||
Task {
|
||||
await uninstallSystemExtension()
|
||||
FormButton(role: .destructive) {
|
||||
Task {
|
||||
await uninstallSystemExtension()
|
||||
}
|
||||
} label: {
|
||||
Label("Uninstall", systemImage: "trash.fill").foregroundColor(.red)
|
||||
}
|
||||
} else {
|
||||
FormButton {
|
||||
Task {
|
||||
await installSystemExtension()
|
||||
}
|
||||
} label: {
|
||||
Label("Install", systemImage: "lock.doc.fill")
|
||||
}
|
||||
} label: {
|
||||
Label("Uninstall", systemImage: "trash.fill").foregroundColor(.red)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -289,6 +303,7 @@ public struct AppView: View {
|
||||
startAtLogin = SMAppService.mainApp.status == .enabled
|
||||
menuBarExtraInBackground = await SharedPreferences.menuBarExtraInBackground.get()
|
||||
if Variant.useSystemExtension {
|
||||
systemExtensionInstalled = await SystemExtension.isInstalled()
|
||||
let trackString = await SharedPreferences.updateTrack.get()
|
||||
updateTrack = UpdateTrack.resolved(from: trackString)
|
||||
checkUpdateEnabled = await SharedPreferences.checkUpdateEnabled.get()
|
||||
@@ -299,9 +314,9 @@ public struct AppView: View {
|
||||
if Variant.useSystemExtension {
|
||||
refreshHelperStatus()
|
||||
helperStatusLoaded = true
|
||||
refreshCacheSize()
|
||||
}
|
||||
#endif
|
||||
refreshCacheSize()
|
||||
}
|
||||
|
||||
private static func currentLanguage() -> String? {
|
||||
@@ -384,6 +399,20 @@ public struct AppView: View {
|
||||
}
|
||||
}
|
||||
|
||||
private func installSystemExtension() async {
|
||||
do {
|
||||
if let result = try await SystemExtension.install() {
|
||||
if result == .willCompleteAfterReboot {
|
||||
alert = AlertState(errorMessage: String(localized: "Need Reboot"))
|
||||
return
|
||||
}
|
||||
}
|
||||
systemExtensionInstalled = true
|
||||
} catch {
|
||||
alert = AlertState(action: "install system extension", error: error)
|
||||
}
|
||||
}
|
||||
|
||||
private func updateSystemExtension() async {
|
||||
do {
|
||||
if let result = try await SystemExtension.install(forceUpdate: true) {
|
||||
@@ -410,6 +439,7 @@ public struct AppView: View {
|
||||
if let result = try await SystemExtension.uninstall() {
|
||||
switch result {
|
||||
case .completed:
|
||||
systemExtensionInstalled = false
|
||||
alert = AlertState(
|
||||
title: String(localized: "Uninstall"),
|
||||
message: String(localized: "System Extension removed.")
|
||||
@@ -452,32 +482,34 @@ public struct AppView: View {
|
||||
NSWorkspace.shared.open(URL(fileURLWithPath: "/System/Applications/System Preferences.app"))
|
||||
}
|
||||
|
||||
private func refreshCacheSize() {
|
||||
Task.detached {
|
||||
let size = Self.calculateDirSize(FilePath.cacheDirectory)
|
||||
await MainActor.run {
|
||||
cacheSize = size
|
||||
cacheSizeText = ByteCountFormatter.string(fromByteCount: size, countStyle: .file)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static func calculateDirSize(_ dir: URL) -> Int64 {
|
||||
guard let enumerator = FileManager.default.enumerator(
|
||||
at: dir,
|
||||
includingPropertiesForKeys: [.fileSizeKey],
|
||||
options: [.skipsHiddenFiles]
|
||||
) else {
|
||||
return 0
|
||||
}
|
||||
var size: Int64 = 0
|
||||
for case let fileURL as URL in enumerator {
|
||||
if let fileSize = try? fileURL.resourceValues(forKeys: [.fileSizeKey]).fileSize {
|
||||
size += Int64(fileSize)
|
||||
}
|
||||
}
|
||||
return size
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
private func refreshCacheSize() {
|
||||
Task.detached {
|
||||
let total = Self.calculateDirSize(FilePath.cacheDirectory)
|
||||
let working = Self.calculateDirSize(FilePath.workingDirectory)
|
||||
let size = max(total - working, 0)
|
||||
await MainActor.run {
|
||||
cacheSize = size
|
||||
cacheSizeText = ByteCountFormatter.string(fromByteCount: size, countStyle: .file)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static func calculateDirSize(_ dir: URL) -> Int64 {
|
||||
guard let enumerator = FileManager.default.enumerator(
|
||||
at: dir,
|
||||
includingPropertiesForKeys: [.fileSizeKey],
|
||||
options: [.skipsHiddenFiles]
|
||||
) else {
|
||||
return 0
|
||||
}
|
||||
var size: Int64 = 0
|
||||
for case let fileURL as URL in enumerator {
|
||||
if let fileSize = try? fileURL.resourceValues(forKeys: [.fileSizeKey]).fileSize {
|
||||
size += Int64(fileSize)
|
||||
}
|
||||
}
|
||||
return size
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,10 +6,6 @@ struct PacketTunnelView: View {
|
||||
@State private var isLoading = true
|
||||
@State private var alert: AlertState?
|
||||
|
||||
#if !os(macOS)
|
||||
@State private var ignoreMemoryLimit = false
|
||||
#endif
|
||||
|
||||
@State private var includeAllNetworks = false
|
||||
@State private var excludeAPNs = false
|
||||
@State private var excludeCellularServices = false
|
||||
@@ -28,15 +24,6 @@ struct PacketTunnelView: View {
|
||||
}
|
||||
} else {
|
||||
FormView {
|
||||
#if !os(macOS)
|
||||
FormToggle("Ignore Memory Limit", """
|
||||
Do not enforce memory limits on sing-box. Will cause OOM on non-jailbroken devices.
|
||||
""", $ignoreMemoryLimit) { newValue in
|
||||
await SharedPreferences.ignoreMemoryLimit.set(newValue)
|
||||
await restartService()
|
||||
}
|
||||
#endif
|
||||
|
||||
#if !os(tvOS)
|
||||
FormToggle("includeAllNetworks", """
|
||||
If this property is true, the system routes network traffic through the tunnel except traffic for designated system services necessary for maintaining expected device functionality. You can exclude some types of traffic using the **excludeAPNs**, **excludeLocalNetworks**, and **excludeCellularServices** properties in combination with this property.
|
||||
@@ -135,9 +122,6 @@ struct PacketTunnelView: View {
|
||||
|
||||
@MainActor
|
||||
private func loadSettings() async {
|
||||
#if !os(macOS)
|
||||
ignoreMemoryLimit = await SharedPreferences.ignoreMemoryLimit.get()
|
||||
#endif
|
||||
#if !os(tvOS)
|
||||
includeAllNetworks = await SharedPreferences.includeAllNetworks.get()
|
||||
excludeLocalNetworks = await SharedPreferences.excludeLocalNetworks.get()
|
||||
|
||||
@@ -1,64 +0,0 @@
|
||||
import Foundation
|
||||
import Library
|
||||
import SwiftUI
|
||||
|
||||
@MainActor
|
||||
public struct ServiceLogView: View {
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
@StateObject private var viewModel = ServiceLogViewModel()
|
||||
|
||||
private let logFont = Font.system(.caption, design: .monospaced)
|
||||
|
||||
public init() {}
|
||||
|
||||
public var body: some View {
|
||||
Group {
|
||||
if viewModel.isLoading {
|
||||
ProgressView().onAppear {
|
||||
Task {
|
||||
await viewModel.loadContent()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if viewModel.isEmpty {
|
||||
Text("Empty content")
|
||||
} else {
|
||||
ScrollView {
|
||||
Text(viewModel.content)
|
||||
.font(logFont)
|
||||
.frame(maxWidth: .infinity, alignment: .topLeading)
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
}
|
||||
.toolbar {
|
||||
if !viewModel.isEmpty {
|
||||
#if !os(tvOS)
|
||||
ShareButtonCompat($viewModel.alert) {
|
||||
Label("Export", systemImage: "square.and.arrow.up.fill")
|
||||
} itemURL: {
|
||||
try await viewModel.generateShareFileAsync()
|
||||
}
|
||||
#endif
|
||||
Button(role: .destructive) {
|
||||
Task {
|
||||
await viewModel.deleteContent(dismiss: dismiss)
|
||||
}
|
||||
} label: {
|
||||
#if !os(tvOS)
|
||||
Label("Delete", systemImage: "trash.fill")
|
||||
#else
|
||||
Image(systemName: "trash.fill")
|
||||
.tint(.red)
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
.alert($viewModel.alert)
|
||||
.navigationTitle("Service Log")
|
||||
#if os(tvOS)
|
||||
.focusable()
|
||||
#endif
|
||||
}
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
import Foundation
|
||||
import Library
|
||||
import SwiftUI
|
||||
|
||||
@MainActor
|
||||
final class ServiceLogViewModel: BaseViewModel {
|
||||
@Published var content = ""
|
||||
|
||||
override init() {
|
||||
super.init()
|
||||
isLoading = true
|
||||
}
|
||||
|
||||
var isEmpty: Bool {
|
||||
content.isEmpty
|
||||
}
|
||||
|
||||
nonisolated func loadContent() async {
|
||||
let primaryLogURL = FilePath.cacheDirectory.appendingPathComponent("stderr.log")
|
||||
let secondaryLogURL = FilePath.cacheDirectory.appendingPathComponent("stderr.log.old")
|
||||
var content = await BlockingIO.run {
|
||||
if let primaryContent = try? String(contentsOf: primaryLogURL), !primaryContent.isEmpty {
|
||||
return primaryContent
|
||||
}
|
||||
return (try? String(contentsOf: secondaryLogURL)) ?? ""
|
||||
}
|
||||
#if DEBUG
|
||||
if content.isEmpty {
|
||||
content = "Empty content"
|
||||
}
|
||||
#endif
|
||||
if !content.isEmpty {
|
||||
var systemInfo = utsname()
|
||||
uname(&systemInfo)
|
||||
let machineMirror = Mirror(reflecting: systemInfo.machine)
|
||||
let machineName = machineMirror.children.reduce("") { identifier, element in
|
||||
guard let value = element.value as? Int8, value != 0 else { return identifier }
|
||||
return identifier + String(UnicodeScalar(UInt8(value)))
|
||||
}
|
||||
var deviceInfo = String("Machine: ") + machineName + "\n"
|
||||
#if os(iOS)
|
||||
await deviceInfo += String("System: ") + (UIDevice.current.systemName) + " " + (UIDevice.current.systemVersion) + "\n"
|
||||
#elseif os(macOS)
|
||||
deviceInfo += String("System: ") + "macOS " + ProcessInfo().operatingSystemVersionString + "\n"
|
||||
#endif
|
||||
content = deviceInfo + "\n" + content
|
||||
}
|
||||
await MainActor.run { [content] in
|
||||
self.content = content
|
||||
isLoading = false
|
||||
}
|
||||
}
|
||||
|
||||
nonisolated func deleteContent(dismiss: DismissAction) async {
|
||||
let primaryLogURL = FilePath.cacheDirectory.appendingPathComponent("stderr.log")
|
||||
let secondaryLogURL = FilePath.cacheDirectory.appendingPathComponent("stderr.log.old")
|
||||
await BlockingIO.run {
|
||||
try? FileManager.default.removeItem(at: primaryLogURL)
|
||||
try? FileManager.default.removeItem(at: secondaryLogURL)
|
||||
}
|
||||
await MainActor.run {
|
||||
dismiss()
|
||||
isLoading = true
|
||||
}
|
||||
}
|
||||
|
||||
func generateShareFile() throws -> URL {
|
||||
try content.generateShareFile(name: "service.log")
|
||||
}
|
||||
|
||||
func generateShareFileAsync() async throws -> URL {
|
||||
let content = content
|
||||
return try await BlockingIO.run {
|
||||
try content.generateShareFile(name: "service.log")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -149,14 +149,17 @@ public struct SettingView: View {
|
||||
}
|
||||
#endif
|
||||
|
||||
@StateObject private var viewModel = SettingViewModel()
|
||||
public init() {}
|
||||
public var body: some View {
|
||||
FormView {
|
||||
Section {
|
||||
ForEach([Tabs.app, Tabs.core, Tabs.packetTunnel, Tabs.onDemandRules, Tabs.profileOverride]) { it in
|
||||
it.navigationLink
|
||||
}
|
||||
Tabs.app.navigationLink
|
||||
Tabs.core.navigationLink
|
||||
#if !os(tvOS)
|
||||
Tabs.packetTunnel.navigationLink
|
||||
#endif
|
||||
Tabs.onDemandRules.navigationLink
|
||||
Tabs.profileOverride.navigationLink
|
||||
}
|
||||
#if !os(tvOS)
|
||||
Section("About") {
|
||||
@@ -193,25 +196,6 @@ public struct SettingView: View {
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
Section("Debug") {
|
||||
FormNavigationLink {
|
||||
ServiceLogView()
|
||||
} label: {
|
||||
Label("Service Log", systemImage: "doc.on.clipboard")
|
||||
}
|
||||
FormTextItem("Taiwan Flag Available", "touchid") {
|
||||
if viewModel.isLoading {
|
||||
Text("Loading...")
|
||||
.onAppear {
|
||||
Task.detached {
|
||||
await viewModel.checkTaiwanFlagAvailability()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Text(viewModel.taiwanFlagAvailable.toString())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#if os(macOS)
|
||||
.formNavigationDestination(for: SettingsPage.self) { page in
|
||||
|
||||
Reference in New Issue
Block a user