Write logs to new post (#236)

This commit is contained in:
Angelo Stavrow 2022-12-06 16:41:12 -05:00 committed by GitHub
parent fcd7c167c7
commit 1361ff7586
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 153 additions and 25 deletions

View File

@ -14,6 +14,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- [Mac] Added a menu item for toggling the toolbar.
- [Mac] In a post with unpublished changes (i.e., with "local" or "edited" status), the post is autosaved after a one-second pause in typing.
- [iOS/Mac] Added a context-menu item to delete local posts from the post list.
- [iOS/Mac] Added methods to fetch device logs.
- [iOS] Added a settings option to generate a log file as a new local draft (iOS 15+).
### Changed

View File

@ -1,9 +1,4 @@
//
// Logging.swift
// WriteFreely-MultiPlatform
//
// Created by Angelo Stavrow on 2022-06-25.
//
// Credit for much of this class: https://steipete.com/posts/logging-in-swift/
import Foundation
import os
@ -14,6 +9,11 @@ protocol LogWriter {
func logCrashAndSetFlag(error: Error)
}
@available(iOS 15, *)
protocol LogReader {
func fetchLogs() -> [String]
}
final class Logging {
private let logger: Logger
@ -48,3 +48,57 @@ extension Logging: LogWriter {
}
}
extension Logging: LogReader {
@available(iOS 15, *)
func fetchLogs() -> [String] {
var logs: [String] = []
do {
let osLog = try getLogEntries()
for logEntry in osLog {
let formattedEntry = formatEntry(logEntry)
logs.append(formattedEntry)
}
} catch {
logs.append("Could not fetch logs")
}
return logs
}
@available(iOS 15, *)
private func getLogEntries() throws -> [OSLogEntryLog] {
let logStore = try OSLogStore(scope: .currentProcessIdentifier)
let oneHourAgo = logStore.position(date: Date().addingTimeInterval(-3600))
let allEntries = try Array(logStore.__entriesEnumerator(position: oneHourAgo, predicate: nil))
return allEntries
.compactMap { $0 as? OSLogEntryLog }
.filter { $0.subsystem == subsystem }
}
@available(iOS 15, *)
private func formatEntry(_ logEntry: OSLogEntryLog) -> String {
/// The desired format is:
/// `date [process/category] LEVEL: composedMessage (threadIdentifier)`
var level: String = ""
switch logEntry.level {
case .debug:
level = "DEBUG"
case .info:
level = "INFO"
case .notice:
level = "NOTICE"
case .error:
level = "ERROR"
case .fault:
level = "FAULT"
default:
level = "UNDEFINED"
}
// swiftlint:disable:next line_length
return "\(logEntry.date) [\(logEntry.process)/\(logEntry.category)] \(level): \(logEntry.composedMessage) (\(logEntry.threadIdentifier))"
}
}

View File

@ -24,6 +24,8 @@ struct CheckForDebugModifier {
struct WriteFreely_MultiPlatformApp: App {
@StateObject private var model = WriteFreelyModel.shared
private let logger = Logging(for: String(describing: WriteFreely_MultiPlatformApp.self))
#if os(macOS)
@NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
@StateObject var updaterViewModel = MacUpdatesViewModel()
@ -68,6 +70,11 @@ struct WriteFreely_MultiPlatformApp: App {
)
)
}
.onAppear {
if #available(iOS 15, *) {
if didCrash { generateCrashLogPost() }
}
}
.withErrorHandling()
.environmentObject(model)
.environment(\.managedObjectContext, LocalStorageManager.standard.container.viewContext)
@ -167,6 +174,33 @@ struct WriteFreely_MultiPlatformApp: App {
}
}
@available(iOS 15, *)
private func generateCrashLogPost() {
logger.log("Generating local log post...")
DispatchQueue.main.asyncAfter(deadline: .now()) {
// Unset selected post and collection and navigate to local drafts.
self.model.selectedPost = nil
self.model.selectedCollection = nil
self.model.showAllPosts = false
// Create the new log post.
let newLogPost = model.editor.generateNewLocalPost(withFont: 2)
newLogPost.title = "Logs For Support"
var postBody: [String] = [
"WriteFreely-Multiplatform v\(Bundle.main.appMarketingVersion) (\(Bundle.main.appBuildVersion))",
"Generated \(Date())",
""
]
postBody.append(contentsOf: logger.fetchLogs())
newLogPost.body = postBody.joined(separator: "\n")
self.model.selectedPost = newLogPost
}
logger.log("Generated local log post.")
}
private func resetCrashFlags() {
UserDefaults.shared.set(false, forKey: WFDefaults.didHaveFatalError)
UserDefaults.shared.removeObject(forKey: WFDefaults.fatalErrorDescription)

View File

@ -1050,7 +1050,7 @@
CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
CODE_SIGN_ENTITLEMENTS = "ActionExtension-iOS/ActionExtension-iOS.entitlements";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 690;
CURRENT_PROJECT_VERSION = 691;
DEVELOPMENT_TEAM = TPPAB4YBA6;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = "ActionExtension-iOS/Info.plist";
@ -1081,7 +1081,7 @@
CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
CODE_SIGN_ENTITLEMENTS = "ActionExtension-iOS/ActionExtension-iOS.entitlements";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 690;
CURRENT_PROJECT_VERSION = 691;
DEVELOPMENT_TEAM = TPPAB4YBA6;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = "ActionExtension-iOS/Info.plist";
@ -1224,7 +1224,7 @@
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_ENTITLEMENTS = "WriteFreely-MultiPlatform (iOS).entitlements";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 690;
CURRENT_PROJECT_VERSION = 691;
DEVELOPMENT_TEAM = TPPAB4YBA6;
ENABLE_PREVIEWS = YES;
INFOPLIST_FILE = iOS/Info.plist;
@ -1250,7 +1250,7 @@
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_ENTITLEMENTS = "WriteFreely-MultiPlatform (iOS).entitlements";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 690;
CURRENT_PROJECT_VERSION = 691;
DEVELOPMENT_TEAM = TPPAB4YBA6;
ENABLE_PREVIEWS = YES;
INFOPLIST_FILE = iOS/Info.plist;

View File

@ -1,7 +1,11 @@
import SwiftUI
struct SettingsView: View {
@EnvironmentObject var model: WriteFreelyModel
@State private var isShowingAlert = false
private let logger = Logging(for: String(describing: SettingsView.self))
var body: some View {
VStack {
@ -14,21 +18,22 @@ struct SettingsView: View {
Section(header: Text("Appearance")) {
PreferencesView(preferences: model.preferences)
}
Section(header: Text("External Links")) {
HStack {
Spacer()
Link("View the Guide", destination: model.howToURL)
Spacer()
}
HStack {
Spacer()
Link("Visit the Help Forum", destination: model.helpURL)
Spacer()
}
HStack {
Spacer()
Link("Write a Review on the App Store", destination: model.reviewURL)
Spacer()
Section(header: Text("Help and Support")) {
Link("View the Guide", destination: model.howToURL)
Link("Visit the Help Forum", destination: model.helpURL)
Link("Write a Review on the App Store", destination: model.reviewURL)
if #available(iOS 15.0, *) {
VStack(alignment: .leading, spacing: 8) {
Button(
action: didTapGenerateLogPostButton,
label: {
Text("Create Log Post")
}
)
Text("Generates a local post using recent logs. You can share this for troubleshooting.")
.font(.footnote)
.foregroundColor(.secondary)
}
}
}
Section(header: Text("Acknowledgements")) {
@ -55,8 +60,41 @@ struct SettingsView: View {
}
}
}
.alert(isPresented: $isShowingAlert) {
Alert(
title: Text("Log Post Created"),
message: Text("Check your local drafts for app logs from the past 24 hours.")
)
}
// .preferredColorScheme(preferences.selectedColorScheme) // See PreferencesModel for info.
}
@available(iOS 15, *)
private func didTapGenerateLogPostButton() {
logger.log("Generating local log post...")
DispatchQueue.main.asyncAfter(deadline: .now()) {
// Unset selected post and collection and navigate to local drafts.
self.model.selectedPost = nil
self.model.selectedCollection = nil
self.model.showAllPosts = false
// Create the new log post.
let newLogPost = model.editor.generateNewLocalPost(withFont: 2)
newLogPost.title = "Logs For Support"
var postBody: [String] = [
"WriteFreely-Multiplatform v\(Bundle.main.appMarketingVersion) (\(Bundle.main.appBuildVersion))",
"Generated \(Date())",
""
]
postBody.append(contentsOf: logger.fetchLogs())
newLogPost.body = postBody.joined(separator: "\n")
self.isShowingAlert = true
}
logger.log("Generated local log post.")
}
}
struct SettingsView_Previews: PreviewProvider {