diff --git a/Sources/SPMUtility/FSWatch.swift b/Sources/SPMUtility/FSWatch.swift index cbb80a50174..70b12765cd3 100644 --- a/Sources/SPMUtility/FSWatch.swift +++ b/Sources/SPMUtility/FSWatch.swift @@ -13,35 +13,23 @@ import Dispatch import Foundation import SPMLibc -public protocol FSWatchDelegate { - // FIXME: We need to provide richer information about the events. - func pathsDidReceiveEvent(_ paths: [AbsolutePath]) -} - /// FSWatch is a cross-platform filesystem watching utility. public class FSWatch { - /// Delegate for handling events from the underling watcher. - fileprivate class _WatcherDelegate { - - /// Back reference to the fswatch instance. - unowned let fsWatch: FSWatch + public typealias EventReceivedBlock = (_ paths: [AbsolutePath]) -> Void - init(_ fsWatch: FSWatch) { - self.fsWatch = fsWatch - } + /// Delegate for handling events from the underling watcher. + fileprivate struct _WatcherDelegate { + let block: EventReceivedBlock func pathsDidReceiveEvent(_ paths: [AbsolutePath]) { - fsWatch.delegate.pathsDidReceiveEvent(paths) + block(paths) } } /// The paths being watched. public let paths: [AbsolutePath] - /// The delegate for reporting received events. - let delegate: FSWatchDelegate - /// The underlying file watching utility. /// /// This is FSEventStream on macOS and inotify on linux. @@ -54,10 +42,9 @@ public class FSWatch { /// Create an instance with given paths. /// /// Paths can be files or directories. Directories are watched recursively. - public init(paths: [AbsolutePath], latency: Double = 1, delegate: FSWatchDelegate) { + public init(paths: [AbsolutePath], latency: Double = 1, block: @escaping EventReceivedBlock) { precondition(!paths.isEmpty) self.paths = paths - self.delegate = delegate self.latency = latency #if canImport(Glibc) @@ -75,9 +62,9 @@ public class FSWatch { } } - self._watcher = Inotify(paths: ipaths, latency: latency, delegate: _WatcherDelegate(self)) + self._watcher = Inotify(paths: ipaths, latency: latency, delegate: _WatcherDelegate(block: block)) #elseif os(macOS) - self._watcher = FSEventStream(paths: paths, latency: latency, delegate: _WatcherDelegate(self)) + self._watcher = FSEventStream(paths: paths, latency: latency, delegate: _WatcherDelegate(block: block)) #else fatalError("Unsupported platform") #endif @@ -603,7 +590,10 @@ public final class FSEventStream { /// Stop watching the events. public func stop() { - CFRunLoopStop(runLoop!) + // FIXME: This is probably not thread safe? + if let runLoop = self.runLoop { + CFRunLoopStop(runLoop) + } } } #endif diff --git a/Sources/Workspace/PinsStore.swift b/Sources/Workspace/PinsStore.swift index df2c8d48d4d..fd2449c2e5f 100644 --- a/Sources/Workspace/PinsStore.swift +++ b/Sources/Workspace/PinsStore.swift @@ -182,3 +182,48 @@ extension PinsStore.Pin: JSONMappable, JSONSerializable, Equatable { ]) } } + +/// A file watcher utility for the Package.resolved file. +/// +/// This is not intended to be used directly by clients. +final class ResolvedFileWatcher { + private var fswatch: FSWatch! + private var existingValue: ByteString? + private let valueLock: Lock = Lock() + private let resolvedFile: AbsolutePath + + public func updateValue() { + valueLock.withLock { + self.existingValue = try? localFileSystem.readFileContents(resolvedFile) + } + } + + init(resolvedFile: AbsolutePath, onChange: @escaping () -> ()) throws { + self.resolvedFile = resolvedFile + + let block = { [weak self] (paths: [AbsolutePath]) in + guard let self = self else { return } + + // Check if resolved file is part of the received paths. + let hasResolvedFile = paths.contains{ $0.appending(component: resolvedFile.basename) == resolvedFile } + guard hasResolvedFile else { return } + + self.valueLock.withLock { + // Compute the contents of the resolved file and fire the onChange block + // if its value is different than existing value. + let newValue = try? localFileSystem.readFileContents(resolvedFile) + if self.existingValue != newValue { + self.existingValue = newValue + onChange() + } + } + } + + fswatch = FSWatch(paths: [resolvedFile.parentDirectory], latency: 1, block: block) + try fswatch.start() + } + + deinit { + fswatch.stop() + } +} diff --git a/Sources/Workspace/Workspace.swift b/Sources/Workspace/Workspace.swift index 65f2dde8774..5e219dc9b73 100644 --- a/Sources/Workspace/Workspace.swift +++ b/Sources/Workspace/Workspace.swift @@ -45,6 +45,11 @@ public protocol WorkspaceDelegate: class { /// Called when the resolver is about to be run. func willResolveDependencies() + + /// Called when the Package.resolved file is changed *outside* of libSwiftPM operations. + /// + /// This is only fired when activated using Workspace's watchResolvedFile() method. + func resolvedFileChanged() } public extension WorkspaceDelegate { @@ -53,6 +58,7 @@ public extension WorkspaceDelegate { func repositoryDidUpdate(_ repository: String) {} func willResolveDependencies() {} func dependenciesUpToDate() {} + func resolvedFileChanged() {} } private class WorkspaceResolverDelegate: DependencyResolverDelegate { @@ -252,6 +258,9 @@ public class Workspace { /// The Pins store. The pins file will be created when first pin is added to pins store. public let pinsStore: LoadableResult + /// The path to the Package.resolved file for this workspace. + public let resolvedFile: AbsolutePath + /// The path for working repository clones (checkouts). public let checkoutsPath: AbsolutePath @@ -291,6 +300,8 @@ public class Workspace { /// Write dependency resolver trace to a file. fileprivate let enableResolverTrace: Bool + fileprivate var resolvedFileWatcher: ResolvedFileWatcher? + /// Typealias for dependency resolver we use in the workspace. fileprivate typealias PackageDependencyResolver = DependencyResolver fileprivate typealias PubgrubResolver = PubgrubDependencyResolver @@ -336,6 +347,7 @@ public class Workspace { self.enablePubgrubResolver = enablePubgrubResolver self.skipUpdate = skipUpdate self.enableResolverTrace = enableResolverTrace + self.resolvedFile = pinsFile let repositoriesPath = self.dataPath.appending(component: "repositories") self.repositoryManager = RepositoryManager( @@ -904,6 +916,10 @@ extension Workspace { } } diagnostics.wrap({ try pinsStore.saveState() }) + + // Ask resolved file watcher to update its value so we don't fire + // an extra event if the file was modified by us. + self.resolvedFileWatcher?.updateValue() } } @@ -911,6 +927,19 @@ extension Workspace { extension Workspace { + /// Watch the Package.resolved for changes. + /// + /// This is useful if clients want to be notified when the Package.resolved + /// file is changed *outside* of libSwiftPM operations. For example, as part + /// of a git operation. + public func watchResolvedFile() throws { + // Return if we're already watching it. + guard self.resolvedFileWatcher == nil else { return } + self.resolvedFileWatcher = try ResolvedFileWatcher(resolvedFile: self.resolvedFile) { [weak self] in + self?.delegate?.resolvedFileChanged() + } + } + /// Create the cache directories. fileprivate func createCacheDirectories(with diagnostics: DiagnosticsEngine) { do {