Skip to content

Commit

Permalink
API guidelines and fix #1838
Browse files Browse the repository at this point in the history
  • Loading branch information
matthid committed Apr 7, 2018
1 parent f867406 commit b35cd39
Show file tree
Hide file tree
Showing 2 changed files with 99 additions and 99 deletions.
185 changes: 93 additions & 92 deletions src/app/Fake.IO.FileSystem/ChangeWatcher.fs
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
/// This module contains helpers to react to file system events.
module Fake.IO.FileSystem.ChangeWatcher
namespace Fake.IO

open System.IO
open Fake.Core
open Fake.IO
open System.Threading
open System
Expand All @@ -17,104 +16,106 @@ type FileChange =
Name : string
Status : FileStatus }

type WatchChangesOption =
{ IncludeSubdirectories: bool }
module ChangeWatcher =

let private handleWatcherEvents (status : FileStatus) (onChange : FileChange -> unit) (e : FileSystemEventArgs) =
onChange ({ FullPath = e.FullPath
Name = e.Name
Status = status })
type Options =
{ IncludeSubdirectories: bool }

let private handleWatcherEvents (status : FileStatus) (onChange : FileChange -> unit) (e : FileSystemEventArgs) =
onChange ({ FullPath = e.FullPath
Name = e.Name
Status = status })

/// Watches for changes in the matching files.
/// Returns an IDisposable which allows to dispose all internally used FileSystemWatchers.
///
/// ## Parameters
/// - `onChange` - function to call when a change is detected.
/// - `fileIncludes` - The glob pattern for files to watch for changes.
///
/// ## Sample
///
/// Target.Create "Watch" (fun _ ->
/// use watcher = !! "c:/projects/watchDir/*.txt" |> ChangeWatcher.Run (fun changes ->
/// // do something
/// )
///
/// System.Console.ReadLine() |> ignore
///
/// watcher.Dispose() // if you need to cleanup the watcher.
/// )
///
let runWithOptions options (onChange : FileChange seq -> unit) (fileIncludes : IGlobbingPattern) =
let dirsToWatch = fileIncludes |> GlobbingPattern.getBaseDirectoryIncludes
/// Watches for changes in the matching files.
/// Returns an IDisposable which allows to dispose all internally used FileSystemWatchers.
///
/// ## Parameters
/// - `onChange` - function to call when a change is detected.
/// - `fileIncludes` - The glob pattern for files to watch for changes.
///
/// ## Sample
///
/// Target.Create "Watch" (fun _ ->
/// use watcher = !! "c:/projects/watchDir/*.txt" |> ChangeWatcher.Run (fun changes ->
/// // do something
/// )
///
/// System.Console.ReadLine() |> ignore
///
/// watcher.Dispose() // if you need to cleanup the watcher.
/// )
///
let runWithOptions (foptions:Options -> Options) (onChange : FileChange seq -> unit) (fileIncludes : IGlobbingPattern) =
let options = foptions { IncludeSubdirectories = true }
let dirsToWatch = fileIncludes |> GlobbingPattern.getBaseDirectoryIncludes

//tracefn "dirs to watch: %A" dirsToWatch
//tracefn "dirs to watch: %A" dirsToWatch

// we collect changes in a mutable ref cell and wait for a few milliseconds to
// receive all notifications when the system sends them repetedly or sends multiple
// updates related to the same file; then we call 'onChange' with all cahnges
let unNotifiedChanges = ref List.empty<FileChange>
// when running 'onChange' we ignore all notifications to avoid infinite loops
let runningHandlers = ref false
let timerCallback = fun _ ->
lock unNotifiedChanges (fun () ->
if not (Seq.isEmpty !unNotifiedChanges) then
let changes =
!unNotifiedChanges
|> Seq.groupBy (fun c -> c.FullPath)
|> Seq.map (fun (name, changes) ->
changes
|> Seq.sortBy (fun c -> c.Status)
|> Seq.head)
unNotifiedChanges := []
try
runningHandlers := true
onChange changes
finally
runningHandlers := false )
// lazy evaluation of timer in order to only start timer once requested
let timer = Lazy<IDisposable>(Func<IDisposable> (fun ()->
// NOTE: that the timer starts immidiatelly when constructed
// we could delay this by sending it how many ms it should delay
// itself
// The timer here has a period of 50 ms:
new Timer(timerCallback, Object(), 0, 50) :> IDisposable
), LazyThreadSafetyMode.ExecutionAndPublication)

