mirror of
https://github.com/ninxsoft/Mist.git
synced 2025-05-29 14:35:26 -04:00
Add support for logs + writing to unified logging system (#120)
This commit is contained in:
parent
82f5b049e7
commit
4cc6319c85
15 changed files with 369 additions and 21 deletions
|
@ -93,6 +93,10 @@
|
|||
398734D028603D9E00B4C357 /* UInt8+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 398734CF28603D9E00B4C357 /* UInt8+Extension.swift */; };
|
||||
398734D228603DE700B4C357 /* [UInt8]+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 398734D128603DE700B4C357 /* [UInt8]+Extension.swift */; };
|
||||
398734D4286046B000B4C357 /* UInt32+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 398734D3286046B000B4C357 /* UInt32+Extension.swift */; };
|
||||
398A131D2B78AA3700F96F7E /* LogManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 398A131C2B78AA3700F96F7E /* LogManager.swift */; };
|
||||
398A131F2B78AF0B00F96F7E /* LogLevel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 398A131E2B78AF0B00F96F7E /* LogLevel.swift */; };
|
||||
398A13222B78BA6D00F96F7E /* LogView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 398A13212B78BA6D00F96F7E /* LogView.swift */; };
|
||||
398A13472B78F22A00F96F7E /* LogEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 398A13462B78F22A00F96F7E /* LogEntry.swift */; };
|
||||
398BE6B52B62450500FE0C29 /* FloatingAlert.swift in Sources */ = {isa = PBXBuildFile; fileRef = 398BE6B42B62450500FE0C29 /* FloatingAlert.swift */; };
|
||||
39CA25E32941D8BB0030711E /* FileAttributesUpdater.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39CA25E22941D8BB0030711E /* FileAttributesUpdater.swift */; };
|
||||
39CB5E3D293F5C2E00CFDBB8 /* Catalog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39CB5E3C293F5C2E00CFDBB8 /* Catalog.swift */; };
|
||||
|
@ -251,6 +255,10 @@
|
|||
398734CF28603D9E00B4C357 /* UInt8+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UInt8+Extension.swift"; sourceTree = "<group>"; };
|
||||
398734D128603DE700B4C357 /* [UInt8]+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "[UInt8]+Extension.swift"; sourceTree = "<group>"; };
|
||||
398734D3286046B000B4C357 /* UInt32+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UInt32+Extension.swift"; sourceTree = "<group>"; };
|
||||
398A131C2B78AA3700F96F7E /* LogManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = LogManager.swift; path = Mist/Helpers/LogManager.swift; sourceTree = SOURCE_ROOT; };
|
||||
398A131E2B78AF0B00F96F7E /* LogLevel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogLevel.swift; sourceTree = "<group>"; };
|
||||
398A13212B78BA6D00F96F7E /* LogView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogView.swift; sourceTree = "<group>"; };
|
||||
398A13462B78F22A00F96F7E /* LogEntry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogEntry.swift; sourceTree = "<group>"; };
|
||||
398BE6B42B62450500FE0C29 /* FloatingAlert.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FloatingAlert.swift; sourceTree = "<group>"; };
|
||||
39CA25E22941D8BB0030711E /* FileAttributesUpdater.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileAttributesUpdater.swift; sourceTree = "<group>"; };
|
||||
39CB5E3C293F5C2E00CFDBB8 /* Catalog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Catalog.swift; sourceTree = "<group>"; };
|
||||
|
@ -411,6 +419,7 @@
|
|||
393F35BD2864197F005B7165 /* PrivilegedHelperTool.swift */,
|
||||
3935F4CC286C6A5D00760AB0 /* ProcessKiller.swift */,
|
||||
574199D12AED15420086493F /* PropertyListUpdater.swift */,
|
||||
398A131C2B78AA3700F96F7E /* LogManager.swift */,
|
||||
3935F4C6286B54E200760AB0 /* SparkleUpdater.swift */,
|
||||
398734C328600E6E00B4C357 /* TaskManager.swift */,
|
||||
398734C5286011C300B4C357 /* Validator.swift */,
|
||||
|
@ -425,6 +434,8 @@
|
|||
39CB5E3C293F5C2E00CFDBB8 /* Catalog.swift */,
|
||||
390451E428574F0000E0B563 /* CatalogType.swift */,
|
||||
39CB5E3E2941486D00CFDBB8 /* CatalogSeedType.swift */,
|
||||
398A131E2B78AF0B00F96F7E /* LogLevel.swift */,
|
||||
398A13462B78F22A00F96F7E /* LogEntry.swift */,
|
||||
398734CB28603D5F00B4C357 /* Chunklist.swift */,
|
||||
398734CD28603D7F00B4C357 /* Chunk.swift */,
|
||||
390451CD2856F42800E0B563 /* DownloadType.swift */,
|
||||
|
@ -458,6 +469,7 @@
|
|||
393F35C128641E1F005B7165 /* HeaderView.swift */,
|
||||
3935F49C286ABE4D00760AB0 /* FooterView.swift */,
|
||||
390451D728573A2500E0B563 /* ExportListView.swift */,
|
||||
398A13212B78BA6D00F96F7E /* LogView.swift */,
|
||||
3935F4A0286ACCE100760AB0 /* List */,
|
||||
393F35BF28641D86005B7165 /* Activity */,
|
||||
393F35C028641D8F005B7165 /* Refresh */,
|
||||
|
@ -805,6 +817,7 @@
|
|||
39CF56172861BE66006FB5D2 /* FileCopier.swift in Sources */,
|
||||
39252AB9285C7BC700956C74 /* SettingsInstallersCacheView.swift in Sources */,
|
||||
390451C62856E80C00E0B563 /* RefreshView.swift in Sources */,
|
||||
398A131D2B78AA3700F96F7E /* LogManager.swift in Sources */,
|
||||
39252A97285BF8BC00956C74 /* MistTaskType.swift in Sources */,
|
||||
39CF56212861C992006FB5D2 /* DiskImageMounter.swift in Sources */,
|
||||
398734CC28603D5F00B4C357 /* Chunklist.swift in Sources */,
|
||||
|
@ -832,8 +845,10 @@
|
|||
39252A9F285C140D00956C74 /* ShellExecutor.swift in Sources */,
|
||||
39CF561A2861C2D1006FB5D2 /* DirectoryCreator.swift in Sources */,
|
||||
39252A77285A849F00956C74 /* AppDelegate.swift in Sources */,
|
||||
398A13472B78F22A00F96F7E /* LogEntry.swift in Sources */,
|
||||
3935F47C2864434B00760AB0 /* SettingsGeneralNotificationsView.swift in Sources */,
|
||||
3935F4C7286B54E200760AB0 /* SparkleUpdater.swift in Sources */,
|
||||
398A13222B78BA6D00F96F7E /* LogView.swift in Sources */,
|
||||
393F35BE2864197F005B7165 /* PrivilegedHelperTool.swift in Sources */,
|
||||
573A23622A28711C00EC9470 /* Architecture.swift in Sources */,
|
||||
390451B92856E24200E0B563 /* Firmware.swift in Sources */,
|
||||
|
@ -854,6 +869,7 @@
|
|||
3935F47428643AB800760AB0 /* UNNotificationCategory+Extension.swift in Sources */,
|
||||
398734D028603D9E00B4C357 /* UInt8+Extension.swift in Sources */,
|
||||
39252AB7285C718C00956C74 /* FileManager+Extension.swift in Sources */,
|
||||
398A131F2B78AF0B00F96F7E /* LogLevel.swift in Sources */,
|
||||
398734C428600E6E00B4C357 /* TaskManager.swift in Sources */,
|
||||
390451D62856F7FE00E0B563 /* UInt64+Extension.swift in Sources */,
|
||||
3935F47E2864813B00760AB0 /* DownloadManager.swift in Sources */,
|
||||
|
|
|
@ -35,6 +35,12 @@ struct AppCommands: Commands {
|
|||
}
|
||||
.disabled(tasksInProgress)
|
||||
}
|
||||
CommandGroup(after: .windowList) {
|
||||
Button("Show Mist Log") {
|
||||
showLog()
|
||||
}
|
||||
.keyboardShortcut("l")
|
||||
}
|
||||
CommandGroup(replacing: .help) {
|
||||
Button("Mist Help") {
|
||||
help()
|
||||
|
@ -50,6 +56,14 @@ struct AppCommands: Commands {
|
|||
try? PrivilegedHelperManager.shared.authorizeAndBless()
|
||||
}
|
||||
|
||||
private func showLog() {
|
||||
guard let url = URL(string: .logURL) else {
|
||||
return
|
||||
}
|
||||
|
||||
openURL(url)
|
||||
}
|
||||
|
||||
private func help() {
|
||||
guard let url: URL = URL(string: .repositoryURL) else {
|
||||
return
|
||||
|
|
|
@ -24,6 +24,7 @@ extension String {
|
|||
static let temporaryDirectory: String = "/private/tmp/\(appIdentifier)"
|
||||
static let cacheDirectory: String = "/Users/Shared/Mist/Cache"
|
||||
static let tccDatabasePath: String = "/Library/Application Support/com.apple.TCC/TCC.db"
|
||||
static let logURL: String = "\(appName)://log"
|
||||
|
||||
func stringWithSubstitutions(name: String, version: String, build: String) -> String {
|
||||
replacingOccurrences(of: "%NAME%", with: name)
|
||||
|
|
40
Mist/Helpers/LogManager.swift
Normal file
40
Mist/Helpers/LogManager.swift
Normal file
|
@ -0,0 +1,40 @@
|
|||
//
|
||||
// LogManager.swift
|
||||
// Mist
|
||||
//
|
||||
// Created by Nindi Gill on 11/2/2024.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import OSLog
|
||||
|
||||
/// Helper class used to manage Log Entries and writing out to the unified logging system.
|
||||
class LogManager: ObservableObject {
|
||||
/// Shared instance of a LogManager helper object
|
||||
static let shared: LogManager = .init()
|
||||
/// Array of Log Entries that have been recorded.
|
||||
@Published var logEntries: [LogEntry] = []
|
||||
/// Logger object used to send logs.
|
||||
private let logger: Logger = .init(subsystem: .appIdentifier, category: "")
|
||||
|
||||
/// Records a log entry and also sends it to the unified logging system.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - level: The log level of the message.
|
||||
/// - message: The message to log.
|
||||
func log(_ level: LogLevel, message: String) {
|
||||
DispatchQueue.main.async {
|
||||
let entry: LogEntry = .init(timestamp: .now, level: level, message: message)
|
||||
self.logEntries.append(entry)
|
||||
|
||||
switch level {
|
||||
case .info:
|
||||
self.logger.notice("\(message)")
|
||||
case .warning:
|
||||
self.logger.error("\(message)")
|
||||
case .error:
|
||||
self.logger.fault("\(message)")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -80,6 +80,7 @@ class TaskManager: ObservableObject {
|
|||
private static func firmwareSetupTasks(temporaryDirectory temporaryDirectoryURL: URL) -> [MistTask] {
|
||||
[
|
||||
MistTask(type: .configure, description: "temporary directory") {
|
||||
LogManager.shared.log(.info, message: "Configuring temporary directory '\(temporaryDirectoryURL.path)'...")
|
||||
try await DirectoryCreator.create(temporaryDirectoryURL)
|
||||
}
|
||||
]
|
||||
|
@ -96,6 +97,7 @@ class TaskManager: ObservableObject {
|
|||
) -> [MistTask] {
|
||||
var tasks: [MistTask] = [
|
||||
MistTask(type: .download, description: firmwareURL.lastPathComponent, downloadSize: firmware.size) {
|
||||
LogManager.shared.log(.info, message: "Downloading '\(firmwareURL.absoluteString)' to '\(temporaryFirmwareURL.path)'...")
|
||||
try await DownloadManager.shared.download(firmwareURL, to: temporaryFirmwareURL, retries: retries, delay: retryDelay)
|
||||
}
|
||||
]
|
||||
|
@ -103,13 +105,15 @@ class TaskManager: ObservableObject {
|
|||
if !firmware.shasum.isEmpty {
|
||||
tasks += [
|
||||
MistTask(type: .verify, description: firmwareURL.lastPathComponent) {
|
||||
LogManager.shared.log(.info, message: "Verifying '\(temporaryFirmwareURL.path)'...")
|
||||
try await Validator.validate(firmware, at: temporaryFirmwareURL)
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
tasks += [
|
||||
MistTask(type: .save, description: "Firmware to destination") {
|
||||
MistTask(type: .move, description: "Firmware to destination") {
|
||||
LogManager.shared.log(.info, message: "Moving '\(temporaryFirmwareURL.path)' to destination '\(destinationURL.path)'...")
|
||||
try await FileMover.move(temporaryFirmwareURL, to: destinationURL)
|
||||
}
|
||||
]
|
||||
|
@ -120,6 +124,7 @@ class TaskManager: ObservableObject {
|
|||
private static func firmwareCleanupTasks(temporaryDirectory temporaryDirectoryURL: URL) -> [MistTask] {
|
||||
[
|
||||
MistTask(type: .remove, description: "temporary directory") {
|
||||
LogManager.shared.log(.info, message: "Removing temporary directory '\(temporaryDirectoryURL.path)'...")
|
||||
try await DirectoryRemover.remove(temporaryDirectoryURL)
|
||||
}
|
||||
]
|
||||
|
@ -267,6 +272,7 @@ class TaskManager: ObservableObject {
|
|||
if !FileManager.default.fileExists(atPath: cacheDirectoryURL.path) {
|
||||
tasks += [
|
||||
MistTask(type: .configure, description: "cache directory") {
|
||||
LogManager.shared.log(.info, message: "Configuring cache directory '\(cacheDirectoryURL.path)'...")
|
||||
try await DirectoryCreator.create(cacheDirectoryURL, withIntermediateDirectories: true)
|
||||
}
|
||||
]
|
||||
|
@ -285,6 +291,7 @@ class TaskManager: ObservableObject {
|
|||
if filePermissions != [.ownerReadWriteExecute, .groupReadExecute, .otherReadExecute] || ownerAccountName != NSUserName() || groupOwnerAccountName != "wheel" {
|
||||
tasks += [
|
||||
MistTask(type: .configure, description: "cache directory") {
|
||||
LogManager.shared.log(.info, message: "Configuring cache directory '\(cacheDirectoryURL.path)'...")
|
||||
try await FileAttributesUpdater.update(url: cacheDirectoryURL, ownerAccountName: ownerAccountName)
|
||||
}
|
||||
]
|
||||
|
@ -300,9 +307,11 @@ class TaskManager: ObservableObject {
|
|||
|
||||
tasks += [
|
||||
MistTask(type: .download, description: package.filename, downloadSize: UInt64(package.size)) {
|
||||
LogManager.shared.log(.info, message: "Downloading '\(packageURL.absoluteString)' to '\(cachePackageURL.path)'...")
|
||||
try await DownloadManager.shared.download(packageURL, to: cachePackageURL, retries: retries, delay: retryDelay)
|
||||
},
|
||||
MistTask(type: .verify, description: package.filename) {
|
||||
LogManager.shared.log(.info, message: "Verifying '\(cachePackageURL.path)'...")
|
||||
try await Validator.validate(package, at: cachePackageURL)
|
||||
}
|
||||
]
|
||||
|
@ -311,16 +320,20 @@ class TaskManager: ObservableObject {
|
|||
return tasks
|
||||
}
|
||||
|
||||
// swiftlint:disable:next function_body_length
|
||||
private static func installTasks(for installer: Installer, temporaryDirectory temporaryDirectoryURL: URL, mountPoint mountPointURL: URL, cacheDirectory: String) -> [MistTask] {
|
||||
let imageURL: URL = temporaryDirectoryURL.appendingPathComponent("\(installer.id) Temp.dmg")
|
||||
var tasks: [MistTask] = [
|
||||
MistTask(type: .configure, description: "temporary directory") {
|
||||
LogManager.shared.log(.info, message: "Configuring temporary directory '\(temporaryDirectoryURL.path)'...")
|
||||
try await DirectoryCreator.create(temporaryDirectoryURL)
|
||||
},
|
||||
MistTask(type: .create, description: "Disk Image") {
|
||||
LogManager.shared.log(.info, message: "Creating Disk Image '\(imageURL.path)'...")
|
||||
try await DiskImageCreator.create(imageURL, size: installer.diskImageSize)
|
||||
},
|
||||
MistTask(type: .mount, description: "Disk Image") {
|
||||
LogManager.shared.log(.info, message: "Mounting Disk Image '\(imageURL.path)' at mount point '\(mountPointURL.path)'...")
|
||||
try await DiskImageMounter.mount(imageURL, mountPoint: mountPointURL)
|
||||
}
|
||||
]
|
||||
|
@ -333,18 +346,22 @@ class TaskManager: ObservableObject {
|
|||
|
||||
tasks += [
|
||||
MistTask(type: .mount, description: "Installer Disk Image") {
|
||||
LogManager.shared.log(.info, message: "Mounting Disk Image '\(legacyDiskImageURL.path)' at mount point '\(legacyDiskImageMountPointURL.path)'...")
|
||||
try await DiskImageMounter.mount(legacyDiskImageURL, mountPoint: legacyDiskImageMountPointURL)
|
||||
},
|
||||
MistTask(type: .create, description: "Installer in Disk Image") {
|
||||
LogManager.shared.log(.info, message: "Creating Installer in Disk Image at mount point '\(mountPointURL.path)' using cache directory '\(cacheDirectory)/\(installer.id)'...")
|
||||
try await InstallerCreator.create(installer, mountPoint: mountPointURL, cacheDirectory: cacheDirectory)
|
||||
},
|
||||
MistTask(type: .unmount, description: "Installer Disk Image") {
|
||||
LogManager.shared.log(.info, message: "Unmounting Installer Disk Image at mount point '\(legacyDiskImageMountPointURL.path)'...")
|
||||
try await DiskImageUnmounter.unmount(legacyDiskImageMountPointURL)
|
||||
}
|
||||
]
|
||||
} else {
|
||||
tasks += [
|
||||
MistTask(type: .create, description: "macOS Installer in Disk Image") {
|
||||
LogManager.shared.log(.info, message: "Creating macOS Installer in Disk Image at mount point '\(mountPointURL.path)' using cache directory '\(cacheDirectory)/\(installer.id)'...")
|
||||
try await InstallerCreator.create(installer, mountPoint: mountPointURL, cacheDirectory: cacheDirectory)
|
||||
|
||||
guard let major: Substring = installer.version.split(separator: ".").first else {
|
||||
|
@ -358,6 +375,7 @@ class TaskManager: ObservableObject {
|
|||
return
|
||||
}
|
||||
|
||||
LogManager.shared.log(.info, message: "Moving '\(source.path)' to '\(destination.path)'...")
|
||||
try await FileMover.move(source, to: destination)
|
||||
}
|
||||
]
|
||||
|
@ -370,7 +388,8 @@ class TaskManager: ObservableObject {
|
|||
let applicationURL: URL = destinationURL.appendingPathComponent(filename.stringWithSubstitutions(name: installer.name, version: installer.version, build: installer.build))
|
||||
|
||||
return [
|
||||
MistTask(type: .save, description: "Application to destination") {
|
||||
MistTask(type: .copy, description: "Application to destination") {
|
||||
LogManager.shared.log(.info, message: "Copying Application '\(installer.temporaryInstallerURL.path)' to destination '\(applicationURL.path)'...")
|
||||
try await FileCopier.copy(installer.temporaryInstallerURL, to: applicationURL)
|
||||
}
|
||||
]
|
||||
|
@ -391,15 +410,19 @@ class TaskManager: ObservableObject {
|
|||
let imageURL: URL = destinationURL.appendingPathComponent(filename.stringWithSubstitutions(name: installer.name, version: installer.version, build: installer.build))
|
||||
var tasks: [MistTask] = [
|
||||
MistTask(type: .configure, description: "temporary Disk Image directory") {
|
||||
LogManager.shared.log(.info, message: "Configuring temporary Disk Image directory '\(imageDirectoryURL.path)'...")
|
||||
try await DirectoryCreator.create(imageDirectoryURL)
|
||||
},
|
||||
MistTask(type: .save, description: "macOS Installer to temporary Disk Image directory") {
|
||||
MistTask(type: .copy, description: "macOS Installer to temporary Disk Image directory") {
|
||||
LogManager.shared.log(.info, message: "Copying macOS Installer '\(installer.temporaryInstallerURL.path)' to temporary Disk Image directory '\(applicationURL.path)'...")
|
||||
try await FileCopier.copy(installer.temporaryInstallerURL, to: applicationURL)
|
||||
},
|
||||
MistTask(type: .create, description: "Disk Image") {
|
||||
LogManager.shared.log(.info, message: "Creating Disk Image '\(temporaryImageURL.path)' from '\(imageDirectoryURL.path)'...")
|
||||
try await DiskImageCreator.create(temporaryImageURL, from: imageDirectoryURL)
|
||||
},
|
||||
MistTask(type: .remove, description: "temporary Disk Image directory") {
|
||||
LogManager.shared.log(.info, message: "Removing temporary Disk Image directory '\(imageDirectoryURL.path)'...")
|
||||
try await DirectoryRemover.remove(imageDirectoryURL)
|
||||
}
|
||||
]
|
||||
|
@ -407,19 +430,22 @@ class TaskManager: ObservableObject {
|
|||
if diskImageSign, !diskImageSigningIdentity.isEmpty, diskImageSigningIdentity != "-" {
|
||||
tasks += [
|
||||
MistTask(type: .codesign, description: "Disk Image") {
|
||||
LogManager.shared.log(.info, message: "Codesigning Disk Image '\(temporaryImageURL.path)' using signing identity '\(diskImageSigningIdentity)'...")
|
||||
try await Codesigner.sign(temporaryImageURL, identity: diskImageSigningIdentity)
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
tasks += [
|
||||
MistTask(type: .save, description: "Disk Image to destination") {
|
||||
MistTask(type: .move, description: "Disk Image to destination") {
|
||||
LogManager.shared.log(.info, message: "Moving Disk Image '\(temporaryImageURL.path)' to destination '\(imageURL.path)'...")
|
||||
try await FileMover.move(temporaryImageURL, to: imageURL)
|
||||
}
|
||||
]
|
||||
return tasks
|
||||
}
|
||||
|
||||
// swiftlint:disable:next function_body_length
|
||||
private static func isoTasks(for installer: Installer, filename: String, destination destinationURL: URL, temporaryDirectory temporaryDirectoryURL: URL) -> [MistTask] {
|
||||
let temporaryImageURL: URL = temporaryDirectoryURL.appendingPathComponent("\(installer.id).dmg")
|
||||
let createInstallMediaURL: URL = installer.temporaryInstallerURL.appendingPathComponent("Contents/Resources/createinstallmedia")
|
||||
|
@ -429,22 +455,28 @@ class TaskManager: ObservableObject {
|
|||
if installer.mavericksOrNewer {
|
||||
return [
|
||||
MistTask(type: .create, description: "temporary Disk Image") {
|
||||
LogManager.shared.log(.info, message: "Creating temporary Disk Image '\(temporaryImageURL.path)'...")
|
||||
try await DiskImageCreator.create(temporaryImageURL, size: installer.isoSize)
|
||||
},
|
||||
MistTask(type: .mount, description: "temporary Disk Image") {
|
||||
LogManager.shared.log(.info, message: "Mounting temporary Disk Image '\(temporaryImageURL.path)' at mount point '\(installer.temporaryISOMountPointURL.path)'...")
|
||||
try await DiskImageMounter.mount(temporaryImageURL, mountPoint: installer.temporaryISOMountPointURL)
|
||||
},
|
||||
MistTask(type: .create, description: "macOS Installer in temporary Disk Image") {
|
||||
// Workaround to make macOS Sierra 10.12 createinstallmedia work
|
||||
if installer.version.hasPrefix("10.12") {
|
||||
let infoPlistURL: URL = installer.temporaryInstallerURL.appendingPathComponent("Contents/Info.plist")
|
||||
LogManager.shared.log(.info, message: "Updating Property List '\(infoPlistURL.path)'...")
|
||||
try PropertyListUpdater.update(infoPlistURL, key: "CFBundleShortVersionString", value: "12.6.03")
|
||||
}
|
||||
|
||||
// swiftlint:disable:next line_length
|
||||
LogManager.shared.log(.info, message: "Creating macOS Installer in temporary Disk Image at mount point '\(installer.temporaryISOMountPointURL.path)' using createinstallmedia '\(createInstallMediaURL.path)'...")
|
||||
try await InstallMediaCreator.create(createInstallMediaURL, mountPoint: installer.temporaryISOMountPointURL, sierraOrOlder: installer.sierraOrOlder)
|
||||
},
|
||||
MistTask(type: .unmount, description: "temporary Disk Image") {
|
||||
if FileManager.default.fileExists(atPath: installer.temporaryISOMountPointURL.path) {
|
||||
LogManager.shared.log(.info, message: "Unmounting temporary Disk Image at mount point '\(installer.temporaryISOMountPointURL.path)'...")
|
||||
try await DiskImageUnmounter.unmount(installer.temporaryISOMountPointURL)
|
||||
}
|
||||
|
||||
|
@ -455,13 +487,16 @@ class TaskManager: ObservableObject {
|
|||
let url: URL = .init(fileURLWithPath: "/Volumes/Install macOS \(major) beta")
|
||||
|
||||
if FileManager.default.fileExists(atPath: url.path) {
|
||||
LogManager.shared.log(.info, message: "Unmounting temporary Disk Image at mount point '\(url.path)'...")
|
||||
try await DiskImageUnmounter.unmount(url)
|
||||
}
|
||||
},
|
||||
MistTask(type: .convert, description: "temporary Disk Image to ISO") {
|
||||
LogManager.shared.log(.info, message: "Converting temporary Disk Image '\(temporaryImageURL.path)' to ISO '\(temporaryCDRURL.path)'...")
|
||||
try await ISOConverter.convert(temporaryImageURL, destination: temporaryCDRURL)
|
||||
},
|
||||
MistTask(type: .save, description: "ISO to destination") {
|
||||
MistTask(type: .move, description: "ISO to destination") {
|
||||
LogManager.shared.log(.info, message: "Moving ISO '\(temporaryCDRURL.path)' to destination '\(isoURL.path)'...")
|
||||
try await FileMover.move(temporaryCDRURL, to: isoURL)
|
||||
}
|
||||
]
|
||||
|
@ -469,9 +504,11 @@ class TaskManager: ObservableObject {
|
|||
let installESDURL: URL = installer.temporaryInstallerURL.appendingPathComponent("Contents/SharedSupport/InstallESD.dmg")
|
||||
return [
|
||||
MistTask(type: .convert, description: "Installer Disk Image to ISO") {
|
||||
LogManager.shared.log(.info, message: "Converting Installer Disk Image '\(installESDURL.path)' to ISO '\(temporaryCDRURL.path)'...")
|
||||
try await ISOConverter.convert(installESDURL, destination: temporaryCDRURL)
|
||||
},
|
||||
MistTask(type: .save, description: "ISO to destination") {
|
||||
MistTask(type: .move, description: "ISO to destination") {
|
||||
LogManager.shared.log(.info, message: "Moving ISO '\(temporaryCDRURL.path)' to destination '\(isoURL.path)'...")
|
||||
try await FileMover.move(temporaryCDRURL, to: isoURL)
|
||||
}
|
||||
]
|
||||
|
@ -496,7 +533,8 @@ class TaskManager: ObservableObject {
|
|||
let sourceURL: URL = cacheDirectoryURL.appendingPathComponent("InstallAssistant.pkg")
|
||||
|
||||
tasks = [
|
||||
MistTask(type: .save, description: "Package to destination") {
|
||||
MistTask(type: .copy, description: "Package to destination") {
|
||||
LogManager.shared.log(.info, message: "Copying Package '\(sourceURL.path)' to destination '\(packageURL.path)'...")
|
||||
try await FileCopier.copy(sourceURL, to: packageURL)
|
||||
}
|
||||
]
|
||||
|
@ -507,9 +545,11 @@ class TaskManager: ObservableObject {
|
|||
|
||||
tasks = [
|
||||
MistTask(type: .create, description: "Package") {
|
||||
LogManager.shared.log(.info, message: "Creating Package '\(temporaryPackageURL.path)'...")
|
||||
try await PackageCreator.create(temporaryPackageURL, from: installer, identifier: identifier, identity: identity)
|
||||
},
|
||||
MistTask(type: .save, description: "Package to destination") {
|
||||
MistTask(type: .move, description: "Package to destination") {
|
||||
LogManager.shared.log(.info, message: "Moving Package '\(temporaryPackageURL.path)' to destination '\(packageURL.path)'...")
|
||||
try await FileMover.move(temporaryPackageURL, to: packageURL)
|
||||
}
|
||||
]
|
||||
|
@ -526,9 +566,11 @@ class TaskManager: ObservableObject {
|
|||
// Workaround to make macOS Sierra 10.12 createinstallmedia work
|
||||
if installer.version.hasPrefix("10.12") {
|
||||
let infoPlistURL: URL = installer.temporaryInstallerURL.appendingPathComponent("Contents/Info.plist")
|
||||
LogManager.shared.log(.info, message: "Updating Property List '\(infoPlistURL.path)'...")
|
||||
try PropertyListUpdater.update(infoPlistURL, key: "CFBundleShortVersionString", value: "12.6.03")
|
||||
}
|
||||
|
||||
LogManager.shared.log(.info, message: "Creating Bootable Installer at mount point '\(mountPointURL.path)' using createinstallmedia '\(createInstallMediaURL.path)'...")
|
||||
try await InstallMediaCreator.create(createInstallMediaURL, mountPoint: mountPointURL, sierraOrOlder: installer.sierraOrOlder)
|
||||
}
|
||||
]
|
||||
|
@ -539,9 +581,11 @@ class TaskManager: ObservableObject {
|
|||
private static func cleanupTasks(mountPoint mountPointURL: URL, temporaryDirectory temporaryDirectoryURL: URL, cacheDownloads: Bool, cacheDirectory cacheDirectoryURL: URL) -> [MistTask] {
|
||||
var tasks: [MistTask] = [
|
||||
MistTask(type: .unmount, description: "Disk Image") {
|
||||
LogManager.shared.log(.info, message: "Unmounting Disk Image at mount point '\(mountPointURL.path)'...")
|
||||
try await DiskImageUnmounter.unmount(mountPointURL)
|
||||
},
|
||||
MistTask(type: .remove, description: "temporary directory") {
|
||||
LogManager.shared.log(.info, message: "Removing temporary directory '\(temporaryDirectoryURL.path)'...")
|
||||
try await DirectoryRemover.remove(temporaryDirectoryURL)
|
||||
}
|
||||
]
|
||||
|
@ -549,6 +593,7 @@ class TaskManager: ObservableObject {
|
|||
if !cacheDownloads {
|
||||
tasks += [
|
||||
MistTask(type: .remove, description: "cache directory") {
|
||||
LogManager.shared.log(.info, message: "Removing cache directory '\(cacheDirectoryURL.path)'...")
|
||||
try await DirectoryRemover.remove(cacheDirectoryURL)
|
||||
}
|
||||
]
|
||||
|
|
|
@ -2,6 +2,19 @@
|
|||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleURLTypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>Editor</string>
|
||||
<key>CFBundleURLName</key>
|
||||
<string>mist</string>
|
||||
<key>CFBundleURLSchemes</key>
|
||||
<array>
|
||||
<string>mist</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
<key>SMPrivilegedExecutables</key>
|
||||
<dict>
|
||||
<key>com.ninxsoft.mist.helper</key>
|
||||
|
|
|
@ -13,6 +13,7 @@ struct MistApp: App {
|
|||
@NSApplicationDelegateAdaptor(AppDelegate.self)
|
||||
var appDelegate: AppDelegate
|
||||
@StateObject var sparkleUpdater: SparkleUpdater = .init()
|
||||
@StateObject var logManager: LogManager = .shared
|
||||
@State private var refreshing: Bool = false
|
||||
@State private var tasksInProgress: Bool = false
|
||||
|
||||
|
@ -30,6 +31,11 @@ struct MistApp: App {
|
|||
Settings {
|
||||
SettingsView(sparkleUpdater: sparkleUpdater)
|
||||
}
|
||||
WindowGroup("Mist Log") {
|
||||
LogView(logEntries: logManager.logEntries)
|
||||
.handlesExternalEvents(preferring: ["log"], allowing: ["*"])
|
||||
}
|
||||
.handlesExternalEvents(matching: ["log"])
|
||||
}
|
||||
|
||||
func hideZoomButton() {
|
||||
|
|
23
Mist/Model/LogEntry.swift
Normal file
23
Mist/Model/LogEntry.swift
Normal file
|
@ -0,0 +1,23 @@
|
|||
//
|
||||
// LogEntry.swift
|
||||
// Mist
|
||||
//
|
||||
// Created by Nindi Gill on 11/2/2024.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
struct LogEntry: Identifiable {
|
||||
static var example: LogEntry {
|
||||
LogEntry(timestamp: .now, level: .info, message: "Message")
|
||||
}
|
||||
|
||||
let id: String = UUID().uuidString
|
||||
let timestamp: Date
|
||||
let level: LogLevel
|
||||
let message: String
|
||||
|
||||
var description: String {
|
||||
"\(timestamp.ISO8601Format()) \(level.description) \(message)"
|
||||
}
|
||||
}
|
45
Mist/Model/LogLevel.swift
Normal file
45
Mist/Model/LogLevel.swift
Normal file
|
@ -0,0 +1,45 @@
|
|||
//
|
||||
// LogLevel.swift
|
||||
// Mist
|
||||
//
|
||||
// Created by Nindi Gill on 11/2/2024.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
|
||||
enum LogLevel: String, Identifiable, CaseIterable {
|
||||
case info
|
||||
case warning
|
||||
case error
|
||||
|
||||
var id: String {
|
||||
rawValue
|
||||
}
|
||||
|
||||
var description: String {
|
||||
rawValue.capitalized
|
||||
}
|
||||
|
||||
var detailLevelDescription: String {
|
||||
switch self {
|
||||
case .info:
|
||||
"Show all logs"
|
||||
case .warning:
|
||||
"Show errors and warnings"
|
||||
case .error:
|
||||
"Show errors only"
|
||||
}
|
||||
}
|
||||
|
||||
var color: Color {
|
||||
switch self {
|
||||
case .info:
|
||||
.blue
|
||||
case .warning:
|
||||
.yellow
|
||||
case .error:
|
||||
.red
|
||||
}
|
||||
}
|
||||
}
|
|
@ -28,7 +28,7 @@ enum MistError: Error, Equatable {
|
|||
var description: String {
|
||||
switch self {
|
||||
case let .generalError(string):
|
||||
return "Error: \(string)"
|
||||
return string
|
||||
case let .chunklistValidationError(string):
|
||||
return "Chunklist validation failed: \(string)"
|
||||
case let .fileSizeAttributesError(url):
|
||||
|
|
|
@ -23,21 +23,30 @@ struct MistTask: Identifiable {
|
|||
case .pending:
|
||||
break
|
||||
case .inProgress:
|
||||
prefix = "\(prefix.last == "e" ? String(prefix.dropLast(1)) : prefix)ing"
|
||||
switch type {
|
||||
case .configure, .move, .create, .remove:
|
||||
prefix = "\(prefix.dropLast(1))ing"
|
||||
default:
|
||||
prefix = "\(prefix)ing"
|
||||
}
|
||||
suffix = "\(suffix)..."
|
||||
case .complete:
|
||||
switch type {
|
||||
case .download, .codesign, .mount, .unmount, .convert, .compress:
|
||||
prefix = "\(prefix)ed"
|
||||
case .verify:
|
||||
prefix = "Verified"
|
||||
case .configure, .save, .create, .remove:
|
||||
case .configure, .move, .create, .remove:
|
||||
prefix = "\(prefix)d"
|
||||
case .split:
|
||||
break
|
||||
case .download, .codesign, .mount, .unmount, .convert:
|
||||
prefix = "\(prefix)ed"
|
||||
case .verify, .copy:
|
||||
prefix = "\(prefix.dropLast(1))ied"
|
||||
}
|
||||
case .error:
|
||||
prefix = "Error \(prefix.last == "e" ? String(prefix.dropLast(1)).lowercased() : prefix)ing"
|
||||
switch type {
|
||||
case .configure, .move, .create, .remove:
|
||||
prefix = "Error \(prefix.dropLast(1).lowercased())ing"
|
||||
default:
|
||||
prefix = "Error \(prefix.lowercased())ing"
|
||||
}
|
||||
suffix = "\(suffix)..."
|
||||
}
|
||||
|
||||
return "\(prefix) \(suffix)"
|
||||
|
|
|
@ -9,13 +9,12 @@ enum MistTaskType: String {
|
|||
case download = "Download"
|
||||
case verify = "Verify"
|
||||
case configure = "Configure"
|
||||
case save = "Save"
|
||||
case move = "Move"
|
||||
case copy = "Copy"
|
||||
case create = "Create"
|
||||
case remove = "Remove"
|
||||
case codesign = "Codesign"
|
||||
case mount = "Mount"
|
||||
case unmount = "Unmount"
|
||||
case convert = "Convert"
|
||||
case compress = "Compress"
|
||||
case split = "Split"
|
||||
}
|
||||
|
|
|
@ -150,6 +150,7 @@ struct ActivityView: View {
|
|||
|
||||
taskManager.taskGroups[taskGroupIndex].tasks[taskIndex].state = .error
|
||||
error = failure as? MistError ?? MistError.generalError(failure.localizedDescription)
|
||||
LogManager.shared.log(.error, message: error?.description ?? "Fatal Error")
|
||||
alertType = .error
|
||||
showAlert = true
|
||||
|
||||
|
|
|
@ -8,6 +8,8 @@
|
|||
import SwiftUI
|
||||
|
||||
struct ContentView: View {
|
||||
@Environment(\.openURL)
|
||||
var openURL: OpenURLAction
|
||||
@AppStorage("downloadType")
|
||||
private var downloadType: DownloadType = .firmware
|
||||
@AppStorage("includeBetas")
|
||||
|
@ -118,6 +120,13 @@ struct ContentView: View {
|
|||
.foregroundColor(.accentColor)
|
||||
}
|
||||
.help("Refresh")
|
||||
Button {
|
||||
showLog()
|
||||
} label: {
|
||||
Label("Show Log", systemImage: "text.and.command.macwindow")
|
||||
.foregroundColor(.accentColor)
|
||||
}
|
||||
.help("Show Mist Log")
|
||||
}
|
||||
.searchable(text: $searchString)
|
||||
.sheet(isPresented: $refreshing) {
|
||||
|
@ -144,6 +153,14 @@ struct ContentView: View {
|
|||
refreshing = true
|
||||
}
|
||||
|
||||
private func showLog() {
|
||||
guard let url = URL(string: .logURL) else {
|
||||
return
|
||||
}
|
||||
|
||||
openURL(url)
|
||||
}
|
||||
|
||||
private func releaseNames(for type: DownloadType) -> [String] {
|
||||
var releaseNames: [String] = []
|
||||
|
||||
|
|
119
Mist/Views/LogView.swift
Normal file
119
Mist/Views/LogView.swift
Normal file
|
@ -0,0 +1,119 @@
|
|||
//
|
||||
// LogView.swift
|
||||
// Mist
|
||||
//
|
||||
// Created by Nindi Gill on 11/2/2024.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct LogView: View {
|
||||
@AppStorage("logDetailLevel")
|
||||
private var detailLevel: LogLevel = .info
|
||||
var logEntries: [LogEntry]
|
||||
@State private var selectedLogEntries: Set<LogEntry.ID> = []
|
||||
@State private var searchString: String = ""
|
||||
@State private var savePanel: NSSavePanel = .init()
|
||||
private let dateFormatter: DateFormatter = .init()
|
||||
private var filteredLogEntries: [LogEntry] {
|
||||
let filteredLogEntries: [LogEntry] = switch detailLevel {
|
||||
case .info:
|
||||
logEntries
|
||||
case .warning:
|
||||
logEntries.filter { $0.level == .error || $0.level == .warning }
|
||||
case .error:
|
||||
logEntries.filter { $0.level == .error }
|
||||
}
|
||||
|
||||
guard !searchString.isEmpty else {
|
||||
return filteredLogEntries
|
||||
}
|
||||
|
||||
return filteredLogEntries.filter { $0.message.lowercased().contains(searchString.lowercased()) }
|
||||
}
|
||||
|
||||
private let timeColumnWidth: CGFloat = 160
|
||||
private let levelColumnWidth: CGFloat = 80
|
||||
private let levelCircleRadius: CGFloat = 8
|
||||
private let width: CGFloat = 800
|
||||
private let height: CGFloat = 600
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 0) {
|
||||
Table(filteredLogEntries, selection: $selectedLogEntries) {
|
||||
TableColumn("Timestamp") { logEntry in
|
||||
Text(logEntry.timestamp.ISO8601Format())
|
||||
}
|
||||
.width(timeColumnWidth)
|
||||
TableColumn("Level") { logEntry in
|
||||
HStack {
|
||||
Circle()
|
||||
.fill(logEntry.level.color)
|
||||
.frame(width: levelCircleRadius, height: levelCircleRadius)
|
||||
Text(logEntry.level.description)
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
.width(levelColumnWidth)
|
||||
TableColumn("Message") { logEntry in
|
||||
Text(logEntry.message)
|
||||
.help(logEntry.message)
|
||||
}
|
||||
}
|
||||
.textSelection(.enabled)
|
||||
Divider()
|
||||
HStack {
|
||||
Spacer()
|
||||
Button("Export Log...") {
|
||||
export()
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
.frame(minWidth: width, minHeight: height)
|
||||
.toolbar {
|
||||
Picker("Detail Level", selection: $detailLevel) {
|
||||
ForEach(LogLevel.allCases.reversed()) { logLevel in
|
||||
Text(logLevel.detailLevelDescription)
|
||||
.tag(logLevel)
|
||||
}
|
||||
}
|
||||
.help("Detail Level")
|
||||
}
|
||||
.searchable(text: $searchString)
|
||||
}
|
||||
|
||||
private func export() {
|
||||
dateFormatter.dateFormat = "yyyy-MM-dd_HH-mm-ss"
|
||||
let date: String = dateFormatter.string(from: Date())
|
||||
|
||||
savePanel.title = "Export Mist Log"
|
||||
savePanel.prompt = "Export"
|
||||
savePanel.nameFieldStringValue = "Mist Log \(date).log"
|
||||
savePanel.canCreateDirectories = true
|
||||
savePanel.canSelectHiddenExtension = true
|
||||
savePanel.isExtensionHidden = false
|
||||
savePanel.allowedContentTypes = [.log]
|
||||
|
||||
let response: NSApplication.ModalResponse = savePanel.runModal()
|
||||
|
||||
guard
|
||||
response == .OK,
|
||||
let url: URL = savePanel.url else {
|
||||
return
|
||||
}
|
||||
|
||||
do {
|
||||
let string: String = logEntries.map(\.description).joined(separator: "\n")
|
||||
try string.write(to: url, atomically: true, encoding: .utf8)
|
||||
} catch {
|
||||
print(error.localizedDescription)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct LogView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
LogView(logEntries: [.example])
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue