mirror of
https://github.com/outbackdingo/firezone.git
synced 2026-01-27 10:18:54 +00:00
This PR replaces the use of Apple Archive with an API that allows us to zip the log file contents. This API doesn't handle symlinks well so we move the symlink out of the way before making the zip. The symlink is then moved back after the process is completed. Any errors in this process are ignored as the symlink itself is not a critical component of Firezone. The zip compression is marginally less efficient than the Apple Archive. Instead of compressing ~2GB of logs to 11.8 MB we now get an archive of 12.4 MB. Considering how much easier zip files are to handle, this seems like a fine trade-off. <img width="774" alt="Screenshot 2025-06-16 at 00 04 52" src="https://github.com/user-attachments/assets/8fb6bade-5308-40b9-a446-2a2c364cb621" /> Resolves: #7475 --------- Signed-off-by: Thomas Eizinger <thomas@eizinger.io> Co-authored-by: Jamil Bou Kheir <jamilbk@users.noreply.github.com>
101 lines
2.9 KiB
Swift
101 lines
2.9 KiB
Swift
//
|
|
// TunnelArchiveByteStream.swift
|
|
// (c) 2024 Firezone, Inc.
|
|
// LICENSE: Apache-2.0
|
|
//
|
|
|
|
import FirezoneKit
|
|
import Foundation
|
|
import System
|
|
|
|
/// We must enable the app sandbox when distributing the macOS client in the App Store. Since the tunnel
|
|
/// process runs as root, this makes sharing log files between the app process (running as the
|
|
/// logged in user) and tunnel process (running as root) tricky. The app process can't read or write directly to
|
|
/// the tunnel's log directory, and vice-versa for the tunnel process and app log directory.
|
|
///
|
|
/// The way we overcome this is IPC. This gets tricky with exporting logs, however. We can't
|
|
/// simply read the tunnel log directory into a giant buffer as this could be too large to send over the IPC
|
|
/// channel. Instead, we write the tunnel log archive to a temp file and then chunk it over with IPC.
|
|
///
|
|
/// Since the IPC channel is unidirectional from app -> tunnel, we use a simple data format to pass chunks
|
|
/// of this archive file from the tunnel back to the app, including a boolean `done` to indicate when the
|
|
/// archive is sent and the app should close its associated file.
|
|
///
|
|
/// Currently this limit is set to 1 MB (chosen somewhat arbitrarily based on limited information found on the
|
|
/// web), but can be easily enlarged in the future to reduce the number of IPC calls required to consume
|
|
/// the entire archive.
|
|
class TunnelLogArchive {
|
|
enum ArchiveError: Error {
|
|
case unableToWriteArchive
|
|
case unableToReadArchive
|
|
}
|
|
|
|
let chunkSize = 1024 * 1024 // 1 MiB
|
|
let encoder = PropertyListEncoder()
|
|
let archiveURL = FileManager
|
|
.default
|
|
.temporaryDirectory
|
|
.appendingPathComponent("logs.zip")
|
|
|
|
var offset: UInt64 = 0
|
|
var fileHandle: FileHandle?
|
|
var source: URL
|
|
|
|
init(source: URL) {
|
|
self.source = source
|
|
}
|
|
|
|
deinit {
|
|
cleanup()
|
|
}
|
|
|
|
func archive() throws {
|
|
try? FileManager.default.removeItem(at: self.archiveURL)
|
|
|
|
try ZipService.createZip(
|
|
source: source,
|
|
to: self.archiveURL
|
|
)
|
|
}
|
|
|
|
func readChunk() throws -> (Data, Bool) {
|
|
if self.fileHandle == nil {
|
|
// Open the file for reading
|
|
try self.fileHandle = FileHandle(forReadingFrom: archiveURL)
|
|
}
|
|
|
|
guard let fileHandle = self.fileHandle
|
|
else {
|
|
throw ArchiveError.unableToReadArchive
|
|
}
|
|
|
|
// Read archive at offset up to chunkSize bytes
|
|
try fileHandle.seek(toOffset: self.offset)
|
|
guard let data = try fileHandle.read(upToCount: chunkSize)
|
|
else {
|
|
throw ArchiveError.unableToReadArchive
|
|
}
|
|
|
|
self.offset += UInt64(data.count)
|
|
|
|
let chunk = LogChunk(
|
|
done: data.count < chunkSize, // we're done if we read less than chunkSize
|
|
data: data
|
|
)
|
|
|
|
if chunk.done {
|
|
cleanup()
|
|
}
|
|
|
|
return try (encoder.encode(chunk), chunk.done)
|
|
}
|
|
|
|
func cleanup() {
|
|
try? self.fileHandle?.close()
|
|
try? FileManager.default.removeItem(at: archiveURL)
|
|
|
|
self.offset = 0
|
|
self.fileHandle = nil
|
|
}
|
|
}
|