let acumChanges (fileChange : FileChange) =
// only record the changes if we are not currently running 'onChange' handler
if not !runningHandlers && fileIncludes.IsMatch fileChange.FullPath then
// we collect changes in a mutable ref cell and wait for a few milliseconds to
// receive all notifications when the system sends them repetedly or sends multiple
// updates related to the same file; then we call 'onChange' with all cahnges
let unNotifiedChanges = ref List.empty<FileChange>
// when running 'onChange' we ignore all notifications to avoid infinite loops
let runningHandlers = ref false
let timerCallback = fun _ ->
lock unNotifiedChanges (fun () ->
unNotifiedChanges := fileChange :: !unNotifiedChanges
// start the timer (ignores repeated calls) to trigger events in 50ms
(timer.Value |> ignore) )
if not (Seq.isEmpty !unNotifiedChanges) then
let changes =
!unNotifiedChanges
|> Seq.groupBy (fun c -> c.FullPath)
|> Seq.map (fun (name, changes) ->
changes
|> Seq.sortBy (fun c -> c.Status)
|> Seq.head)
unNotifiedChanges := []
try
runningHandlers := true
onChange changes
finally
runningHandlers := false )
// lazy evaluation of timer in order to only start timer once requested
let timer = Lazy<IDisposable>(Func<IDisposable> (fun ()->
// NOTE: that the timer starts immidiatelly when constructed
// we could delay this by sending it how many ms it should delay
// itself
// The timer here has a period of 50 ms:
new Timer(timerCallback, Object(), 0, 50) :> IDisposable
), LazyThreadSafetyMode.ExecutionAndPublication)

let acumChanges (fileChange : FileChange) =
// only record the changes if we are not currently running 'onChange' handler
if not !runningHandlers && fileIncludes.IsMatch fileChange.FullPath then
lock unNotifiedChanges (fun () ->
unNotifiedChanges := fileChange :: !unNotifiedChanges
// start the timer (ignores repeated calls) to trigger events in 50ms
(timer.Value |> ignore) )

let watchers =
dirsToWatch |> List.map (fun dir ->
//tracefn "watching dir: %s" dir
let watchers =
dirsToWatch |> List.map (fun dir ->
//tracefn "watching dir: %s" dir

let watcher = new FileSystemWatcher(Path.getFullName dir, "*.*")
watcher.EnableRaisingEvents <- true
watcher.IncludeSubdirectories <- options.IncludeSubdirectories
watcher.Changed.Add(handleWatcherEvents Changed acumChanges)
watcher.Created.Add(handleWatcherEvents Created acumChanges)
watcher.Deleted.Add(handleWatcherEvents Deleted acumChanges)
watcher.Renamed.Add(fun (e : RenamedEventArgs) ->
acumChanges { FullPath = e.OldFullPath
Name = e.OldName
Status = Deleted }
acumChanges { FullPath = e.FullPath
Name = e.Name
Status = Created })
watcher)
let watcher = new FileSystemWatcher(Path.getFullName dir, "*.*")
watcher.EnableRaisingEvents <- true
watcher.IncludeSubdirectories <- options.IncludeSubdirectories
watcher.Changed.Add(handleWatcherEvents Changed acumChanges)
watcher.Created.Add(handleWatcherEvents Created acumChanges)
watcher.Deleted.Add(handleWatcherEvents Deleted acumChanges)
watcher.Renamed.Add(fun (e : RenamedEventArgs) ->
acumChanges { FullPath = e.OldFullPath
Name = e.OldName
Status = Deleted }
acumChanges { FullPath = e.FullPath
Name = e.Name
Status = Created })
watcher)

