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

JuliaLang / julia / #38182

15 Aug 2025 03:55AM UTC coverage: 77.87% (-0.4%) from 78.28%
#38182

push

local

web-flow
🤖 [master] Bump the SparseArrays stdlib from 30201ab to bb5ecc0 (#59263)

Stdlib: SparseArrays
URL: https://github.com/JuliaSparse/SparseArrays.jl.git
Stdlib branch: main
Julia branch: master
Old commit: 30201ab
New commit: bb5ecc0
Julia version: 1.13.0-DEV
SparseArrays version: 1.13.0
Bump invoked by: @ViralBShah
Powered by:
[BumpStdlibs.jl](https://github.com/JuliaLang/BumpStdlibs.jl)

Diff:
https://github.com/JuliaSparse/SparseArrays.jl/compare/30201abcb...bb5ecc091

```
$ git log --oneline 30201ab..bb5ecc0
bb5ecc0 fast quadratic form for dense matrix, sparse vectors (#640)
34ece87 Extend 3-arg `dot` to generic `HermOrSym` sparse matrices (#643)
095b685 Exclude unintended complex symmetric sparse matrices from 3-arg `dot` (#642)
8049287 Fix signature for 2-arg matrix-matrix `dot` (#641)
cff971d Make cond(::SparseMatrix, 1 / Inf) discoverable from 2-norm error (#629)
```

Co-authored-by: ViralBShah <744411+ViralBShah@users.noreply.github.com>

48274 of 61993 relevant lines covered (77.87%)

9571166.83 hits per line

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

38.6
/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)
1✔
43
end
44
FileEvent() = FileEvent(false, false, true)
×
45
FileEvent(flags::Integer) = FileEvent((flags & UV_RENAME) != 0,
1✔
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)
4,840✔
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
1,296✔
64

65
function getproperty(f::FDEvent, field::Symbol)
66
    events = getfield(f, :events)
42,801✔
67
    field === :readable && return (events & UV_READABLE) != 0
39,911✔
68
    field === :writable && return (events & UV_WRITABLE) != 0
27,999✔
69
    field === :disconnect && return (events & UV_DISCONNECT) != 0
19,296✔
70
    field === :prioritized && return (events & UV_PRIORITIZED) != 0
14,830✔
71
    field === :timedout && return events == 0
14,830✔
72
    field === :events && return Int(events)
3,454✔
73
    getfield(f, field)::Union{}
×
74
end
75
propertynames(f::FDEvent) = (:readable, :writable, :disconnect, :prioritized, :timedout, :events)
×
76

77
isreadable(f::FDEvent) = f.readable
4,466✔
78
iswritable(f::FDEvent) = f.writable
4,466✔
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)
1,632✔
119
    t = @handle_as handle _FDWatcher
1,632✔
120
    lock(t.notify)
1,632✔
121
    try
1,632✔
122
        if status != 0
1,632✔
123
            notify_error(t.notify, _UVError("FDWatcher", status))
×
124
        else
125
            t.events |= events
1,632✔
126
            if t.active[1] || t.active[2]
1,654✔
127
                if isempty(t.notify)
1,632✔
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)
74✔
131
                    ccall(:uv_poll_stop, Int32, (Ptr{Cvoid},), t.handle)
74✔
132
                end
133
            end
134
            notify(t.notify, events)
3,264✔
135
        end
136
    finally
137
        unlock(t.notify)
1,632✔
138
    end
139
    nothing
1,632✔
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()
10✔
164
                lock(pfw.notify)
10✔
165
                try
10✔
166
                    if pfw.timer === t # use identity check to test if this callback is stale by the time we got the lock
10✔
167
                        pfw.timer = nothing
6✔
168
                        @assert !pfw.active
6✔
169
                        if isopen(pfw) && !isempty(pfw.notify)
6✔
170
                            preserve_handle(pfw)
6✔
171
                            uv_jl_fspollcb = @cfunction(uv_fspollcb, Cvoid, (Ptr{Cvoid},))
6✔
172
                            err = ccall(:uv_fs_stat, Cint, (Ptr{Cvoid}, Ptr{Cvoid}, Cstring, Ptr{Cvoid}),
6✔
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
6✔
175
                            pfw.active = true
6✔
176
                        end
177
                    end
178
                finally
179
                    unlock(pfw.notify)
10✔
180
                end
181
                iolock_end()
10✔
182
                nothing
10✔
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))
25✔
316
        this = new(file, interval, Base.ThreadSynchronizer(), nothing, stat_req, false, false, 0, StatStruct())
25✔
317
        uv_req_set_data(stat_req, this)
25✔
318
        wait(this) # initialize with the current stat before return
25✔
319
        return this
20✔
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)
1,296✔
335
            function _FDWatcher(fd::RawFD, readable::Bool, writable::Bool)
105✔
336
                fdnum = Core.Intrinsics.bitcast(Int32, fd) + 1
105✔
337
                if fdnum <= 0
105✔
338
                    throw(ArgumentError("Passed file descriptor fd=$(fd) is not a valid file descriptor"))
×
339
                elseif !readable && !writable
105✔
340
                    throw(ArgumentError("must specify at least one of readable or writable to create a FDWatcher"))
×
341
                end
342

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

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

379
        function uvfinalize(t::_FDWatcher)
279✔
380
            iolock_begin()
279✔
381
            lock(t.notify)
279✔
382
            try
279✔
383
                if t.handle != C_NULL
279✔
384
                    disassociate_julia_struct(t)
94✔
385
                    ccall(:jl_close_uv, Cvoid, (Ptr{Cvoid},), t.handle)
94✔
386
                    @atomic :monotonic t.handle = C_NULL
94✔
387
                end
388
                t.refcount = (0, 0)
279✔
389
                t.active = (false, false)
279✔
390
                @static if Sys.isunix()
391
                    if FDWatchers[t.fdnum] === t
279✔
392
                        FDWatchers[t.fdnum] = nothing
94✔
393
                    end
394
                end
395
                notify(t.notify, Int32(0))
279✔
396
            finally
397
                unlock(t.notify)
279✔
398
            end
399
            iolock_end()
279✔
400
            nothing
279✔
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))
996✔
471
    end
472
    function FDWatcher(fd::RawFD, mask::FDEvent)
473
        this = new(_FDWatcher(fd, mask), mask)
996✔
474
        finalizer(close, this)
996✔
475
        return this
105✔
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 getfield(fdw, :mask).readable
22,821✔
492
    s === :writable && return getfield(fdw, :mask).writable
12,893✔
493
    return getfield(fdw, s)
13,228✔
494
end
495

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

511
function close(t::FDWatcher)
98✔
512
    mask = t.mask
1,094✔
513
    t.mask = FDEvent()
1,094✔
514
    close(t.watcher, mask)
1,094✔
515
end
516

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

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

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

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

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

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

580
isopen(fm::FileMonitor) = fm.handle != C_NULL
×
581
isopen(fm::FolderMonitor) = fm.handle != C_NULL
×
582
isopen(pfw::PollingFileWatcher) = !pfw.closed
6✔
583
isopen(pfw::_FDWatcher) = pfw.refcount != (0, 0)
1,590✔
584
isopen(pfw::FDWatcher) = !pfw.mask.timedout
3,156✔
585

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

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

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

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

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

662

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

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

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

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

781

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1010
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

© 2026 Coveralls, Inc