Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .github/workflows/ci-macos.yml
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ jobs:
needs: changes
if: needs.changes.outputs.full == 'true'
runs-on: macos-14
timeout-minutes: 20
timeout-minutes: 30

steps:
- name: Check out source
Expand All @@ -104,12 +104,12 @@ jobs:
- name: Run Swift tests
id: swift-tests-primary
continue-on-error: true
timeout-minutes: 8
timeout-minutes: 12
run: ./scripts/test-macos.sh

- name: Retry Swift tests after a failed attempt
if: steps.swift-tests-primary.outcome == 'failure'
timeout-minutes: 8
timeout-minutes: 12
run: ./scripts/test-macos.sh --skip-build

rust-tests:
Expand Down
7 changes: 7 additions & 0 deletions Resources/zh-Hans.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,13 @@
"The interface language changes immediately. English is the default." = "界面语言会立即生效。默认语言为英文。";
"Files" = "文件";
"Save changed files automatically" = "自动保存已修改的文件";
"Logs" = "日志";
"Log directory" = "日志目录";
"Default directory" = "默认目录";
"Selected directory" = "当前选择的目录";
"Choose Directory" = "选择目录";
"Choose Log Directory" = "选择日志目录";
"Restore Default" = "恢复默认";
"Save after" = "保存延迟";
"Hidden paths" = "隐藏路径";
"One entry per line. Directory names hide matching folders; file entries support * and ?." = "每行输入一项。目录名用于隐藏匹配的文件夹;文件条目支持 * 和 ?。";
Expand Down
5 changes: 5 additions & 0 deletions Sources/Lithe/Core/Ports/LogDirectoryProviding.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import Foundation