{ new System.IDisposable with
member this.Dispose() =
for watcher in watchers do
watcher.EnableRaisingEvents <- false
watcher.Dispose()
// only dispose the timer if it has been constructed
if timer.IsValueCreated then timer.Value.Dispose() }
{ new System.IDisposable with
member this.Dispose() =
for watcher in watchers do
watcher.EnableRaisingEvents <- false
watcher.Dispose()
// only dispose the timer if it has been constructed
if timer.IsValueCreated then timer.Value.Dispose() }


let run (onChange : FileChange seq -> unit) (fileIncludes : IGlobbingPattern) = runWithOptions { IncludeSubdirectories = true } onChange fileIncludes
let run (onChange : FileChange seq -> unit) (fileIncludes : IGlobbingPattern) = runWithOptions id onChange fileIncludes
13 changes: 6 additions & 7 deletions src/legacy/FakeLib/ChangeWatcher.fs
Original file line number Diff line number Diff line change
@@ -1,23 +1,23 @@
[<AutoOpen>]
/// This module contains helpers to react to file system events.
[<System.Obsolete("Use Fake.IO.FileSystem instead (FAKE0001 - package: Fake.IO.FileSystem)")>]
[<System.Obsolete("Open Fake.IO instead (FAKE0001 - package: Fake.IO.FileSystem, module: ChangeWatcher)")>]
module Fake.ChangeWatcher

open System.IO

[<System.Obsolete("Use Fake.IO.FileSystem instead (FAKE0001 - package: Fake.IO.FileSystem)")>]
[<System.Obsolete("Open Fake.IO instead (FAKE0001 - package: Fake.IO.FileSystem, type: FileStatus)")>]
type FileStatus =
| Deleted
| Created
| Changed

[<System.Obsolete("Use Fake.IO.FileSystem instead (FAKE0001 - package: Fake.IO.FileSystem)")>]
[<System.Obsolete("Open Fake.IO instead (FAKE0001 - package: Fake.IO.FileSystem, type: FileChange)")>]
type FileChange =
{ FullPath : string
Name : string
Status : FileStatus }

[<System.Obsolete("Use Fake.IO.FileSystem instead (FAKE0001 - package: Fake.IO.FileSystem)")>]
[<System.Obsolete("Open Fake.IO instead (FAKE0001 - package: Fake.IO.FileSystem, type: ChangeWatcher.Options)")>]
type WatchChangesOption =
{ IncludeSubdirectories: bool }

Expand Down Expand Up @@ -56,7 +56,7 @@ let private calcDirsToWatch fileIncludes =
/// watcher.Dispose() // if you need to cleanup the watches.
/// )
///
[<System.Obsolete("Use Fake.IO.FileSystem instead (FAKE0001 - package: Fake.IO.FileSystem)")>]
[<System.Obsolete("Open Fake.IO and use ChangeWatcher.runWithOptions instead (FAKE0001 - package: Fake.IO.FileSystem, module Fake.IO.ChangeWatcher, function: runWithOptions)")>]
let WatchChangesWithOptions options (onChange : FileChange seq -> unit) (fileIncludes : FileIncludes) =
let dirsToWatch = fileIncludes |> calcDirsToWatch

Expand Down Expand Up @@ -122,6 +122,5 @@ let WatchChangesWithOptions options (onChange : FileChange seq -> unit) (fileInc
watcher.Dispose()
timer.Dispose() }


[<System.Obsolete("Use Fake.IO.FileSystem instead (FAKE0001 - package: Fake.IO.FileSystem)")>]
[<System.Obsolete("Open Fake.IO and use ChangeWatcher.run instead (FAKE0001 - package: Fake.IO.FileSystem, module Fake.IO.ChangeWatcher, function: run)")>]
let WatchChanges = WatchChangesWithOptions { IncludeSubdirectories = true }

0 comments on commit b35cd39

Please sign in to comment.