-
Notifications
You must be signed in to change notification settings - Fork 338
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
Nmccann/watch files #138
base: master
Are you sure you want to change the base?
Nmccann/watch files #138
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,20 +1,88 @@ | ||
/** | ||
* Publish | ||
* Copyright (c) John Sundell 2019 | ||
* MIT license, see LICENSE file for details | ||
*/ | ||
* Publish | ||
* Copyright (c) John Sundell 2019 | ||
* MIT license, see LICENSE file for details | ||
*/ | ||
|
||
import Foundation | ||
import Files | ||
import ShellOut | ||
|
||
#if canImport(FileWatcher) | ||
import FileWatcher | ||
#endif | ||
|
||
internal struct WebsiteRunner { | ||
static let nanosecondsPerSecond: UInt64 = 1_000_000_000 | ||
static let normalTerminationStatus = 15 | ||
static let debounceDuration = 3 * nanosecondsPerSecond | ||
static let runLoopInterval: TimeInterval = 0.1 | ||
static let exitMessage = "Press CTRL+C to stop the server and exit" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I was unable to get the folder watching to work while maintaining the |
||
let folder: Folder | ||
var portNumber: Int | ||
let portNumber: Int | ||
let shouldWatch: Bool | ||
|
||
var foldersToWatch: [Folder] { | ||
get throws { | ||
try ["Sources", "Resources", "Content"].map(folder.subfolder(named:)) | ||
} | ||
} | ||
|
||
func run() throws { | ||
let serverProcess: Process = try generateAndRun() | ||
let watchTask = shouldWatch ? watch() : nil | ||
|
||
let interruptHandler = registerInterruptHandler { | ||
watchTask?.cancel() | ||
serverProcess.terminate() | ||
exit(0) | ||
} | ||
|
||
interruptHandler.resume() | ||
|
||
while true { | ||
RunLoop.main.run(until: Date(timeIntervalSinceNow: Self.runLoopInterval)) | ||
} | ||
} | ||
} | ||
|
||
private extension WebsiteRunner { | ||
func registerInterruptHandler(_ handler: @escaping () -> Void) -> DispatchSourceSignal { | ||
let interruptHandler = DispatchSource.makeSignalSource(signal: SIGINT, queue: .main) | ||
|
||
signal(SIGINT, SIG_IGN) | ||
|
||
interruptHandler.setEventHandler(handler: handler) | ||
return interruptHandler | ||
} | ||
|
||
func watch() -> Task<Void, Error>? { | ||
#if canImport(FileWatcher) | ||
return Task.detached { | ||
for try await _ in FileWatcher.changes(on: try foldersToWatch, debouncedBy: Self.debounceDuration) { | ||
print("Changes detected, regenerating...") | ||
let generator = WebsiteGenerator(folder: folder) | ||
do { | ||
try generator.generate() | ||
print(Self.exitMessage) | ||
} catch { | ||
outputErrorMessage("Regeneration failed") | ||
} | ||
} | ||
} | ||
#else | ||
print("File watching not available") | ||
return nil | ||
#endif | ||
} | ||
|
||
func generate() throws { | ||
let generator = WebsiteGenerator(folder: folder) | ||
try generator.generate() | ||
} | ||
|
||
func generateAndRun() throws -> Process { | ||
try generate() | ||
|
||
let outputFolder = try resolveOutputFolder() | ||
|
||
|
@@ -24,7 +92,7 @@ internal struct WebsiteRunner { | |
print(""" | ||
🌍 Starting web server at http://localhost:\(portNumber) | ||
|
||
Press ENTER to stop the server and exit | ||
\(Self.exitMessage) | ||
""") | ||
|
||
serverQueue.async { | ||
|
@@ -44,12 +112,9 @@ internal struct WebsiteRunner { | |
exit(1) | ||
} | ||
|
||
_ = readLine() | ||
serverProcess.terminate() | ||
return serverProcess | ||
} | ||
} | ||
|
||
private extension WebsiteRunner { | ||
func resolveOutputFolder() throws -> Folder { | ||
do { return try folder.subfolder(named: "Output") } | ||
catch { throw CLIError.outputFolderNotFound } | ||
|
@@ -70,6 +135,60 @@ private extension WebsiteRunner { | |
""" | ||
} | ||
|
||
fputs("\n❌ Failed to start local web server:\n\(message)\n", stderr) | ||
outputErrorMessage("Failed to start local web server:\n\(message)") | ||
} | ||
|
||
func outputErrorMessage(_ message: String) { | ||
fputs("\n❌ \(message)\n", stderr) | ||
} | ||
} | ||
|
||
#if canImport(FileWatcher) | ||
private extension FileWatcher { | ||
static func changes(on folders: [Folder], debouncedBy nanoseconds: UInt64?) -> AsyncThrowingStream<String, Error> { | ||
.init { continuation in | ||
let watcher = FileWatcher(folders.map(\.path)) | ||
|
||
var deferredTask: Task<Void, Error>? | ||
|
||
watcher.callback = { event in | ||
guard event.isFileChanged || event.isDirectoryChanged else { | ||
return | ||
} | ||
|
||
guard let nanoseconds = nanoseconds else { | ||
continuation.yield(event.path) | ||
return | ||
} | ||
|
||
deferredTask?.cancel() | ||
|
||
deferredTask = Task { | ||
do { | ||
try await Task.sleep(nanoseconds: nanoseconds) | ||
continuation.yield(event.path) | ||
} catch where !(error is CancellationError) { | ||
continuation.finish() | ||
} | ||
} | ||
} | ||
|
||
watcher.start() | ||
|
||
continuation.onTermination = { _ in | ||
watcher.stop() | ||
} | ||
} | ||
} | ||
} | ||
|
||
private extension FileWatcherEvent { | ||
var isFileChanged: Bool { | ||
fileRenamed || fileRemoved || fileCreated || fileModified | ||
} | ||
|
||
var isDirectoryChanged: Bool { | ||
dirRenamed || dirRemoved || dirCreated || dirModified | ||
} | ||
} | ||
#endif |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Workaround for
NSEC_PER_SEC
not being available on Linux which according to this is a bug. Alternatively, I could have wrapped the NSEC_PER_SEC usage in a#if canImport(FileWatcher)
condition, since it's only used alongside FileWatcher.