• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

JuliaLang / julia / #37999

02 Feb 2025 07:22AM UTC coverage: 17.218% (-8.3%) from 25.515%
#37999

push

local

web-flow
bpart: Start tracking backedges for bindings (#57213)

This PR adds limited backedge support for Bindings. There are two
classes of bindings that get backedges:

1. Cross-module `GlobalRef` bindings (new in this PR)
2. Any globals accesses through intrinsics (i.e. those with forward
edges from #57009)

This is a time/space trade-off for invalidation. As a result of the
first category, invalidating a binding now only needs to scan all the
methods defined in the same module as the binding. At the same time, it
is anticipated that most binding references are to bindings in the same
module, keeping the list of bindings that need explicit (back)edges
small.

7 of 30 new or added lines in 3 files covered. (23.33%)

4235 existing lines in 124 files now uncovered.

7882 of 45779 relevant lines covered (17.22%)

98289.89 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

4.28
/stdlib/FileWatching/src/FileWatching.jl
1
# This file is a part of Julia. License is MIT: https://julialang.org/license
2

3
"""
4
Utilities for monitoring files and file descriptors for events.
5
"""
6
module FileWatching
7

8
export
9
    # one-shot API (returns results, race-y):
10
    watch_file, # efficient for small numbers of files
11
    watch_folder, # efficient for large numbers of files
12
    unwatch_folder,
13
    poll_file, # very inefficient alternative to above
14
    poll_fd, # very efficient, unrelated to above
15
    # continuous API (returns objects):
16
    FileMonitor,
17
    FolderMonitor,
18
    PollingFileWatcher,
19
    FDWatcher,
20
    # pidfile:
21
    mkpidlock,
22
    trymkpidlock
23

24
import Base: @handle_as, wait, close, eventloop, notify_error, IOError,
25
    uv_req_data, uv_req_set_data, associate_julia_struct, disassociate_julia_struct,
26
    _sizeof_uv_poll, _sizeof_uv_fs, _sizeof_uv_fs_event, _uv_hook_close, uv_error, _UVError,
27
    iolock_begin, iolock_end, preserve_handle, unpreserve_handle,
28
    isreadable, iswritable, isopen, |, getproperty, propertynames
29
import Base.Filesystem: StatStruct, uv_fs_req_cleanup
30
if Sys.iswindows()
31
    import Base.WindowsRawSocket
32
end
33

34

35
# libuv file watching event flags
36
const UV_RENAME = Int32(1)
37
const UV_CHANGE = Int32(2)
38
struct FileEvent
39
    renamed::Bool
40
    changed::Bool
41
    timedout::Bool # aka canceled
42
    FileEvent(r::Bool, c::Bool, t::Bool) = new(r, c, t)
×
43
end
44
FileEvent() = FileEvent(false, false, true)
×
45
FileEvent(flags::Integer) = FileEvent((flags & UV_RENAME) != 0,
×
46
                                      (flags & UV_CHANGE) != 0,
47
                                      iszero(flags))
48
|(a::FileEvent, b::FileEvent) =
×
49
    FileEvent(a.renamed | b.renamed,
50
              a.changed | b.changed,
51
              a.timedout | b.timedout)
52

53
# libuv file descriptor event flags
54
const UV_READABLE = Int32(1)
55
const UV_WRITABLE = Int32(2)
56
const UV_DISCONNECT = Int32(4)
57
const UV_PRIORITIZED = Int32(8)
58
struct FDEvent
59
    events::Int32
60
    FDEvent(flags::Integer=0) = new(flags)
129✔
61
end
62

63
FDEvent(r::Bool, w::Bool, d::Bool, t::Bool) = FDEvent((UV_READABLE * r) | (UV_WRITABLE * w) | (UV_DISCONNECT * d)) # deprecated method
129✔
64

65
function getproperty(f::FDEvent, field::Symbol)
66
    events = getfield(f, :events)
12,400✔
67
    field === :readable && return (events & UV_READABLE) != 0
10,413✔
68
    field === :writable && return (events & UV_WRITABLE) != 0
6,310✔
69
    field === :disconnect && return (events & UV_DISCONNECT) != 0
3,974✔
70
    field === :prioritized && return (events & UV_PRIORITIZED) != 0
1,987✔
71
    field === :timedout && return events == 0
1,987✔
72
    field === :events && return Int(events)
×
73
    getfield(f, field)::Union{}
×
74
end
75
propertynames(f::FDEvent) = (:readable, :writable, :disconnect, :prioritized, :timedout, :events)
×
76

77
isreadable(f::FDEvent) = f.readable
1,987✔
78
iswritable(f::FDEvent) = f.writable
1,987✔
79
|(a::FDEvent, b::FDEvent) = FDEvent(getfield(a, :events) | getfield(b, :events))
×
80

81
# Callback functions
82

83
function uv_fseventscb_file(handle::Ptr{Cvoid}, filename::Ptr, events::Int32, status::Int32)
×
84
    t = @handle_as handle FileMonitor
×
85
    lock(t.notify)
×
86
    try
×
87
        if status != 0
×
88
            t.ioerrno = status
×
89
            notify_error(t.notify, _UVError("FileMonitor", status))
×
90
            uvfinalize(t)
×
91
        elseif events != t.events
×
92
            events = t.events |= events
×
93
            notify(t.notify, all=false)
×
94
        end
95
    finally
96
        unlock(t.notify)
×
97
    end
98
    nothing
×
99
end
100

101
function uv_fseventscb_folder(handle::Ptr{Cvoid}, filename::Ptr, events::Int32, status::Int32)
×
102
    t = @handle_as handle FolderMonitor
×
103
    lock(t.notify)
×
104
    try
×
105
        if status != 0
×
106
            notify_error(t.notify, _UVError("FolderMonitor", status))
×
107
        else
108
            fname = (filename == C_NULL) ? "" : unsafe_string(convert(Cstring, filename))
×
109
            push!(t.channel, fname => FileEvent(events))
×
110
            notify(t.notify)
×
111
        end
112
    finally
113
        unlock(t.notify)
×
114
    end
115
    nothing
×
116
end
117

118
function uv_pollcb(handle::Ptr{Cvoid}, status::Int32, events::Int32)
×
119
    t = @handle_as handle _FDWatcher
×
120
    lock(t.notify)
×
121
    try
×
122
        if status != 0
×
123
            notify_error(t.notify, _UVError("FDWatcher", status))
×
124
        else
125
            t.events |= events
×
126
            if t.active[1] || t.active[2]
×
127
                if isempty(t.notify)
×
128
                    # if we keep hearing about events when nobody appears to be listening,
129
                    # stop the poll to save cycles
130
                    t.active = (false, false)
×
131
                    ccall(:uv_poll_stop, Int32, (Ptr{Cvoid},), t.handle)
×
132
                end
133
            end
134
            notify(t.notify, events)
×
135
        end
136
    finally
137
        unlock(t.notify)
×
138
    end
139
    nothing
×
140
end
141

142
function uv_fspollcb(req::Ptr{Cvoid})
×
143
    pfw = unsafe_pointer_to_objref(uv_req_data(req))::PollingFileWatcher
×
144
    pfw.active = false
×
145
    unpreserve_handle(pfw)
×
146
    @assert pointer(pfw.stat_req) == req
×
147
    r = Int32(ccall(:uv_fs_get_result, Cssize_t, (Ptr{Cvoid},), req))
×
148
    statbuf = ccall(:uv_fs_get_statbuf, Ptr{UInt8}, (Ptr{Cvoid},), req)
×
149
    curr_stat = StatStruct(pfw.file, statbuf, r)
×
150
    uv_fs_req_cleanup(req)
×
151
    lock(pfw.notify)
×
152
    try
×
153
        if !isempty(pfw.notify) # must discard the update if nobody watching
×
154
            if pfw.ioerrno != r || (r == 0 && pfw.prev_stat != curr_stat)
×
155
                if r == 0
×
156
                    pfw.prev_stat = curr_stat
×
157
                end
158
                pfw.ioerrno = r
×
159
                notify(pfw.notify, true)
×
160
            end
161
            pfw.timer = Timer(pfw.interval) do t
×
162
                # async task
163
                iolock_begin()
×
164
                lock(pfw.notify)
×
165
                try
×
166
                    if pfw.timer === t # use identity check to test if this callback is stale by the time we got the lock
×
167
                        pfw.timer = nothing
×
168
                        @assert !pfw.active
×
169
                        if isopen(pfw) && !isempty(pfw.notify)
×
170
                            preserve_handle(pfw)
×
171
                            uv_jl_fspollcb = @cfunction(uv_fspollcb, Cvoid, (Ptr{Cvoid},))
×
172
                            err = ccall(:uv_fs_stat, Cint, (Ptr{Cvoid}, Ptr{Cvoid}, Cstring, Ptr{Cvoid}),
×
173
                                eventloop(), pfw.stat_req, pfw.file, uv_jl_fspollcb::Ptr{Cvoid})
174
                            err == 0 || notify(pfw.notify, _UVError("PollingFileWatcher (start)", err), error=true) # likely just ENOMEM
×
175
                            pfw.active = true
×
176
                        end
177
                    end
178
                finally
179
                    unlock(pfw.notify)
×
180
                end
181
                iolock_end()
×
182
                nothing
×
183
            end
184
        end
185
    finally
186
        unlock(pfw.notify)
×
187
    end
188
    nothing
×
189
end
190

191
# Types
192

193
"""
194
    FileMonitor(path::AbstractString)
195

196
Watch file or directory `path` (which must exist) for changes until a change occurs. This
197
function does not poll the file system and instead uses platform-specific functionality to
198
receive notifications from the operating system (e.g. via inotify on Linux). See the NodeJS
199
documentation linked below for details.
200

201
`fm = FileMonitor(path)` acts like an auto-reset Event, so `wait(fm)` blocks until there has
202
been at least one event in the file originally at the given path and then returns an object
203
with boolean fields `renamed`, `changed`, `timedout` summarizing all changes that have
204
occurred since the last call to `wait` returned.
205

206
This behavior of this function varies slightly across platforms. See
207
<https://nodejs.org/api/fs.html#fs_caveats> for more detailed information.
208
"""
209
mutable struct FileMonitor
210
    @atomic handle::Ptr{Cvoid}
211
    const file::String
212
    const notify::Base.ThreadSynchronizer
213
    events::Int32 # accumulator for events that occurred since the last wait call, similar to Event with autoreset
214
    ioerrno::Int32 # record the error, if any occurs (unlikely)
215
    FileMonitor(file::AbstractString) = FileMonitor(String(file))
×
216
    function FileMonitor(file::String)
×
217
        handle = Libc.malloc(_sizeof_uv_fs_event)
×
218
        this = new(handle, file, Base.ThreadSynchronizer(), 0, 0)
×
219
        associate_julia_struct(handle, this)
×
220
        iolock_begin()
×
221
        err = ccall(:uv_fs_event_init, Cint, (Ptr{Cvoid}, Ptr{Cvoid}), eventloop(), handle)
×
222
        if err != 0
×
223
            Libc.free(handle)
×
224
            uv_error("FileMonitor", err)
×
225
        end
226
        finalizer(uvfinalize, this)
×
227
        uv_jl_fseventscb_file = @cfunction(uv_fseventscb_file, Cvoid, (Ptr{Cvoid}, Ptr{Int8}, Int32, Int32))
×
228
        uv_error("FileMonitor (start)",
×
229
                 ccall(:uv_fs_event_start, Int32, (Ptr{Cvoid}, Ptr{Cvoid}, Cstring, Int32),
230
                       this.handle, uv_jl_fseventscb_file::Ptr{Cvoid}, file, 0))
231
        iolock_end()
×
232
        return this
×
233
    end
234
end
235

236

237
"""
238
    FolderMonitor(folder::AbstractString)
239

240
Watch a file or directory `path` for changes until a change has occurred. This function does
241
not poll the file system and instead uses platform-specific functionality to receive
242
notifications from the operating system (e.g. via inotify on Linux). See the NodeJS
243
documentation linked below for details.
244

245
This acts similar to a Channel, so calling `take!` (or `wait`) blocks until some change has
246
occurred. The `wait` function will return a pair where the first field is the name of the
247
changed file (if available) and the second field is an object with boolean fields `renamed`
248
and `changed`, giving the event that occurred on it.
249

250
This behavior of this function varies slightly across platforms. See
251
<https://nodejs.org/api/fs.html#fs_caveats> for more detailed information.
252
"""
253
mutable struct FolderMonitor
254
    @atomic handle::Ptr{Cvoid}
255
    # notify::Channel{Any} # eltype = Union{Pair{String, FileEvent}, IOError}
256
    const notify::Base.ThreadSynchronizer
257
    const channel::Vector{Any} # eltype = Pair{String, FileEvent}
258
    FolderMonitor(folder::AbstractString) = FolderMonitor(String(folder))
×
259
    function FolderMonitor(folder::String)
×
260
        handle = Libc.malloc(_sizeof_uv_fs_event)
×
261
        this = new(handle, Base.ThreadSynchronizer(), [])
×
262
        associate_julia_struct(handle, this)
×
263
        iolock_begin()
×
264
        err = ccall(:uv_fs_event_init, Cint, (Ptr{Cvoid}, Ptr{Cvoid}), eventloop(), handle)
×
265
        if err != 0
×
266
            Libc.free(handle)
×
267
            throw(_UVError("FolderMonitor", err))
×
268
        end
269
        finalizer(uvfinalize, this)
×
270
        uv_jl_fseventscb_folder = @cfunction(uv_fseventscb_folder, Cvoid, (Ptr{Cvoid}, Ptr{Int8}, Int32, Int32))
×
271
        uv_error("FolderMonitor (start)",
×
272
                 ccall(:uv_fs_event_start, Int32, (Ptr{Cvoid}, Ptr{Cvoid}, Cstring, Int32),
273
                       handle, uv_jl_fseventscb_folder::Ptr{Cvoid}, folder, 0))
274
        iolock_end()
×
275
        return this
×
276
    end
277
end
278

279
# this is similar to uv_fs_poll, but strives to avoid the design mistakes that make it unsuitable for any usable purpose
280
# https://github.com/libuv/libuv/issues/4543
281
"""
282
    PollingFileWatcher(path::AbstractString, interval_s::Real=5.007)
283

284
Monitor a file for changes by polling `stat` every `interval_s` seconds until a change
285
occurs or `timeout_s` seconds have elapsed. The `interval_s` should be a long period; the
286
default is 5.007 seconds. Call `stat` on it to get the most recent, but old, result.
287

288
This acts like an auto-reset Event, so calling `wait` blocks until the `stat` result has
289
changed since the previous value captured upon entry to the `wait` call. The `wait` function
290
will return a pair of status objects `(previous, current)` once any `stat` change is
291
detected since the previous time that `wait` was called. The `previous` status is always a
292
`StatStruct`, but it may have all of the fields zeroed (indicating the file didn't
293
previously exist, or wasn't previously accessible).
294

295
The `current` status object may be a `StatStruct`, an `EOFError` (if the wait is canceled by
296
closing this object), or some other `Exception` subtype (if the `stat` operation failed: for
297
example, if the path is removed). Note that `stat` value may be outdated if the file has
298
changed again multiple times.
299

300
Using [`FileMonitor`](@ref) for this operation is preferred, since it is more reliable and
301
efficient, although in some situations it may not be available.
302
"""
303
mutable struct PollingFileWatcher
304
    file::String
305
    interval::Float64
306
    const notify::Base.ThreadSynchronizer # lock protects all fields which can be changed (including interval and file, if you really must)
307
    timer::Union{Nothing,Timer}
308
    const stat_req::Memory{UInt8}
309
    active::Bool # whether there is already an uv_fspollcb in-flight, so to speak
310
    closed::Bool # whether the user has explicitly destroyed this
311
    ioerrno::Int32 # the stat errno as of the last result
312
    prev_stat::StatStruct # the stat as of the last successful result
313
    PollingFileWatcher(file::AbstractString, interval::Float64=5.007) = PollingFileWatcher(String(file), interval)
×
314
    function PollingFileWatcher(file::String, interval::Float64=5.007) # same default as nodejs
×
315
        stat_req = Memory{UInt8}(undef, Int(_sizeof_uv_fs))
×
316
        this = new(file, interval, Base.ThreadSynchronizer(), nothing, stat_req, false, false, 0, StatStruct())
×
317
        uv_req_set_data(stat_req, this)
×
318
        wait(this) # initialize with the current stat before return
×
319
        return this
×
320
    end
321
end
322

323
mutable struct _FDWatcher
324
    @atomic handle::Ptr{Cvoid}
325
    const fdnum::Int # this is NOT the file descriptor
326
    refcount::Tuple{Int, Int}
327
    const notify::Base.ThreadSynchronizer
328
    events::Int32
329
    active::Tuple{Bool, Bool}
330

331
    let FDWatchers = Vector{Any}() # n.b.: this structure and the refcount are protected by the iolock
332
        global _FDWatcher, uvfinalize
333
        @static if Sys.isunix()
334
            _FDWatcher(fd::RawFD, mask::FDEvent) = _FDWatcher(fd, mask.readable, mask.writable)
129✔
335
            function _FDWatcher(fd::RawFD, readable::Bool, writable::Bool)
×
336
                fdnum = Core.Intrinsics.bitcast(Int32, fd) + 1
×
337
                if fdnum <= 0
×
338
                    throw(ArgumentError("Passed file descriptor fd=$(fd) is not a valid file descriptor"))
×
339
                elseif !readable && !writable
×
340
                    throw(ArgumentError("must specify at least one of readable or writable to create a FDWatcher"))
×
341
                end
342

343
                iolock_begin()
×
344
                if fdnum > length(FDWatchers)
×
345
                    old_len = length(FDWatchers)
×
346
                    resize!(FDWatchers, fdnum)
×
347
                    FDWatchers[(old_len + 1):fdnum] .= nothing
×
348
                elseif FDWatchers[fdnum] !== nothing
×
349
                    this = FDWatchers[fdnum]::_FDWatcher
×
350
                    this.refcount = (this.refcount[1] + Int(readable), this.refcount[2] + Int(writable))
×
351
                    iolock_end()
×
352
                    return this
×
353
                end
354
                if ccall(:jl_uv_unix_fd_is_watched, Int32, (RawFD, Ptr{Cvoid}, Ptr{Cvoid}), fd, C_NULL, eventloop()) == 1
×
355
                    throw(ArgumentError("$(fd) is already being watched by libuv"))
×
356
                end
357

358
                handle = Libc.malloc(_sizeof_uv_poll)
×
359
                this = new(
×
360
                    handle,
361
                    fdnum,
362
                    (Int(readable), Int(writable)),
363
                    Base.ThreadSynchronizer(),
364
                    Int32(0),
365
                    (false, false))
366
                associate_julia_struct(handle, this)
×
367
                err = ccall(:uv_poll_init, Int32, (Ptr{Cvoid}, Ptr{Cvoid}, RawFD), eventloop(), handle, fd)
×
368
                if err != 0
×
369
                    Libc.free(handle)
×
370
                    throw(_UVError("FDWatcher", err))
×
371
                end
372
                finalizer(uvfinalize, this)
×
373
                FDWatchers[fdnum] = this
×
374
                iolock_end()
×
375
                return this
×
376
            end
377
        end
378

379
        function uvfinalize(t::_FDWatcher)
×
380
            iolock_begin()
×
381
            lock(t.notify)
×
382
            try
×
383
                if t.handle != C_NULL
×
384
                    disassociate_julia_struct(t)
×
385
                    ccall(:jl_close_uv, Cvoid, (Ptr{Cvoid},), t.handle)
×
386
                    @atomic :monotonic t.handle = C_NULL
×
387
                end
388
                t.refcount = (0, 0)
×
389
                t.active = (false, false)
×
390
                @static if Sys.isunix()
×
391
                    if FDWatchers[t.fdnum] === t
×
392
                        FDWatchers[t.fdnum] = nothing
×
393
                    end
394
                end
395
                notify(t.notify, Int32(0))
×
396
            finally
397
                unlock(t.notify)
×
398
            end
399
            iolock_end()
×
400
            nothing
×
401
        end
402
    end
403

404
    @static if Sys.iswindows()
405
        _FDWatcher(fd::RawFD, mask::FDEvent) = _FDWatcher(fd, mask.readable, mask.writable)
×
406
        function _FDWatcher(fd::RawFD, readable::Bool, writable::Bool)
×
407
            fdnum = Core.Intrinsics.bitcast(Int32, fd) + 1
×
408
            if fdnum <= 0
×
409
                throw(ArgumentError("Passed file descriptor fd=$(fd) is not a valid file descriptor"))
×
410
            end
411

412
            handle = Libc._get_osfhandle(fd)
×
413
            return _FDWatcher(handle, readable, writable)
×
414
        end
415
        _FDWatcher(fd::WindowsRawSocket, mask::FDEvent) = _FDWatcher(fd, mask.readable, mask.writable)
×
416
        function _FDWatcher(fd::WindowsRawSocket, readable::Bool, writable::Bool)
×
417
            if fd == Base.INVALID_OS_HANDLE
×
418
                throw(ArgumentError("Passed file descriptor fd=$(fd) is not a valid file descriptor"))
×
419
            elseif !readable && !writable
×
420
                throw(ArgumentError("must specify at least one of readable or writable to create a FDWatcher"))
×
421
            end
422

423
            handle = Libc.malloc(_sizeof_uv_poll)
×
424
            this = new(
×
425
                handle,
426
                0,
427
                (Int(readable), Int(writable)),
428
                Base.ThreadSynchronizer(),
429
                0,
430
                (false, false))
431
            associate_julia_struct(handle, this)
×
432
            iolock_begin()
×
433
            err = ccall(:uv_poll_init, Int32, (Ptr{Cvoid},  Ptr{Cvoid}, WindowsRawSocket),
×
434
                                               eventloop(), handle,     fd)
435
            iolock_end()
×
436
            if err != 0
×
437
                Libc.free(handle)
×
438
                throw(_UVError("FDWatcher", err))
×
439
            end
440
            finalizer(uvfinalize, this)
×
441
            return this
×
442
        end
443
    end
444
end
445

446
"""
447
    FDWatcher(fd::Union{RawFD,WindowsRawSocket}, readable::Bool, writable::Bool)
448

449
Monitor a file descriptor `fd` for changes in the read or write availability.
450

451
The keyword arguments determine which of read and/or write status should be monitored; at
452
least one of them must be set to `true`.
453

454
The returned value is an object with boolean fields `readable`, `writable`, and `timedout`,
455
giving the result of the polling.
456

457
This acts like a level-set event, so calling `wait` blocks until one of those conditions is
458
met, but then continues to return without blocking until the condition is cleared (either
459
there is no more to read, or no more space in the write buffer, or both).
460

461
!!! warning
462
    You must call `close` manually, when finished with this object, before the fd
463
    argument is closed. Failure to do so risks serious crashes.
464
"""
465
mutable struct FDWatcher
466
    # WARNING: make sure `close` has been manually called on this watcher before closing / destroying `fd`
467
    const watcher::_FDWatcher
468
    mask::FDEvent
469
    function FDWatcher(fd::RawFD, readable::Bool, writable::Bool)
470
        return FDWatcher(fd, FDEvent(readable, writable, false, false))
129✔
471
    end
472
    function FDWatcher(fd::RawFD, mask::FDEvent)
473
        this = new(_FDWatcher(fd, mask), mask)
129✔
474
        finalizer(close, this)
129✔
475
        return this
×
476
    end
477
    @static if Sys.iswindows()
478
        function FDWatcher(fd::WindowsRawSocket, readable::Bool, writable::Bool)
×
479
            return FDWatcher(fd, FDEvent(readable, writable, false, false))
×
480
        end
481
        function FDWatcher(fd::WindowsRawSocket, mask::FDEvent)
×
482
            this = new(_FDWatcher(fd, mask), mask)
×
483
            finalizer(close, this)
×
484
            return this
×
485
        end
486
    end
487
end
488

489
function getproperty(fdw::FDWatcher, s::Symbol)
490
    # support deprecated field names
491
    s === :readable && return fdw.mask.readable
8,388✔
492
    s === :writable && return fdw.mask.writable
4,285✔
493
    return getfield(fdw, s)
4,452✔
494
end
495

496

497
close(t::_FDWatcher, mask::FDEvent) = close(t, mask.readable, mask.writable)
129✔
498
function close(t::_FDWatcher, readable::Bool, writable::Bool)
×
499
    iolock_begin()
×
500
    if t.refcount != (0, 0)
×
501
        t.refcount = (t.refcount[1] - Int(readable), t.refcount[2] - Int(writable))
×
502
    end
503
    if t.refcount == (0, 0)
×
504
        uvfinalize(t)
×
505
    else
506
        @lock t.notify notify(t.notify, Int32(0))
×
507
    end
508
    iolock_end()
×
509
    nothing
×
510
end
511

512
function close(t::FDWatcher)
513
    mask = t.mask
129✔
514
    t.mask = FDEvent()
129✔
515
    close(t.watcher, mask)
129✔
516
end
517

518
function uvfinalize(uv::Union{FileMonitor, FolderMonitor})
×
519
    iolock_begin()
×
520
    if uv.handle != C_NULL
×
521
        disassociate_julia_struct(uv) # close (and free) without notify
×
522
        ccall(:jl_close_uv, Cvoid, (Ptr{Cvoid},), uv.handle)
×
523
    end
524
    iolock_end()
×
525
end
526

527
function close(t::Union{FileMonitor, FolderMonitor})
×
528
    iolock_begin()
×
529
    if t.handle != C_NULL
×
530
        ccall(:jl_close_uv, Cvoid, (Ptr{Cvoid},), t.handle)
×
531
    end
532
    iolock_end()
×
533
end
534

535
function close(pfw::PollingFileWatcher)
×
536
    timer = nothing
×
537
    lock(pfw.notify)
×
538
    try
×
539
        pfw.closed = true
×
540
        notify(pfw.notify, false)
×
541
        timer = pfw.timer
×
542
        pfw.timer = nothing
×
543
    finally
544
        unlock(pfw.notify)
×
545
    end
546
    timer === nothing || close(timer)
×
547
    nothing
×
548
end
549

550
function _uv_hook_close(uv::_FDWatcher)
×
551
    # fyi: jl_atexit_hook can cause this to get called too
552
    Libc.free(@atomicswap :monotonic uv.handle = C_NULL)
×
553
    uvfinalize(uv)
×
554
    nothing
×
555
end
556

557
function _uv_hook_close(uv::FileMonitor)
×
558
    lock(uv.notify)
×
559
    try
×
560
        Libc.free(@atomicswap :monotonic uv.handle = C_NULL)
×
561
        notify(uv.notify)
×
562
    finally
563
        unlock(uv.notify)
×
564
    end
565
    nothing
×
566
end
567

568
function _uv_hook_close(uv::FolderMonitor)
×
569
    lock(uv.notify)
×
570
    try
×
571
        Libc.free(@atomicswap :monotonic uv.handle = C_NULL)
×
572
        notify_error(uv.notify, EOFError())
×
573
    finally
574
        unlock(uv.notify)
×
575
    end
576
    nothing
×
577
end
578

579
isopen(fm::FileMonitor) = fm.handle != C_NULL
×
580
isopen(fm::FolderMonitor) = fm.handle != C_NULL
×
581
isopen(pfw::PollingFileWatcher) = !pfw.closed
×
582
isopen(pfw::_FDWatcher) = pfw.refcount != (0, 0)
×
583
isopen(pfw::FDWatcher) = !pfw.mask.timedout
×
584

585
Base.stat(pfw::PollingFileWatcher) = Base.checkstat(@lock pfw.notify pfw.prev_stat)
×
586

587
# n.b. this _wait may return spuriously early with a timedout event
588
function _wait(fdw::_FDWatcher, mask::FDEvent)
×
589
    iolock_begin()
×
590
    preserve_handle(fdw)
×
591
    lock(fdw.notify)
×
592
    try
×
593
        events = FDEvent(fdw.events & mask.events)
×
594
        if !isopen(fdw) # !open
×
595
            throw(EOFError())
×
596
        elseif events.timedout
×
597
            fdw.handle == C_NULL && throw(ArgumentError("FDWatcher is closed"))
×
598
            # start_watching to make sure the poll is active
599
            readable = fdw.refcount[1] > 0
×
600
            writable = fdw.refcount[2] > 0
×
601
            if fdw.active[1] != readable || fdw.active[2] != writable
×
602
                # make sure the READABLE / WRITEABLE state is updated
603
                uv_jl_pollcb = @cfunction(uv_pollcb, Cvoid, (Ptr{Cvoid}, Cint, Cint))
×
604
                uv_error("FDWatcher (start)",
×
605
                         ccall(:uv_poll_start, Int32, (Ptr{Cvoid}, Int32, Ptr{Cvoid}),
606
                               fdw.handle,
607
                               (readable ? UV_READABLE : 0) | (writable ? UV_WRITABLE : 0),
608
                               uv_jl_pollcb::Ptr{Cvoid}))
609
                fdw.active = (readable, writable)
×
610
            end
611
            iolock_end()
×
612
            return FDEvent(wait(fdw.notify)::Int32)
×
613
        else
614
            iolock_end()
×
615
            return events
×
616
        end
617
    finally
618
        unlock(fdw.notify)
×
619
        unpreserve_handle(fdw)
×
620
    end
621
end
622

623
function wait(fdw::_FDWatcher; readable=true, writable=true)
×
624
    return wait(fdw, FDEvent(readable, writable, false, false))
×
625
end
626
function wait(fdw::_FDWatcher, mask::FDEvent)
×
627
    while true
×
628
        mask.timedout && return mask
×
629
        events = _wait(fdw, mask)
×
630
        if !events.timedout
×
631
            @lock fdw.notify fdw.events &= ~events.events
×
632
            return events
×
633
        end
634
    end
×
635
end
636

637
function wait(fdw::FDWatcher)
×
638
    isopen(fdw) || throw(EOFError())
×
639
    while true
×
640
        events = GC.@preserve fdw _wait(fdw.watcher, fdw.mask)
×
641
        isopen(fdw) || throw(EOFError())
×
642
        if !events.timedout
×
643
            @lock fdw.watcher.notify fdw.watcher.events &= ~events.events
×
644
            return events
×
645
        end
646
    end
×
647
end
648

649
function wait(socket::RawFD; readable=false, writable=false)
×
650
    return wait(socket, FDEvent(readable, writable, false, false))
×
651
end
652
function wait(fd::RawFD, mask::FDEvent)
×
653
    fdw = _FDWatcher(fd, mask)
×
654
    try
×
655
        return wait(fdw, mask)
×
656
    finally
657
        close(fdw, mask)
×
658
    end
659
end
660

661

662
if Sys.iswindows()
663
    function wait(socket::WindowsRawSocket; readable=false, writable=false)
×
664
        return wait(socket, FDEvent(readable, writable, false, false))
×
665
    end
666
    function wait(socket::WindowsRawSocket, mask::FDEvent)
×
667
        fdw = _FDWatcher(socket, mask)
×
668
        try
×
669
            return wait(fdw, mask)
×
670
        finally
671
            close(fdw, mask)
×
672
        end
673
    end
674
end
675

676
function wait(pfw::PollingFileWatcher)
×
677
    iolock_begin()
×
678
    lock(pfw.notify)
×
679
    prevstat = pfw.prev_stat
×
680
    havechange = false
×
681
    timer = nothing
×
682
    try
×
683
        # we aren't too strict about the first interval after `wait`, but rather always
684
        # check right away to see if it had immediately changed again, and then repeatedly
685
        # after interval again until success
686
        pfw.closed && throw(ArgumentError("PollingFileWatcher is closed"))
×
687
        timer = pfw.timer
×
688
        pfw.timer = nothing # disable Timer callback
×
689
        # start_watching
690
        if !pfw.active
×
691
            preserve_handle(pfw)
×
692
            uv_jl_fspollcb = @cfunction(uv_fspollcb, Cvoid, (Ptr{Cvoid},))
×
693
            err = ccall(:uv_fs_stat, Cint, (Ptr{Cvoid}, Ptr{Cvoid}, Cstring, Ptr{Cvoid}),
×
694
                eventloop(), pfw.stat_req, pfw.file, uv_jl_fspollcb::Ptr{Cvoid})
695
            err == 0 || uv_error("PollingFileWatcher (start)", err) # likely just ENOMEM
×
696
            pfw.active = true
×
697
        end
698
        iolock_end()
×
699
        havechange = wait(pfw.notify)::Bool
×
700
        unlock(pfw.notify)
×
701
        iolock_begin()
×
702
    catch
703
        # stop_watching: cleanup any timers from before or after starting this wait before it failed, if there are no other watchers
704
        latetimer = nothing
×
705
        try
×
706
            if isempty(pfw.notify)
×
707
                latetimer = pfw.timer
×
708
                pfw.timer = nothing
×
709
            end
710
        finally
711
            unlock(pfw.notify)
×
712
        end
713
        if timer !== nothing || latetimer !== nothing
×
714
            iolock_end()
×
715
            timer === nothing || close(timer)
×
716
            latetimer === nothing || close(latetimer)
×
717
            iolock_begin()
×
718
        end
719
        rethrow()
×
720
    end
721
    iolock_end()
×
722
    timer === nothing || close(timer) # cleanup resources so we don't hang on exit
×
723
    if !havechange # user canceled by calling close
×
724
        return prevstat, EOFError()
×
725
    end
726
    # grab the most up-to-date stat result as of this time, even if it was a bit newer than
727
    # the notify call (unlikely, as there would need to be a concurrent call to wait)
728
    lock(pfw.notify)
×
729
    currstat = pfw.prev_stat
×
730
    ioerrno = pfw.ioerrno
×
731
    unlock(pfw.notify)
×
732
    if ioerrno == 0
×
733
        @assert currstat.ioerrno == 0
×
734
        return prevstat, currstat
×
735
    elseif ioerrno in (Base.UV_ENOENT, Base.UV_ENOTDIR, Base.UV_EINVAL)
×
736
        return prevstat, StatStruct(pfw.file, Ptr{UInt8}(0), ioerrno)
×
737
    else
738
        return prevstat, _UVError("PollingFileWatcher", ioerrno)
×
739
    end
740
end
741

742
function wait(m::FileMonitor)
×
743
    m.handle == C_NULL && throw(EOFError())
×
744
    preserve_handle(m)
×
745
    lock(m.notify)
×
746
    try
×
747
        while true
×
748
            m.handle == C_NULL && throw(EOFError())
×
749
            events = @atomicswap :not_atomic m.events = 0
×
750
            events == 0 || return FileEvent(events)
×
751
            if m.ioerrno != 0
×
752
                uv_error("FileMonitor", m.ioerrno)
×
753
            end
754
            wait(m.notify)
×
755
        end
×
756
    finally
757
        unlock(m.notify)
×
758
        unpreserve_handle(m)
×
759
    end
760
end
761

762
function wait(m::FolderMonitor)
×
763
    m.handle == C_NULL && throw(EOFError())
×
764
    preserve_handle(m)
×
765
    lock(m.notify)
×
766
    evt = try
×
767
            m.handle == C_NULL && throw(EOFError())
×
768
            while isempty(m.channel)
×
769
                wait(m.notify)
×
770
            end
×
771
            popfirst!(m.channel)
×
772
        finally
773
            unlock(m.notify)
×
774
            unpreserve_handle(m)
×
775
        end
776
    return evt::Pair{String, FileEvent}
×
777
end
778
Base.take!(m::FolderMonitor) = wait(m) # Channel-like API
×
779

780

781
"""
782
    poll_fd(fd, timeout_s::Real=-1; readable=false, writable=false)
783

784
Monitor a file descriptor `fd` for changes in the read or write availability, and with a
785
timeout given by `timeout_s` seconds.
786

787
The keyword arguments determine which of read and/or write status should be monitored; at
788
least one of them must be set to `true`.
789

790
The returned value is an object with boolean fields `readable`, `writable`, and `timedout`,
791
giving the result of the polling.
792

793
This is a thin wrapper over calling `wait` on a [`FDWatcher`](@ref), which implements the
794
functionality but requires the user to call `close` manually when finished with it, or risk
795
serious crashes.
796
"""
797
function poll_fd(s::Union{RawFD, Sys.iswindows() ? WindowsRawSocket : Union{}}, timeout_s::Real=-1; readable=false, writable=false)
×
798
    mask = FDEvent(readable, writable, false, false)
×
799
    mask.timedout && return mask
×
800
    fdw = _FDWatcher(s, mask)
×
801
    local timer
×
802
    # we need this flag to explicitly track whether we call `close` already, to update the internal refcount correctly
803
    timedout = false # TODO: make this atomic
×
804
    try
×
805
        if timeout_s >= 0
×
806
            # delay creating the timer until shortly before we start the poll wait
807
            timer = Timer(timeout_s) do t
×
808
                timedout && return
×
809
                timedout = true
×
810
                close(fdw, mask)
×
811
            end
812
            try
×
813
                while true
×
814
                    events = _wait(fdw, mask)
×
815
                    if timedout || !events.timedout
×
816
                        @lock fdw.notify fdw.events &= ~events.events
×
817
                        return events
×
818
                    end
819
                end
×
820
            catch ex
821
                ex isa EOFError || rethrow()
×
822
                return FDEvent()
×
823
            end
824
        else
825
            return wait(fdw, mask)
×
826
        end
827
    finally
828
        if @isdefined(timer)
×
829
            if !timedout
×
830
                timedout = true
×
831
                close(timer)
×
832
                close(fdw, mask)
×
833
            end
834
        else
835
            close(fdw, mask)
×
836
        end
837
    end
838
end
839

840
"""
841
    watch_file(path::AbstractString, timeout_s::Real=-1)
842

843
Watch file or directory `path` for changes until a change occurs or `timeout_s` seconds have
844
elapsed. This function does not poll the file system and instead uses platform-specific
845
functionality to receive notifications from the operating system (e.g. via inotify on Linux).
846
See the NodeJS documentation linked below for details.
847

848
The returned value is an object with boolean fields `renamed`, `changed`, and `timedout`,
849
giving the result of watching the file.
850

851
This behavior of this function varies slightly across platforms. See
852
<https://nodejs.org/api/fs.html#fs_caveats> for more detailed information.
853

854
This is a thin wrapper over calling `wait` on a [`FileMonitor`](@ref). This function has a
855
small race window between consecutive calls to `watch_file` where the file might change
856
without being detected. To avoid this race, use
857

858
    fm = FileMonitor(path)
859
    wait(fm)
860

861
directly, re-using the same `fm` each time you `wait`.
862
"""
863
function watch_file(s::String, timeout_s::Float64=-1.0)
×
864
    fm = FileMonitor(s)
×
865
    local timer
×
866
    try
×
867
        if timeout_s >= 0
×
868
            timer = Timer(timeout_s) do t
×
869
                close(fm)
×
870
            end
871
        end
872
        try
×
873
            return wait(fm)
×
874
        catch ex
875
            ex isa EOFError && return FileEvent()
×
876
            rethrow()
×
877
        end
878
    finally
879
        close(fm)
×
880
        @isdefined(timer) && close(timer)
×
881
    end
882
end
883
watch_file(s::AbstractString, timeout_s::Real=-1) = watch_file(String(s), Float64(timeout_s))
×
884

885
"""
886
    watch_folder(path::AbstractString, timeout_s::Real=-1)
887

888
Watch a file or directory `path` for changes until a change has occurred or `timeout_s`
889
seconds have elapsed. This function does not poll the file system and instead uses platform-specific
890
functionality to receive notifications from the operating system (e.g. via inotify on Linux).
891
See the NodeJS documentation linked below for details.
892

893
This will continuing tracking changes for `path` in the background until
894
`unwatch_folder` is called on the same `path`.
895

896
The returned value is an pair where the first field is the name of the changed file (if available)
897
and the second field is an object with boolean fields `renamed`, `changed`, and `timedout`,
898
giving the event.
899

900
This behavior of this function varies slightly across platforms. See
901
<https://nodejs.org/api/fs.html#fs_caveats> for more detailed information.
902

903
This function is a thin wrapper over calling `wait` on a [`FolderMonitor`](@ref), with added timeout support.
904
"""
905
watch_folder(s::AbstractString, timeout_s::Real=-1) = watch_folder(String(s), timeout_s)
×
906
function watch_folder(s::String, timeout_s::Real=-1)
×
907
    fm = @lock watched_folders get!(watched_folders[], s) do
×
908
        return FolderMonitor(s)
×
909
    end
910
    local timer
×
911
    if timeout_s >= 0
×
912
        @lock fm.notify isempty(fm.channel) || return popfirst!(fm.channel)
×
913
        if timeout_s <= 0.010
×
914
            # for very small timeouts, we can just sleep for the whole timeout-interval
915
            (timeout_s == 0) ? yield() : sleep(timeout_s)
×
916
            @lock fm.notify isempty(fm.channel) || return popfirst!(fm.channel)
×
917
            return "" => FileEvent() # timeout
×
918
        else
919
            timer = Timer(timeout_s) do t
×
920
                @lock fm.notify notify(fm.notify)
×
921
            end
922
        end
923
    end
924
    # inline a copy of `wait` with added support for checking timer
925
    fm.handle == C_NULL && throw(EOFError())
×
926
    preserve_handle(fm)
×
927
    lock(fm.notify)
×
928
    evt = try
×
929
            fm.handle == C_NULL && throw(EOFError())
×
930
            while isempty(fm.channel)
×
931
                if @isdefined(timer)
×
932
                    isopen(timer) || return "" => FileEvent() # timeout
×
933
                end
934
                wait(fm.notify)
×
935
            end
×
936
            popfirst!(fm.channel)
×
937
        finally
938
            unlock(fm.notify)
×
939
            unpreserve_handle(fm)
×
940
            @isdefined(timer) && close(timer)
×
941
        end
942
    return evt::Pair{String, FileEvent}
×
943
end
944

945
"""
946
    unwatch_folder(path::AbstractString)
947

948
Stop background tracking of changes for `path`.
949
It is not recommended to do this while another task is waiting for
950
`watch_folder` to return on the same path, as the result may be unpredictable.
951
"""
952
unwatch_folder(s::AbstractString) = unwatch_folder(String(s))
×
953
function unwatch_folder(s::String)
×
954
    fm = @lock watched_folders pop!(watched_folders[], s, nothing)
×
955
    fm === nothing || close(fm)
×
956
    nothing
×
957
end
958

959
const watched_folders = Lockable(Dict{String, FolderMonitor}())
960

961
"""
962
    poll_file(path::AbstractString, interval_s::Real=5.007, timeout_s::Real=-1) -> (previous::StatStruct, current)
963

964
Monitor a file for changes by polling every `interval_s` seconds until a change occurs or
965
`timeout_s` seconds have elapsed. The `interval_s` should be a long period; the default is
966
5.007 seconds.
967

968
Returns a pair of status objects `(previous, current)` when a change is detected.
969
The `previous` status is always a `StatStruct`, but it may have all of the fields zeroed
970
(indicating the file didn't previously exist, or wasn't previously accessible).
971

972
The `current` status object may be a `StatStruct`, an `EOFError` (indicating the timeout elapsed),
973
or some other `Exception` subtype (if the `stat` operation failed: for example, if the path does not exist).
974

975
To determine when a file was modified, compare `!(current isa StatStruct && prev == current)` to detect
976
notification of changes to the mtime or inode. However, using [`watch_file`](@ref) for this operation
977
is preferred, since it is more reliable and efficient, although in some situations it may not be available.
978

979
This is a thin wrapper over calling `wait` on a [`PollingFileWatcher`](@ref), which implements
980
the functionality, but this function has a small race window between consecutive calls to
981
`poll_file` where the file might change without being detected.
982
"""
983
function poll_file(s::AbstractString, interval_seconds::Real=5.007, timeout_s::Real=-1)
×
984
    pfw = PollingFileWatcher(s, Float64(interval_seconds))
×
985
    local timer
×
986
    try
×
987
        if timeout_s >= 0
×
988
            timer = Timer(timeout_s) do t
×
989
                close(pfw)
×
990
            end
991
        end
992
        return wait(pfw)
×
993
    finally
994
        close(pfw)
×
995
        @isdefined(timer) && close(timer)
×
996
    end
997
end
998

999
include("pidfile.jl")
1000
import .Pidfile: mkpidlock, trymkpidlock
1001

UNCOV
1002
function __init__()
×
UNCOV
1003
    Base.mkpidlock_hook = mkpidlock
×
UNCOV
1004
    Base.trymkpidlock_hook = trymkpidlock
×
UNCOV
1005
    Base.parse_pidfile_hook = Pidfile.parse_pidfile
×
1006
    nothing
×
1007
end
1008

1009
end
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc