Tools View & Crash Report & OOM Report
This commit is contained in:
@@ -0,0 +1,246 @@
|
||||
import Libbox
|
||||
import Library
|
||||
import SwiftUI
|
||||
|
||||
@MainActor
|
||||
public struct OOMReportListView: View {
|
||||
@EnvironmentObject private var environments: ExtensionEnvironments
|
||||
@State private var isLoading = true
|
||||
#if os(tvOS)
|
||||
@State private var selectedReport: OOMReport?
|
||||
#endif
|
||||
#if os(macOS)
|
||||
@State private var oomKillerEnabled = false
|
||||
@State private var oomMemoryLimitMB = 50
|
||||
@State private var oomKillerKillConnections = false
|
||||
@State private var alert: AlertState?
|
||||
#endif
|
||||
|
||||
public init() {}
|
||||
|
||||
private var manager: OOMReportManager {
|
||||
environments.oomReportManager
|
||||
}
|
||||
|
||||
public var body: some View {
|
||||
FormView {
|
||||
if !isLoading {
|
||||
Section {
|
||||
if manager.reports.isEmpty {
|
||||
Text("Empty")
|
||||
.foregroundStyle(.secondary)
|
||||
} else {
|
||||
ForEach(manager.reports) { report in
|
||||
#if os(tvOS)
|
||||
Button {
|
||||
selectedReport = report
|
||||
} label: {
|
||||
reportLabel(report)
|
||||
}
|
||||
#else
|
||||
FormNavigationLink {
|
||||
OOMReportDetailView(report: report)
|
||||
} label: {
|
||||
reportLabel(report)
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
} header: {
|
||||
Text("Reports")
|
||||
} footer: {
|
||||
#if os(macOS)
|
||||
Text("When memory limit is enabled, you will receive a report if the service memory exceeds the limit. You can also manually trigger report collection.")
|
||||
#else
|
||||
Text("You will receive a report when the service runs out of memory. You can also manually trigger report collection.")
|
||||
#endif
|
||||
}
|
||||
|
||||
#if os(macOS)
|
||||
Section {
|
||||
FormToggle("Enable Memory Limit", """
|
||||
Provide a soft memory limit for the service. The service will perform multiple processes to try to stay within this memory limit.
|
||||
""", $oomKillerEnabled) { newValue in
|
||||
await SharedPreferences.oomKillerEnabled.set(newValue)
|
||||
await restartService()
|
||||
}
|
||||
|
||||
if oomKillerEnabled {
|
||||
Picker("Memory Limit", selection: $oomMemoryLimitMB) {
|
||||
ForEach(Self.memoryLimitOptions, id: \.self) { value in
|
||||
Text(LibboxFormatMemoryBytes(Int64(value) * 1024 * 1024))
|
||||
.tag(value)
|
||||
}
|
||||
}
|
||||
.onChange(of: oomMemoryLimitMB) { _ in
|
||||
Task {
|
||||
await SharedPreferences.oomMemoryLimitMB.set(oomMemoryLimitMB)
|
||||
await restartService()
|
||||
}
|
||||
}
|
||||
|
||||
FormToggle("Kill Connections", """
|
||||
Kill all connections to free memory when the service memory exceeds the limit.
|
||||
""", $oomKillerKillConnections) { newValue in
|
||||
await SharedPreferences.oomKillerKillConnections.set(newValue)
|
||||
await restartService()
|
||||
}
|
||||
}
|
||||
} header: {
|
||||
Text("Settings")
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
.overlay {
|
||||
if isLoading {
|
||||
ProgressView()
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
Task {
|
||||
await manager.refresh()
|
||||
#if os(macOS)
|
||||
oomKillerEnabled = await SharedPreferences.oomKillerEnabled.get()
|
||||
let storedLimit = await SharedPreferences.oomMemoryLimitMB.get()
|
||||
if Self.memoryLimitOptions.contains(storedLimit) {
|
||||
oomMemoryLimitMB = storedLimit
|
||||
} else {
|
||||
oomMemoryLimitMB = Self.memoryLimitOptions.first!
|
||||
await SharedPreferences.oomMemoryLimitMB.set(oomMemoryLimitMB)
|
||||
}
|
||||
oomKillerKillConnections = await SharedPreferences.oomKillerKillConnections.get()
|
||||
#endif
|
||||
isLoading = false
|
||||
}
|
||||
}
|
||||
.navigationTitle("OOM Report")
|
||||
#if os(macOS)
|
||||
.alert($alert)
|
||||
#endif
|
||||
#if os(tvOS)
|
||||
.navigationDestination(item: $selectedReport) { report in
|
||||
OOMReportDetailView(report: report)
|
||||
.toolbar {
|
||||
ToolbarItemGroup(placement: .topBarLeading) {
|
||||
BackButton()
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
.toolbar {
|
||||
#if os(tvOS)
|
||||
if !manager.reports.isEmpty {
|
||||
ToolbarItem(placement: .confirmationAction) {
|
||||
Button {
|
||||
Task {
|
||||
await manager.deleteAll()
|
||||
}
|
||||
} label: {
|
||||
Image(systemName: "trash.fill")
|
||||
}
|
||||
.tint(.red)
|
||||
}
|
||||
}
|
||||
if let profile = environments.extensionProfile {
|
||||
ToolbarItem(placement: .confirmationAction) {
|
||||
OOMReportTriggerButton(manager: manager, profile: profile)
|
||||
}
|
||||
}
|
||||
#else
|
||||
if let profile = environments.extensionProfile {
|
||||
OOMReportToolbarMenu(manager: manager, profile: profile)
|
||||
} else if !manager.reports.isEmpty {
|
||||
Menu {
|
||||
Button(role: .destructive) {
|
||||
Task {
|
||||
await manager.deleteAll()
|
||||
}
|
||||
} label: {
|
||||
Label("Delete All", systemImage: "trash.fill")
|
||||
}
|
||||
} label: {
|
||||
Label("Others", systemImage: "line.3.horizontal.circle")
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
private func reportLabel(_ report: OOMReport) -> some View {
|
||||
ReportLabel(date: report.date, isRead: report.isRead, origin: report.origin)
|
||||
}
|
||||
|
||||
#if os(macOS)
|
||||
private static let memoryLimitOptions = [50, 100, 200, 300, 500, 750, 1024]
|
||||
|
||||
private func restartService() async {
|
||||
guard let profile = environments.extensionProfile, profile.status.isConnected else {
|
||||
return
|
||||
}
|
||||
do {
|
||||
try await profile.restart()
|
||||
} catch {
|
||||
alert = AlertState(action: "restart service", error: error)
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
#if os(tvOS)
|
||||
private struct OOMReportTriggerButton: View {
|
||||
let manager: OOMReportManager
|
||||
@ObservedObject var profile: ExtensionProfile
|
||||
@State private var alert: AlertState?
|
||||
|
||||
var body: some View {
|
||||
Button {
|
||||
triggerOOMReport(profile: profile, manager: manager, alert: &alert)
|
||||
} label: {
|
||||
Image(systemName: "memorychip")
|
||||
}
|
||||
.alert($alert)
|
||||
}
|
||||
}
|
||||
#else
|
||||
private struct OOMReportToolbarMenu: View {
|
||||
let manager: OOMReportManager
|
||||
@ObservedObject var profile: ExtensionProfile
|
||||
@State private var alert: AlertState?
|
||||
|
||||
var body: some View {
|
||||
Menu {
|
||||
Button {
|
||||
triggerOOMReport(profile: profile, manager: manager, alert: &alert)
|
||||
} label: {
|
||||
Label("Fetch Memory Report", systemImage: "memorychip")
|
||||
}
|
||||
if !manager.reports.isEmpty {
|
||||
Button(role: .destructive) {
|
||||
Task {
|
||||
await manager.deleteAll()
|
||||
}
|
||||
} label: {
|
||||
Label("Delete All", systemImage: "trash.fill")
|
||||
}
|
||||
}
|
||||
} label: {
|
||||
Label("Others", systemImage: "line.3.horizontal.circle")
|
||||
}
|
||||
.alert($alert)
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
@MainActor
|
||||
private func triggerOOMReport(profile: ExtensionProfile, manager: OOMReportManager, alert: inout AlertState?) {
|
||||
guard profile.status.isConnectedStrict else {
|
||||
alert = AlertState(errorMessage: String(localized: "Service not started"))
|
||||
return
|
||||
}
|
||||
try? LibboxNewStandaloneCommandClient()?.triggerOOMReport()
|
||||
Task {
|
||||
try? await Task.sleep(nanoseconds: NSEC_PER_SEC)
|
||||
await manager.refresh()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user