mirror of
https://github.com/passepartoutvpn/passepartout-apple.git
synced 2025-01-22 00:22:26 +00:00
Improve debug logs and move methods to library (#1076)
- Move availableLogs() / purgeLogs() to library - Append and rotate logs by size (500k) - Add marker between app/tunnel launches - Purge logs on each save (3 days) - Unify debug log content view across platforms - macOS: Table + inspect full line - iOS/tvOS: Use List - Scroll to bottom onLoad()
This commit is contained in:
parent
a6e44872e9
commit
bd9f8d63a5
@ -1,39 +0,0 @@
|
|||||||
//
|
|
||||||
// DebugLogContentView+iOS.swift
|
|
||||||
// Passepartout
|
|
||||||
//
|
|
||||||
// Created by Davide De Rosa on 8/31/24.
|
|
||||||
// Copyright (c) 2025 Davide De Rosa. All rights reserved.
|
|
||||||
//
|
|
||||||
// https://github.com/passepartoutvpn
|
|
||||||
//
|
|
||||||
// This file is part of Passepartout.
|
|
||||||
//
|
|
||||||
// Passepartout is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU General Public License as published by
|
|
||||||
// the Free Software Foundation, either version 3 of the License, or
|
|
||||||
// (at your option) any later version.
|
|
||||||
//
|
|
||||||
// Passepartout is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU General Public License
|
|
||||||
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
//
|
|
||||||
|
|
||||||
#if os(iOS)
|
|
||||||
|
|
||||||
import SwiftUI
|
|
||||||
|
|
||||||
struct DebugLogContentView: View {
|
|
||||||
let lines: [String]
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
TextEditor(text: .constant(lines.joined(separator: "\n")))
|
|
||||||
.font(.caption)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#endif
|
|
@ -1,42 +0,0 @@
|
|||||||
//
|
|
||||||
// DebugLogContentView+macOS.swift
|
|
||||||
// Passepartout
|
|
||||||
//
|
|
||||||
// Created by Davide De Rosa on 8/31/24.
|
|
||||||
// Copyright (c) 2025 Davide De Rosa. All rights reserved.
|
|
||||||
//
|
|
||||||
// https://github.com/passepartoutvpn
|
|
||||||
//
|
|
||||||
// This file is part of Passepartout.
|
|
||||||
//
|
|
||||||
// Passepartout is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU General Public License as published by
|
|
||||||
// the Free Software Foundation, either version 3 of the License, or
|
|
||||||
// (at your option) any later version.
|
|
||||||
//
|
|
||||||
// Passepartout is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU General Public License
|
|
||||||
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
//
|
|
||||||
|
|
||||||
#if os(macOS)
|
|
||||||
|
|
||||||
import SwiftUI
|
|
||||||
|
|
||||||
struct DebugLogContentView: View {
|
|
||||||
let lines: [String]
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
List {
|
|
||||||
ForEach(Array(lines.enumerated()), id: \.offset) {
|
|
||||||
Text($0.element)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#endif
|
|
@ -1,45 +0,0 @@
|
|||||||
//
|
|
||||||
// DebugLogContentView.swift
|
|
||||||
// Passepartout
|
|
||||||
//
|
|
||||||
// Created by Davide De Rosa on 11/23/24.
|
|
||||||
// Copyright (c) 2025 Davide De Rosa. All rights reserved.
|
|
||||||
//
|
|
||||||
// https://github.com/passepartoutvpn
|
|
||||||
//
|
|
||||||
// This file is part of Passepartout.
|
|
||||||
//
|
|
||||||
// Passepartout is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU General Public License as published by
|
|
||||||
// the Free Software Foundation, either version 3 of the License, or
|
|
||||||
// (at your option) any later version.
|
|
||||||
//
|
|
||||||
// Passepartout is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU General Public License
|
|
||||||
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
//
|
|
||||||
|
|
||||||
import SwiftUI
|
|
||||||
|
|
||||||
struct DebugLogContentView: View {
|
|
||||||
let lines: [String]
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
ScrollView {
|
|
||||||
LazyVStack {
|
|
||||||
ForEach(Array(lines.enumerated()), id: \.offset) {
|
|
||||||
Button($0.element) {
|
|
||||||
//
|
|
||||||
}
|
|
||||||
.buttonStyle(.plain)
|
|
||||||
.frame(maxWidth: .infinity, alignment: .leading)
|
|
||||||
.font(.caption)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -122,7 +122,7 @@ extension ExtendedTunnel {
|
|||||||
public func currentLog(parameters: Constants.Log) async -> [String] {
|
public func currentLog(parameters: Constants.Log) async -> [String] {
|
||||||
let output = try? await tunnel.sendMessage(.localLog(
|
let output = try? await tunnel.sendMessage(.localLog(
|
||||||
sinceLast: parameters.sinceLast,
|
sinceLast: parameters.sinceLast,
|
||||||
maxLevel: parameters.maxLevel
|
maxLevel: parameters.options.maxLevel
|
||||||
))
|
))
|
||||||
switch output {
|
switch output {
|
||||||
case .debugLog(let log):
|
case .debugLog(let log):
|
||||||
|
@ -145,9 +145,7 @@ public struct Constants: Decodable, Sendable {
|
|||||||
|
|
||||||
public let sinceLast: TimeInterval
|
public let sinceLast: TimeInterval
|
||||||
|
|
||||||
public let maxLevel: DebugLog.Level
|
public let options: LocalLogger.Options
|
||||||
|
|
||||||
public let maxNumberOfLines: Int
|
|
||||||
|
|
||||||
public let maxAge: TimeInterval?
|
public let maxAge: TimeInterval?
|
||||||
}
|
}
|
||||||
|
@ -30,67 +30,27 @@ extension PassepartoutConfiguration {
|
|||||||
public func configureLogging(to url: URL, parameters: Constants.Log, logsPrivateData: Bool) {
|
public func configureLogging(to url: URL, parameters: Constants.Log, logsPrivateData: Bool) {
|
||||||
pp_log(.app, .debug, "Log to: \(url)")
|
pp_log(.app, .debug, "Log to: \(url)")
|
||||||
|
|
||||||
setLocalLogger(options: .init(
|
setLocalLogger(
|
||||||
url: url,
|
url: url,
|
||||||
maxNumberOfLines: parameters.maxNumberOfLines,
|
options: parameters.options,
|
||||||
maxLevel: parameters.maxLevel,
|
|
||||||
mapper: parameters.formatter.formattedLine
|
mapper: parameters.formatter.formattedLine
|
||||||
))
|
)
|
||||||
|
|
||||||
if logsPrivateData {
|
if logsPrivateData {
|
||||||
logsAddresses = true
|
logsAddresses = true
|
||||||
logsModules = true
|
logsModules = true
|
||||||
}
|
}
|
||||||
|
|
||||||
if let maxAge = parameters.maxAge {
|
appendLog(parameters.options.maxLevel, message: "")
|
||||||
purgeLogs(at: url, beyond: maxAge)
|
appendLog(parameters.options.maxLevel, message: "--- BEGIN ---")
|
||||||
}
|
appendLog(parameters.options.maxLevel, message: "")
|
||||||
}
|
}
|
||||||
|
|
||||||
public func currentLog(parameters: Constants.Log) -> [String] {
|
public func currentLog(parameters: Constants.Log) -> [String] {
|
||||||
currentLogLines(
|
currentLogLines(
|
||||||
sinceLast: parameters.sinceLast,
|
sinceLast: parameters.sinceLast,
|
||||||
maxLevel: parameters.maxLevel
|
maxLevel: parameters.options.maxLevel
|
||||||
)
|
)
|
||||||
.map(parameters.formatter.formattedLine)
|
.map(parameters.formatter.formattedLine)
|
||||||
}
|
}
|
||||||
|
|
||||||
public func availableLogs(at url: URL) -> [Date: URL] {
|
|
||||||
let parent = url.deletingLastPathComponent()
|
|
||||||
let prefix = url.lastPathComponent
|
|
||||||
do {
|
|
||||||
let contents = try FileManager.default.contentsOfDirectory(at: parent, includingPropertiesForKeys: nil)
|
|
||||||
return contents.reduce(into: [:]) { found, item in
|
|
||||||
let filename = item.lastPathComponent
|
|
||||||
guard filename.hasPrefix(prefix) else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
guard let timestampString = filename.split(separator: ".").last,
|
|
||||||
let timestamp = TimeInterval(timestampString) else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
let date = Date(timeIntervalSince1970: timestamp)
|
|
||||||
found[date] = item
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
return [:]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public func flushLog() {
|
|
||||||
try? saveLog()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private extension PassepartoutConfiguration {
|
|
||||||
func purgeLogs(at url: URL, beyond maxAge: TimeInterval) {
|
|
||||||
let logs = availableLogs(at: url)
|
|
||||||
let minDate = Date().addingTimeInterval(-maxAge)
|
|
||||||
logs.forEach { date, url in
|
|
||||||
guard date >= minDate else {
|
|
||||||
try? FileManager.default.removeItem(at: url)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -37,9 +37,12 @@
|
|||||||
"appPath": "app.log",
|
"appPath": "app.log",
|
||||||
"tunnelPath": "tunnel.log",
|
"tunnelPath": "tunnel.log",
|
||||||
"sinceLast": 86400,
|
"sinceLast": 86400,
|
||||||
"maxLevel": 3,
|
"options": {
|
||||||
"maxNumberOfLines": 10000,
|
"maxLevel": 3,
|
||||||
"maxAge": 604800,
|
"maxSize": 500000,
|
||||||
|
"maxBufferedLines": 5000,
|
||||||
|
"maxAge": 259200
|
||||||
|
},
|
||||||
"formatter": {
|
"formatter": {
|
||||||
"timestamp": "HH:mm:ss",
|
"timestamp": "HH:mm:ss",
|
||||||
"message": "%@ - %@"
|
"message": "%@ - %@"
|
||||||
|
@ -266,8 +266,23 @@ extension View {
|
|||||||
modifier(ThemeHoverListRowModifier())
|
modifier(ThemeHoverListRowModifier())
|
||||||
}
|
}
|
||||||
|
|
||||||
public func themeTip(_ text: String, edge: Edge) -> some View {
|
public func themeTip<Label>(
|
||||||
modifier(ThemeTipModifier(text: text, edge: edge))
|
_ text: String,
|
||||||
|
edge: Edge,
|
||||||
|
width: Double = 150.0,
|
||||||
|
alignment: Alignment = .center,
|
||||||
|
label: @escaping () -> Label = {
|
||||||
|
ThemeImage(.tip)
|
||||||
|
.imageScale(.large)
|
||||||
|
}
|
||||||
|
) -> some View where Label: View {
|
||||||
|
modifier(ThemeTipModifier(
|
||||||
|
text: text,
|
||||||
|
edge: edge,
|
||||||
|
width: width,
|
||||||
|
alignment: alignment,
|
||||||
|
label: label
|
||||||
|
))
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
@ -617,11 +632,17 @@ struct ThemeLockScreenModifier<LockedContent>: ViewModifier where LockedContent:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct ThemeTipModifier: ViewModifier {
|
struct ThemeTipModifier<Label>: ViewModifier where Label: View {
|
||||||
let text: String
|
let text: String
|
||||||
|
|
||||||
let edge: Edge
|
let edge: Edge
|
||||||
|
|
||||||
|
let width: Double
|
||||||
|
|
||||||
|
let alignment: Alignment
|
||||||
|
|
||||||
|
let label: () -> Label
|
||||||
|
|
||||||
@State
|
@State
|
||||||
private var isPresenting = false
|
private var isPresenting = false
|
||||||
|
|
||||||
@ -631,9 +652,8 @@ struct ThemeTipModifier: ViewModifier {
|
|||||||
Button {
|
Button {
|
||||||
isPresenting = true
|
isPresenting = true
|
||||||
} label: {
|
} label: {
|
||||||
ThemeImage(.tip)
|
label()
|
||||||
}
|
}
|
||||||
.imageScale(.large)
|
|
||||||
.buttonStyle(.borderless)
|
.buttonStyle(.borderless)
|
||||||
.popover(isPresented: $isPresenting, arrowEdge: edge) {
|
.popover(isPresented: $isPresenting, arrowEdge: edge) {
|
||||||
VStack {
|
VStack {
|
||||||
@ -642,7 +662,7 @@ struct ThemeTipModifier: ViewModifier {
|
|||||||
.foregroundStyle(.primary)
|
.foregroundStyle(.primary)
|
||||||
.lineLimit(nil)
|
.lineLimit(nil)
|
||||||
.multilineTextAlignment(.leading)
|
.multilineTextAlignment(.leading)
|
||||||
.frame(width: 150.0)
|
.frame(width: width, alignment: alignment)
|
||||||
}
|
}
|
||||||
.padding(12)
|
.padding(12)
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,110 @@
|
|||||||
|
//
|
||||||
|
// DebugLogContentView.swift
|
||||||
|
// Passepartout
|
||||||
|
//
|
||||||
|
// Created by Davide De Rosa on 11/23/24.
|
||||||
|
// Copyright (c) 2025 Davide De Rosa. All rights reserved.
|
||||||
|
//
|
||||||
|
// https://github.com/passepartoutvpn
|
||||||
|
//
|
||||||
|
// This file is part of Passepartout.
|
||||||
|
//
|
||||||
|
// Passepartout is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// Passepartout is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
public struct DebugLogContentView: View {
|
||||||
|
|
||||||
|
@EnvironmentObject
|
||||||
|
private var theme: Theme
|
||||||
|
|
||||||
|
public let lines: [String]
|
||||||
|
|
||||||
|
public init(lines: [String]) {
|
||||||
|
self.lines = lines
|
||||||
|
}
|
||||||
|
|
||||||
|
public var body: some View {
|
||||||
|
ScrollViewReader { proxy in
|
||||||
|
scrollView
|
||||||
|
.onLoad {
|
||||||
|
withAnimation {
|
||||||
|
proxy.scrollTo(lines.count - 1, anchor: .bottom)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#if os(macOS)
|
||||||
|
|
||||||
|
private extension DebugLogContentView {
|
||||||
|
struct Entry: Identifiable {
|
||||||
|
let id: Int
|
||||||
|
|
||||||
|
let line: String
|
||||||
|
}
|
||||||
|
|
||||||
|
var scrollView: some View {
|
||||||
|
let entries = lines
|
||||||
|
.enumerated()
|
||||||
|
.map {
|
||||||
|
Entry.init(id: $0.offset, line: $0.element)
|
||||||
|
}
|
||||||
|
|
||||||
|
return Table(entries) {
|
||||||
|
TableColumn("") { entry in
|
||||||
|
HStack {
|
||||||
|
EmptyView()
|
||||||
|
.themeTip(entry.line, edge: .bottom, width: 400.0, alignment: .leading) {
|
||||||
|
ThemeImage(.search)
|
||||||
|
}
|
||||||
|
.environmentObject(theme) // TODO: #873, Table loses environment
|
||||||
|
|
||||||
|
Text(entry.line)
|
||||||
|
.font(.caption)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.withoutColumnHeaders()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#else
|
||||||
|
|
||||||
|
private extension DebugLogContentView {
|
||||||
|
var scrollView: some View {
|
||||||
|
List(lines.indices, id: \.self, rowContent: entryView)
|
||||||
|
.listStyle(.plain)
|
||||||
|
}
|
||||||
|
|
||||||
|
func entryView(for index: Int) -> some View {
|
||||||
|
Text(lines[index])
|
||||||
|
.themeMultiLine(true)
|
||||||
|
.scrollableOnTV()
|
||||||
|
.buttonStyle(.plain)
|
||||||
|
.font(.caption)
|
||||||
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
|
.id(index)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#Preview {
|
||||||
|
DebugLogContentView(
|
||||||
|
lines: Array(repeating: "foobar foobar foobar foobar foobar foobar foobar foobar foobar foobar foobar foobar foobar foobar foobar foobar foobar foobar foobar foobar foobar foobar foobar foobar foobar foobar foobar foobar foobar foobar foobar foobar foobar foobar foobar foobar foobar foobar foobar foobar foobar foobar foobar foobar foobar foobar foobar foobar foobar foobar foobar foobar foobar foobar foobar foobar foobar foobar foobar foobar foobar foobar foobar foobar ", count: 200)
|
||||||
|
)
|
||||||
|
}
|
@ -1 +1 @@
|
|||||||
Subproject commit f6d85fdf1e186fa13c820166f3a414962bcc52c1
|
Subproject commit b140470e9c2567fcb5e7053b030c1df509d7cd24
|
@ -124,6 +124,11 @@
|
|||||||
value = "1"
|
value = "1"
|
||||||
isEnabled = "NO">
|
isEnabled = "NO">
|
||||||
</EnvironmentVariable>
|
</EnvironmentVariable>
|
||||||
|
<EnvironmentVariable
|
||||||
|
key = "SWIFTUI_ATTRIBUTEDGRAPH_DEBUG"
|
||||||
|
value = "1"
|
||||||
|
isEnabled = "YES">
|
||||||
|
</EnvironmentVariable>
|
||||||
</EnvironmentVariables>
|
</EnvironmentVariables>
|
||||||
<StoreKitConfigurationFileReference
|
<StoreKitConfigurationFileReference
|
||||||
identifier = "../../Passepartout/Passepartout.storekit">
|
identifier = "../../Passepartout/Passepartout.storekit">
|
||||||
|
Loading…
Reference in New Issue
Block a user