protocol LogDirectoryProviding {
var defaultLogDirectory: URL { get }
}
55 changes: 51 additions & 4 deletions Sources/Lithe/LitheApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -133,11 +133,29 @@ struct LitheApp: App {
@StateObject private var memoryUsageMonitor: MemoryUsageMonitor
@StateObject private var frameRateMonitor = FrameRateMonitor()
@StateObject private var updateChecker = UpdateChecker()
private let applicationLogWriter: MacApplicationLogWriter

init() {
MacBundledFontRegistry.registerFonts()
let store = MacUserDefaultsStore()
let settings = AppSettings(store: store)
let settings = AppSettings(
store: store,
logDirectoryProvider: MacServiceContainer.makeLogDirectoryProvider()
)
let applicationLogWriter = MacServiceContainer.makeApplicationLogWriter()
if !Self.redirectApplicationLogs(applicationLogWriter, to: settings.logDirectory),
settings.customLogDirectory != nil {
settings.setCustomLogDirectory(nil)
_ = Self.redirectApplicationLogs(applicationLogWriter, to: settings.defaultLogDirectory)
}
settings.addLogDirectoryObserver { [weak settings] directory in
guard !Self.redirectApplicationLogs(applicationLogWriter, to: directory),
settings?.customLogDirectory != nil else { return }
settings?.setCustomLogDirectory(nil)
}
self.applicationLogWriter = applicationLogWriter
MacBundledFontRegistry.registerFonts { message in
Self.appendApplicationLog(applicationLogWriter, message: message)
}
let processRegistry = ManagedProcessRegistry()
let moduleStore = MacModuleConfigurationStore(store: store)
let pluginRuntimeRecovery = MacPluginRuntimeRecoveryCoordinator()
Expand Down Expand Up @@ -171,8 +189,7 @@ struct LitheApp: App {
_memoryUsageMonitor = StateObject(wrappedValue: MemoryUsageMonitor(
startedAt: litheProcessLaunchDate,
baselineReporter: { marker in
guard let data = (marker + "\n").data(using: .utf8) else { return }
FileHandle.standardError.write(data)
Self.appendApplicationLog(applicationLogWriter, message: marker + "\n")
},
logsPerformanceBaseline: ProcessInfo.processInfo.environment["LITHE_PERFORMANCE_BASELINE"] == "1",
processRegistry: processRegistry,
Expand All @@ -185,6 +202,36 @@ struct LitheApp: App {
}
}

private static func redirectApplicationLogs(
_ writer: MacApplicationLogWriter,
to directory: URL
) -> Bool {
do {
try writer.redirect(to: directory)
return true
} catch {
let message = "Could not redirect Lithe logs to \(directory.path): \(error.localizedDescription)\n"
if let data = message.data(using: .utf8) {
FileHandle.standardError.write(data)
}
return false
}
}

private static func appendApplicationLog(
_ writer: MacApplicationLogWriter,
message: String
) {
do {
try writer.append(message)
} catch {
let fallback = "Could not write Lithe log: \(error.localizedDescription)\n"
if let data = fallback.data(using: .utf8) {
FileHandle.standardError.write(data)
}
}
}

private var model: AppModel { projectSessions.activeModel }

var body: some Scene {
Expand Down
36 changes: 35 additions & 1 deletion Sources/Lithe/Models/Settings/AppSettings.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ final class AppSettings: ObservableObject {
static let javaLanguageServerJDKPath = "settings.javaLanguageServerJDKPath"
static let commitMessageAI = "settings.commitMessageAI"
static let keyboardShortcutOverrides = "settings.keyboardShortcutOverrides"
static let customLogDirectory = "settings.customLogDirectory"
}

private struct KeyboardShortcutOverridesPayload: Codable {
Expand All @@ -32,6 +33,7 @@ final class AppSettings: ObservableObject {
}

private let defaults: any KeyValueStore
private let logDirectoryProvider: any LogDirectoryProviding

@Published var colorTheme: AppColorTheme {
didSet {
Expand Down Expand Up @@ -77,11 +79,17 @@ final class AppSettings: ObservableObject {
didSet { saveCommitMessageAI() }
}
@Published private(set) var keyboardShortcutOverrides: [String: [KeyboardShortcutBinding]]
@Published private(set) var customLogDirectory: URL?

private var fileVisibilityRulesObservers: [UUID: () -> Void] = [:]
private var logDirectoryObservers: [UUID: (URL) -> Void] = [:]

init(store: any KeyValueStore) {
init(
store: any KeyValueStore,
logDirectoryProvider: any LogDirectoryProviding
) {
self.defaults = store
self.logDirectoryProvider = logDirectoryProvider
colorTheme = AppColorTheme(
rawValue: defaults.string(forKey: Key.colorTheme) ?? ""
) ?? .lithe
Expand Down Expand Up @@ -110,6 +118,10 @@ final class AppSettings: ObservableObject {
) ?? .ask
javaLanguageServerJDKPath = defaults.string(forKey: Key.javaLanguageServerJDKPath) ?? ""
keyboardShortcutOverrides = Self.loadKeyboardShortcutOverrides(from: defaults)
customLogDirectory = defaults.string(forKey: Key.customLogDirectory).flatMap { path in
guard !path.isEmpty else { return nil }
return URL(fileURLWithPath: path, isDirectory: true).standardizedFileURL
}
if let data = defaults.data(forKey: Key.commitMessageAI),
let saved = try? JSONDecoder().decode(CommitMessageAISettings.self, from: data) {
commitMessageAI = saved
Expand All @@ -121,6 +133,27 @@ final class AppSettings: ObservableObject {

var terminalShellPath: String? { terminalShell.path }

var defaultLogDirectory: URL {
logDirectoryProvider.defaultLogDirectory
}

var logDirectory: URL { customLogDirectory ?? defaultLogDirectory }
Comment thread
1lck marked this conversation as resolved.

func setCustomLogDirectory(_ url: URL?) {
customLogDirectory = url?.standardizedFileURL
defaults.set(customLogDirectory?.path, forKey: Key.customLogDirectory)
for observer in logDirectoryObservers.values {
observer(logDirectory)
}
}

@discardableResult
func addLogDirectoryObserver(_ observer: @escaping (URL) -> Void) -> UUID {
let id = UUID()
logDirectoryObservers[id] = observer
return id
}

var fileVisibilityRules: FileVisibilityRules {
FileVisibilityRules(
hiddenDirectoryNames: hiddenDirectoryNames,
Expand Down Expand Up @@ -162,6 +195,7 @@ final class AppSettings: ObservableObject {
projectOpenBehavior = .ask
javaLanguageServerJDKPath = ""
commitMessageAI = .default
setCustomLogDirectory(nil)
setKeyboardShortcutOverrides([:])
}

Expand Down
55 changes: 55 additions & 0 deletions Sources/Lithe/Platform/MacOS/Logging/MacApplicationLogWriter.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import Darwin
import Foundation

final class MacApplicationLogWriter {
static let fileName = "lithe.log"

private let lock = NSLock()
private var directory: URL?

func redirect(to directory: URL) throws {
try FileManager.default.createDirectory(
at: directory,
withIntermediateDirectories: true
)
let logURL = directory.appendingPathComponent(Self.fileName, isDirectory: false)
let descriptor: Int32 = logURL.withUnsafeFileSystemRepresentation { path in
guard let path else { return Int32(-1) }
return open(path, O_WRONLY | O_CREAT | O_APPEND, S_IRUSR | S_IWUSR)
}
guard descriptor >= 0 else {
throw POSIXError(POSIXErrorCode(rawValue: errno) ?? .EIO)
}
close(descriptor)

lock.lock()
self.directory = directory
lock.unlock()
}

func append(_ message: String) throws {
lock.lock()
defer { lock.unlock() }
guard let directory else {
throw CocoaError(.fileNoSuchFile)
}

let logURL = directory.appendingPathComponent(Self.fileName, isDirectory: false)
let descriptor: Int32 = logURL.withUnsafeFileSystemRepresentation { path in
guard let path else { return Int32(-1) }
return open(path, O_WRONLY | O_CREAT | O_APPEND, S_IRUSR | S_IWUSR)
}
guard descriptor >= 0 else {
throw POSIXError(POSIXErrorCode(rawValue: errno) ?? .EIO)
}
defer { close(descriptor) }

let data = Data(message.utf8)
let written = data.withUnsafeBytes { bytes in
Darwin.write(descriptor, bytes.baseAddress, bytes.count)
}
guard written == data.count else {
throw POSIXError(POSIXErrorCode(rawValue: errno) ?? .EIO)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import Foundation

struct MacLogDirectoryProvider: LogDirectoryProviding {
var defaultLogDirectory: URL {
FileManager.default.urls(for: .libraryDirectory, in: .userDomainMask)[0]
.appendingPathComponent("Logs/Lithe", isDirectory: true)
}
}
8 changes: 8 additions & 0 deletions Sources/Lithe/Platform/MacOS/MacServiceContainer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,14 @@ final class MacServiceContainer {
let runConfigurationStore: MacRunConfigurationStore
let moduleLifecycleCoordinator: ModuleLifecycleCoordinator

static func makeLogDirectoryProvider() -> any LogDirectoryProviding {
MacLogDirectoryProvider()
}

static func makeApplicationLogWriter() -> MacApplicationLogWriter {
MacApplicationLogWriter()
}

init(
store: any KeyValueStore,
settings: AppSettings,
Expand Down
13 changes: 10 additions & 3 deletions Sources/Lithe/Platform/MacOS/UI/MacBundledFontRegistry.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,13 @@ enum MacBundledFontRegistry {
]

static func registerFonts(bundle: Bundle = .main) {
registerFonts(bundle: bundle, reporter: report)
}

static func registerFonts(
bundle: Bundle = .main,
reporter: (String) -> Void
) {
guard bundle.url(forResource: "JetBrainsMono-Regular", withExtension: "ttf", subdirectory: "Fonts") != nil else {
return
}
Expand All @@ -21,22 +28,22 @@ enum MacBundledFontRegistry {
withExtension: "ttf",
subdirectory: "Fonts"
) else {
report("Missing bundled font: \(font.resource).ttf")
reporter("Lithe font registration: Missing bundled font: \(font.resource).ttf\n")
continue
}

var registrationError: Unmanaged<CFError>?
guard CTFontManagerRegisterFontsForURL(url as CFURL, .process, &registrationError) else {
let detail = registrationError?.takeRetainedValue().localizedDescription
?? "Unknown CoreText error"
report("Could not register \(font.resource).ttf: \(detail)")
reporter("Lithe font registration: Could not register \(font.resource).ttf: \(detail)\n")
continue
}
}
}

private static func report(_ message: String) {
guard let data = ("Lithe font registration: \(message)\n").data(using: .utf8) else { return }
guard let data = message.data(using: .utf8) else { return }
FileHandle.standardError.write(data)
}
}
Loading
Loading