Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Clean Up Child Processes #1885

Merged
merged 16 commits into from
Sep 25, 2024
Merged
Changes from 15 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
56 changes: 54 additions & 2 deletions CodeEdit/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,17 @@
import SwiftUI
import CodeEditSymbols
import CodeEditSourceEditor
import OSLog

final class AppDelegate: NSObject, NSApplicationDelegate, ObservableObject {
private let logger = Logger(subsystem: Bundle.main.bundleIdentifier ?? "", category: "AppDelegate")
private let updater = SoftwareUpdater()

@Environment(\.openWindow)
var openWindow

@LazyService var lspService: LSPService

func applicationDidFinishLaunching(_ notification: Notification) {
setupServiceContainer()
enableWindowSizeSaveOnQuit()
Expand Down Expand Up @@ -115,6 +119,20 @@ final class AppDelegate: NSObject, NSApplicationDelegate, ObservableObject {
}
}

/// Defers the application terminate message until we've finished cleanup.
///
/// All paths _must_ call `NSApplication.shared.reply(toApplicationShouldTerminate: true)` as soon as possible.
///
/// The two things needing deferring are:
/// - Language server cancellation
/// - Outstanding document changes.
///
/// Things that don't need deferring (happen immediately):
/// - Task termination.
/// These are called immediately if no documents need closing, and are called by
/// ``documentController(_:didCloseAll:contextInfo:)`` if there are documents we need to defer for.
///
/// See ``terminateLanguageServers()`` and ``documentController(_:didCloseAll:contextInfo:)`` for deferring tasks.
func applicationShouldTerminate(_ sender: NSApplication) -> NSApplication.TerminateReply {
let projects: [String] = CodeEditDocumentController.shared.documents
.compactMap { ($0 as? WorkspaceDocument)?.fileURL?.path }
Expand All @@ -128,10 +146,13 @@ final class AppDelegate: NSObject, NSApplicationDelegate, ObservableObject {
didCloseAllSelector: #selector(documentController(_:didCloseAll:contextInfo:)),
contextInfo: nil
)
// `documentController(_:didCloseAll:contextInfo:)` will call `terminateLanguageServers()`
return .terminateLater
}

return .terminateNow
terminateTasks()
terminateLanguageServers()
return .terminateLater
}

func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
Expand Down Expand Up @@ -224,7 +245,38 @@ final class AppDelegate: NSObject, NSApplicationDelegate, ObservableObject {

@objc
func documentController(_ docController: NSDocumentController, didCloseAll: Bool, contextInfo: Any) {
NSApplication.shared.reply(toApplicationShouldTerminate: didCloseAll)
if didCloseAll {
terminateTasks()
terminateLanguageServers()
}
}

/// Terminates running language servers. Used during app termination to ensure resources are freed.
private func terminateLanguageServers() {
Task {
await withTaskGroup(of: Void.self) { group in
await lspService.languageClients.forEach { (key, value) in
thecoolwinter marked this conversation as resolved.
Show resolved Hide resolved
group.addTask {
do {
try await value.shutdown()
} catch {
self.logger.error("Shutting down \(key.languageId.rawValue): Error \(error)")
}
}
}
}
await MainActor.run {
NSApplication.shared.reply(toApplicationShouldTerminate: true)
}
}
}

/// Terminates all running tasks. Used during app termination to ensure resources are freed.
private func terminateTasks() {
let documents = CodeEditDocumentController.shared.documents.compactMap({ $0 as? WorkspaceDocument })
documents.forEach { workspace in
workspace.taskManager?.stopAllTasks()
}
}

/// Setup all the services into a ServiceContainer for the application to use.
Expand Down
Loading