diff --git a/PHP Monitor.xcodeproj/project.pbxproj b/PHP Monitor.xcodeproj/project.pbxproj index 67bc156..a6967db 100644 --- a/PHP Monitor.xcodeproj/project.pbxproj +++ b/PHP Monitor.xcodeproj/project.pbxproj @@ -90,6 +90,10 @@ C4B97B79275CF1B5003F3378 /* App+ActivationPolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4B97B77275CF1B5003F3378 /* App+ActivationPolicy.swift */; }; C4B97B7B275CF20A003F3378 /* App+GlobalHotkey.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4B97B7A275CF20A003F3378 /* App+GlobalHotkey.swift */; }; C4B97B7C275CF20A003F3378 /* App+GlobalHotkey.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4B97B7A275CF20A003F3378 /* App+GlobalHotkey.swift */; }; + C4C8E818276F54D8003AC782 /* App+ConfigWatch.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4C8E817276F54D8003AC782 /* App+ConfigWatch.swift */; }; + C4C8E819276F54D8003AC782 /* App+ConfigWatch.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4C8E817276F54D8003AC782 /* App+ConfigWatch.swift */; }; + C4C8E81B276F54E5003AC782 /* PhpConfigWatcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4C8E81A276F54E5003AC782 /* PhpConfigWatcher.swift */; }; + C4C8E81C276F54E5003AC782 /* PhpConfigWatcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4C8E81A276F54E5003AC782 /* PhpConfigWatcher.swift */; }; C4CCBA6C275C567B008C7055 /* PMWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4CCBA6B275C567B008C7055 /* PMWindowController.swift */; }; C4CCBA6D275C567B008C7055 /* PMWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4CCBA6B275C567B008C7055 /* PMWindowController.swift */; }; C4D8016622B1584700C6DA1B /* Startup.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4D8016522B1584700C6DA1B /* Startup.swift */; }; @@ -197,6 +201,8 @@ C4B97B74275CF08C003F3378 /* AppDelegate+MenuOutlets.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AppDelegate+MenuOutlets.swift"; sourceTree = ""; }; C4B97B77275CF1B5003F3378 /* App+ActivationPolicy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "App+ActivationPolicy.swift"; sourceTree = ""; }; C4B97B7A275CF20A003F3378 /* App+GlobalHotkey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "App+GlobalHotkey.swift"; sourceTree = ""; }; + C4C8E817276F54D8003AC782 /* App+ConfigWatch.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "App+ConfigWatch.swift"; sourceTree = ""; }; + C4C8E81A276F54E5003AC782 /* PhpConfigWatcher.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhpConfigWatcher.swift; sourceTree = ""; }; C4CCBA6B275C567B008C7055 /* PMWindowController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PMWindowController.swift; sourceTree = ""; }; C4D8016522B1584700C6DA1B /* Startup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Startup.swift; sourceTree = ""; }; C4E713562570150F00007428 /* SECURITY.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = SECURITY.md; sourceTree = ""; }; @@ -421,7 +427,9 @@ C436039F275E67610028EFC6 /* AppDelegate+Notifications.swift */, C4811D2322D70A4700B5F6B3 /* App.swift */, C4B97B77275CF1B5003F3378 /* App+ActivationPolicy.swift */, + C4C8E817276F54D8003AC782 /* App+ConfigWatch.swift */, C4B97B7A275CF20A003F3378 /* App+GlobalHotkey.swift */, + C4C8E81A276F54E5003AC782 /* PhpConfigWatcher.swift */, C4D8016522B1584700C6DA1B /* Startup.swift */, C41C1B4C22B0215A00E7CF16 /* Actions.swift */, ); @@ -597,6 +605,7 @@ C4AF9F7A2754499000D44ED0 /* Valet.swift in Sources */, 5420395926135DC100FB00FA /* PrefsVC.swift in Sources */, C43603A0275E67610028EFC6 /* AppDelegate+Notifications.swift in Sources */, + C4C8E818276F54D8003AC782 /* App+ConfigWatch.swift in Sources */, 54FCFD30276C8DA4004CE748 /* HotkeyPreferenceView.swift in Sources */, C41C1B4722B009A400E7CF16 /* Shell.swift in Sources */, C4F2E43A2752F7D00020E974 /* PhpInstallation.swift in Sources */, @@ -618,6 +627,7 @@ C4811D2A22D70F9A00B5F6B3 /* MainMenu.swift in Sources */, C412E5FC25700D5300A1FB67 /* HomebrewPackage.swift in Sources */, 54AB03262763858F00A29D5F /* Timer.swift in Sources */, + C4C8E81B276F54E5003AC782 /* PhpConfigWatcher.swift in Sources */, C41C1B3722B0097F00E7CF16 /* AppDelegate.swift in Sources */, C42759672627662800093CAE /* NSMenuExtension.swift in Sources */, C464ADAF275A7A69003FCD53 /* SiteListVC.swift in Sources */, @@ -654,6 +664,7 @@ C4FBFC532616485F00CDB8E1 /* PhpVersionDetectionTest.swift in Sources */, C43A8A2425D9D20D00591B77 /* BrewJsonParserTest.swift in Sources */, C4F780CA25D80B75000DBC97 /* HomebrewPackage.swift in Sources */, + C4C8E81C276F54E5003AC782 /* PhpConfigWatcher.swift in Sources */, C4AF9F7B2754499000D44ED0 /* Valet.swift in Sources */, C4F780C025D80B6E000DBC97 /* Startup.swift in Sources */, C4CCBA6D275C567B008C7055 /* PMWindowController.swift in Sources */, @@ -661,6 +672,7 @@ C4F2E4382752F08D0020E974 /* HomebrewDiagnostics.swift in Sources */, C4F780AE25D80B37000DBC97 /* ExtensionParserTest.swift in Sources */, C4F780C725D80B75000DBC97 /* StatusMenu.swift in Sources */, + C4C8E819276F54D8003AC782 /* App+ConfigWatch.swift in Sources */, C43603A1275E67610028EFC6 /* AppDelegate+Notifications.swift in Sources */, C42759682627662800093CAE /* NSMenuExtension.swift in Sources */, C4B97B76275CF08C003F3378 /* AppDelegate+MenuOutlets.swift in Sources */, diff --git a/phpmon/Domain/Core/App+ConfigWatch.swift b/phpmon/Domain/Core/App+ConfigWatch.swift new file mode 100644 index 0000000..33e11bd --- /dev/null +++ b/phpmon/Domain/Core/App+ConfigWatch.swift @@ -0,0 +1,44 @@ +// +// App+ConfigWatch.swift +// PHP Monitor +// +// Created by Nico Verbruggen on 30/03/2021. +// Copyright © 2021 Nico Verbruggen. All rights reserved. +// + +import Foundation + +extension App { + + func startWatcher(_ url: URL) { + print("No watcher currently active...") + self.watcher = PhpConfigWatcher(for: url) + + self.watcher.didChange = { url in + // TODO: Make sure this is debounced, because a single process may update the config file many times; this occurs when installing Xdebug, for example + print("Something has changed in: \(url)") + MainMenu.shared.reloadPhpMonitorMenuInBackground() + } + } + + func handlePhpConfigWatcher() { + if self.currentInstall != nil { + // Determine the path of the config folder + let url = URL(fileURLWithPath: "\(Paths.etcPath)/php/\(self.currentInstall!.version.short)") + + // Watcher needs to be created + if self.watcher == nil { + startWatcher(url) + } + + // Watcher needs to be updated + if self.watcher.url != url { + self.watcher.disable() + self.watcher = nil + print("Watcher has stopped watching files. Starting new one...") + startWatcher(url) + } + } + } + +} diff --git a/phpmon/Domain/Core/App.swift b/phpmon/Domain/Core/App.swift index 73ec015..9b77ed1 100644 --- a/phpmon/Domain/Core/App.swift +++ b/phpmon/Domain/Core/App.swift @@ -45,9 +45,13 @@ class App { /** Whether the application is busy switching versions. */ var busy: Bool = false - + /** The currently active installation of PHP. */ - var currentInstall: ActivePhpInstallation? = nil + var currentInstall: ActivePhpInstallation? = nil { + didSet { + handlePhpConfigWatcher() + } + } /** All available versions of PHP. */ var availablePhpVersions: [String] = [] @@ -102,4 +106,10 @@ class App { */ var openWindows: [String] = [] + // MARK: - App Watchers + + /** + The `PhpConfigWatcher` is responsible for watching the `.ini` files and the `.conf.d` folder. + */ + var watcher: PhpConfigWatcher! } diff --git a/phpmon/Domain/Core/PhpConfigWatcher.swift b/phpmon/Domain/Core/PhpConfigWatcher.swift new file mode 100644 index 0000000..5312d92 --- /dev/null +++ b/phpmon/Domain/Core/PhpConfigWatcher.swift @@ -0,0 +1,113 @@ +// +// FolderWatcher.swift +// PHP Monitor +// +// Created by Nico Verbruggen on 30/03/2021. +// Copyright © 2021 Nico Verbruggen. All rights reserved. +// + +import Foundation + +class PhpConfigWatcher { + + let folderMonitorQueue = DispatchQueue(label: "FolderMonitorQueue", attributes: .concurrent) + + let url: URL + + var didChange: ((URL) -> Void)? + + var watchers: [FSWatcher] = [] + + init(for url: URL) { + self.url = url + + // Add a watcher for php.ini + self.addWatcher(for: self.url.appendingPathComponent("php.ini"), eventMask: .write) + + // Add a watcher for conf.d (in case a new file is added or a file is deleted) + // TODO: Make sure that the contents of the conf.d folder is checked each time... this might mean + // that watchers are due for deletion / need to be created + self.addWatcher(for: self.url.appendingPathComponent("conf.d"), eventMask: .all) + + // Scan the conf.d folder for .ini files, and add a watcher for each file + let enumerator = FileManager.default.enumerator(atPath: self.url.appendingPathComponent("conf.d").path) + let filePaths = enumerator?.allObjects as! [String] + + // Loop over the .ini files that we discovered + filePaths.filter { $0.contains(".ini") }.forEach { (file) in + // Add a watcher for each file we have discovered + self.addWatcher(for: self.url.appendingPathComponent("conf.d/\(file)"), eventMask: .write) + } + + print("A watcher exists for the following config paths:") + for watcher in self.watchers { + print(watcher.url) + } + } + + func addWatcher(for url: URL, eventMask: DispatchSource.FileSystemEvent) { + let watcher = FSWatcher(for: url, eventMask: eventMask, parent: self) + self.watchers.append(watcher) + } + + func disable() { + print("Turning off existing watchers...") + self.watchers.forEach { (watcher) in + watcher.stopMonitoring() + } + } + + deinit { + print("An existing config watcher has been deinitialized.") + } + +} + +class FSWatcher { + + private var parent: PhpConfigWatcher! + + private var monitoredFolderFileDescriptor: CInt = -1 + + private var folderMonitorSource: DispatchSourceFileSystemObject? + + let url: URL + + init(for url: URL, eventMask: DispatchSource.FileSystemEvent, parent: PhpConfigWatcher) { + self.url = url + self.parent = parent + self.startMonitoring(eventMask) + } + + func startMonitoring(_ eventMask: DispatchSource.FileSystemEvent) { + guard folderMonitorSource == nil && monitoredFolderFileDescriptor == -1 else { + return + } + + // Open the file or folder referenced by URL for monitoring only. + monitoredFolderFileDescriptor = open(url.path, O_EVTONLY) + + folderMonitorSource = DispatchSource.makeFileSystemObjectSource(fileDescriptor: monitoredFolderFileDescriptor, eventMask: eventMask, queue: parent.folderMonitorQueue) + + // Define the block to call when a file change is detected. + folderMonitorSource?.setEventHandler { [weak self] in + self?.parent.didChange?(self!.url) + } + + // Define a cancel handler to ensure the directory is closed when the source is cancelled. + folderMonitorSource?.setCancelHandler { [weak self] in + guard let self = self else { return } + close(self.monitoredFolderFileDescriptor) + self.monitoredFolderFileDescriptor = -1 + self.folderMonitorSource = nil + } + + // Start monitoring the directory via the source. + folderMonitorSource?.resume() + } + + func stopMonitoring() { + folderMonitorSource?.cancel() + self.parent = nil + } +} diff --git a/phpmon/Domain/Menu/MainMenu.swift b/phpmon/Domain/Menu/MainMenu.swift index f18d89d..9f5486a 100644 --- a/phpmon/Domain/Menu/MainMenu.swift +++ b/phpmon/Domain/Menu/MainMenu.swift @@ -221,6 +221,13 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate { } } + @objc func reloadPhpMonitorMenuInBackground() { + waitAndExecute { + // This automatically reloads the menu + print("Reloading information about the PHP installation (in the background)...") + } + } + @objc func reloadPhpMonitorMenu() { waitAndExecute { // This automatically reloads the menu diff --git a/phpmon/Domain/Menu/StatusMenu.swift b/phpmon/Domain/Menu/StatusMenu.swift index 74bde44..5a10369 100644 --- a/phpmon/Domain/Menu/StatusMenu.swift +++ b/phpmon/Domain/Menu/StatusMenu.swift @@ -189,4 +189,4 @@ class ExtensionMenuItem: NSMenuItem { class EditorMenuItem: NSMenuItem { var editor: Application? = nil -} +} \ No newline at end of file