Skip to content

[Workspace] Add ability to notify when Package.resolved file is changed #2171

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

Merged
merged 1 commit into from
Jun 14, 2019
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
34 changes: 12 additions & 22 deletions Sources/SPMUtility/FSWatch.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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)
Expand All @@ -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
Expand Down Expand Up @@ -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
45 changes: 45 additions & 0 deletions Sources/Workspace/PinsStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
}
29 changes: 29 additions & 0 deletions Sources/Workspace/Workspace.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -53,6 +58,7 @@ public extension WorkspaceDelegate {
func repositoryDidUpdate(_ repository: String) {}
func willResolveDependencies() {}
func dependenciesUpToDate() {}
func resolvedFileChanged() {}
}

private class WorkspaceResolverDelegate: DependencyResolverDelegate {
Expand Down Expand Up @@ -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<PinsStore>

/// 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

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -904,13 +916,30 @@ 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()
}
}

// MARK: - Utility Functions

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 {
Expand Down