Files
ShadowsocksX-NG/ShadowsocksX-NG2/Domain/RuntimeFileStore.swift
T

144 lines
5.2 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import Foundation
/// 运行时文件存取(spec #21 D5,issue #27):`sslocal-active.json` 是 GUI
/// 写、wrapper 读的跨进程交接契约。写入复用 `AtomicFileWriter` 的物理安全
/// 基线(目录 0700 含自愈、临时文件创建即 0600、写全校验后原子替换);显式
/// 停止后的清理只做普通 unlink,不承诺安全擦除(D5)。
struct RuntimeFileStore {
enum PersistenceError: Error, Equatable {
/// 写入时的文件系统错误。
case ioFailure(detail: String)
/// Runtime JSON 写失败后无法恢复此前的 ACL sidecar。
case rollbackFailed(detail: String)
}
let fileURL: URL
private let fileWriter: (Data, URL) throws -> Void
init(
fileURL: URL = RuntimePaths.runtimeFileURL(),
fileWriter: @escaping (Data, URL) throws -> Void = {
try AtomicFileWriter.write($0, to: $1)
}
) {
self.fileURL = fileURL
self.fileWriter = fileWriter
}
/// wrapper pid 文件与契约同目录:GUI 判活与 SIGUSR1 投递依据
/// (SMAppService 不暴露运行中 agent 的 pid)。
var pidFileURL: URL {
fileURL.deletingLastPathComponent().appendingPathComponent("agent.pid")
}
var aclFileURL: URL {
fileURL.deletingLastPathComponent().appendingPathComponent("sslocal-active.acl")
}
var runtimeStatusFileURL: URL {
fileURL.deletingLastPathComponent().appendingPathComponent("agent-runtime-status.json")
}
/// 原子写盘;任一步失败保留原文件。
func write(_ document: SslocalRuntimeDocument) throws {
let data: Data
do {
data = try document.jsonData()
} catch {
throw PersistenceError.ioFailure(detail: String(describing: error))
}
let previousACLData = try? Data(contentsOf: aclFileURL)
if let acl = document.aclRuntime {
guard
acl.path == aclFileURL.standardizedFileURL.path,
acl.isWellFormed
else {
throw PersistenceError.ioFailure(detail: "ACL sidecar path or digest is invalid")
}
do {
try fileWriter(Data(acl.content.utf8), aclFileURL)
} catch {
throw PersistenceError.ioFailure(detail: String(describing: error))
}
}
do {
try fileWriter(data, fileURL)
} catch {
if document.aclRuntime != nil {
do {
if let previousACLData {
try fileWriter(previousACLData, aclFileURL)
} else {
try FileManager.default.removeItem(at: aclFileURL)
}
} catch {
throw PersistenceError.rollbackFailed(detail: String(describing: error))
}
}
throw PersistenceError.ioFailure(detail: String(describing: error))
}
}
/// 读取侧判定与 wrapper 同源:缺失、损坏或结构性无效一律 `nil`。
func loadDocument() -> SslocalRuntimeDocument? {
guard let data = try? Data(contentsOf: fileURL) else { return nil }
guard let document = SslocalRuntimeDocument.decodeValidated(data) else { return nil }
guard let acl = document.aclRuntime else { return document }
guard
acl.path == aclFileURL.standardizedFileURL.path,
let aclData = try? Data(contentsOf: aclFileURL),
aclData == Data(acl.content.utf8)
else { return nil }
return document
}
/// 磁盘原始字节,供与派生文档比较以跳过相同内容的写入(幂等)。
func readData() -> Data? {
try? Data(contentsOf: fileURL)
}
func readRuntimeReceipt() -> RuntimeDeploymentReceipt? {
guard let data = try? Data(contentsOf: runtimeStatusFileURL) else { return nil }
return try? JSONDecoder().decode(RuntimeDeploymentReceipt.self, from: data)
}
func writeRuntimeReceipt(for document: SslocalRuntimeDocument, processID: Int32) throws {
guard let digest = document.deploymentSHA256 else {
throw PersistenceError.ioFailure(detail: "Runtime contract digest is unavailable")
}
let receipt = RuntimeDeploymentReceipt(processID: processID, contractSHA256: digest)
let encoder = JSONEncoder()
encoder.outputFormatting = [.sortedKeys]
do {
try AtomicFileWriter.write(try encoder.encode(receipt), to: runtimeStatusFileURL)
} catch {
throw PersistenceError.ioFailure(detail: String(describing: error))
}
}
/// 显式停止清理(D2 停止协议末端):运行时契约文件、wrapper pid 文件与
/// 原子写残留的临时文件。异常崩溃路径不调用本方法(文件保留供 KeepAlive
/// 重放,D5)。文件缺失时调用无害。
func deleteRuntimeFiles() {
let fileManager = FileManager.default
try? fileManager.removeItem(at: fileURL)
try? fileManager.removeItem(at: pidFileURL)
try? fileManager.removeItem(at: aclFileURL)
try? fileManager.removeItem(at: runtimeStatusFileURL)
let directory = fileURL.deletingLastPathComponent()
guard let contents = try? fileManager.contentsOfDirectory(atPath: directory.path) else {
return
}
let temporaryPrefixes = [
".\(fileURL.lastPathComponent).tmp-",
".\(aclFileURL.lastPathComponent).tmp-",
".\(runtimeStatusFileURL.lastPathComponent).tmp-",
]
for name in contents where temporaryPrefixes.contains(where: name.hasPrefix) {
try? fileManager.removeItem(at: directory.appendingPathComponent(name))
}
}
}