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

JuliaLang / julia / #37758

23 Apr 2024 09:56PM UTC coverage: 87.451% (+0.02%) from 87.428%
#37758

push

local

web-flow
Better error message for task.scope (#54180)

Fixes #54178

1 of 1 new or added line in 1 file covered. (100.0%)

115 existing lines in 10 files now uncovered.

76384 of 87345 relevant lines covered (87.45%)

15888871.85 hits per line

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

79.71
/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):
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
    _sizeof_uv_poll, _sizeof_uv_fs_poll, _sizeof_uv_fs_event, _uv_hook_close, uv_error, _UVError,
26
    iolock_begin, iolock_end, associate_julia_struct, disassociate_julia_struct,
27
    preserve_handle, unpreserve_handle, isreadable, iswritable, isopen,
28
    |, getproperty, propertynames
29
import Base.Filesystem.StatStruct
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
42
    FileEvent(r::Bool, c::Bool, t::Bool) = new(r, c, t)
136✔
43
end
44
FileEvent() = FileEvent(false, false, true)
×
45
FileEvent(flags::Integer) = FileEvent((flags & UV_RENAME) != 0,
134✔
46
                                      (flags & UV_CHANGE) != 0,
47
                                      false)
48
|(a::FileEvent, b::FileEvent) =
33✔
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)
5,341✔
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
979✔
64

65
function getproperty(f::FDEvent, field::Symbol)
300✔
66
    events = getfield(f, :events)
28,184✔
67
    field === :readable && return (events & UV_READABLE) != 0
27,506✔
68
    field === :writable && return (events & UV_WRITABLE) != 0
21,053✔
69
    field === :disconnect && return (events & UV_DISCONNECT) != 0
15,744✔
70
    field === :prioritized && return (events & UV_PRIORITIZED) != 0
14,086✔
71
    field === :timedout && return events == 0
14,086✔
72
    field === :events && return Int(events)
4,139✔
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,658✔
78
iswritable(f::FDEvent) = f.writable
1,658✔
79
|(a::FDEvent, b::FDEvent) = FDEvent(getfield(a, :events) | getfield(b, :events))
×
80

81
mutable struct FileMonitor
82
    @atomic handle::Ptr{Cvoid}
83
    file::String
84
    notify::Base.ThreadSynchronizer
85
    events::Int32
86
    active::Bool
87
    FileMonitor(file::AbstractString) = FileMonitor(String(file))
×
88
    function FileMonitor(file::String)
46✔
89
        handle = Libc.malloc(_sizeof_uv_fs_event)
46✔
90
        this = new(handle, file, Base.ThreadSynchronizer(), 0, false)
46✔
91
        associate_julia_struct(handle, this)
46✔
92
        iolock_begin()
46✔
93
        err = ccall(:uv_fs_event_init, Cint, (Ptr{Cvoid}, Ptr{Cvoid}), eventloop(), handle)
46✔
94
        if err != 0
46✔
95
            Libc.free(handle)
×
96
            throw(_UVError("FileMonitor", err))
×
97
        end
98
        iolock_end()
46✔
99
        finalizer(uvfinalize, this)
46✔
100
        return this
46✔
101
    end
102
end
103

104
mutable struct FolderMonitor
105
    @atomic handle::Ptr{Cvoid}
106
    # notify::Channel{Any} # eltype = Union{Pair{String, FileEvent}, IOError}
107
    notify::Base.ThreadSynchronizer
108
    channel::Vector{Any} # eltype = Pair{String, FileEvent}
109
    FolderMonitor(folder::AbstractString) = FolderMonitor(String(folder))
×
110
    function FolderMonitor(folder::String)
10✔
111
        handle = Libc.malloc(_sizeof_uv_fs_event)
10✔
112
        this = new(handle, Base.ThreadSynchronizer(), [])
10✔
113
        associate_julia_struct(handle, this)
10✔
114
        iolock_begin()
10✔
115
        err = ccall(:uv_fs_event_init, Cint, (Ptr{Cvoid}, Ptr{Cvoid}), eventloop(), handle)
10✔
116
        if err != 0
10✔
117
            Libc.free(handle)
×
118
            throw(_UVError("FolderMonitor", err))
×
119
        end
120
        finalizer(uvfinalize, this)
10✔
121
        uv_error("FolderMonitor (start)",
10✔
122
                 ccall(:uv_fs_event_start, Int32, (Ptr{Cvoid}, Ptr{Cvoid}, Cstring, Int32),
123
                       handle, uv_jl_fseventscb_folder::Ptr{Cvoid}, folder, 0))
124
        iolock_end()
8✔
125
        return this
8✔
126
    end
127
end
128

129
mutable struct PollingFileWatcher
130
    @atomic handle::Ptr{Cvoid}
131
    file::String
132
    interval::UInt32
133
    notify::Base.ThreadSynchronizer
134
    active::Bool
135
    curr_error::Int32
136
    curr_stat::StatStruct
137
    PollingFileWatcher(file::AbstractString, interval::Float64=5.007) = PollingFileWatcher(String(file), interval)
×
138
    function PollingFileWatcher(file::String, interval::Float64=5.007) # same default as nodejs
25✔
139
        handle = Libc.malloc(_sizeof_uv_fs_poll)
25✔
140
        this = new(handle, file, round(UInt32, interval * 1000), Base.ThreadSynchronizer(), false, 0, StatStruct())
25✔
141
        associate_julia_struct(handle, this)
25✔
142
        iolock_begin()
25✔
143
        err = ccall(:uv_fs_poll_init, Int32, (Ptr{Cvoid}, Ptr{Cvoid}), eventloop(), handle)
25✔
144
        if err != 0
25✔
145
            Libc.free(handle)
×
146
            throw(_UVError("PollingFileWatcher", err))
×
147
        end
148
        finalizer(uvfinalize, this)
25✔
149
        iolock_end()
25✔
150
        return this
25✔
151
    end
152
end
153

154
mutable struct _FDWatcher
155
    @atomic handle::Ptr{Cvoid}
156
    fdnum::Int # this is NOT the file descriptor
157
    refcount::Tuple{Int, Int}
158
    notify::Base.ThreadSynchronizer
159
    events::Int32
160
    active::Tuple{Bool, Bool}
161

162
    let FDWatchers = Vector{Any}() # n.b.: this structure and the refcount are protected by the iolock
163
        global _FDWatcher, uvfinalize
164
        @static if Sys.isunix()
165
            _FDWatcher(fd::RawFD, mask::FDEvent) = _FDWatcher(fd, mask.readable, mask.writable)
979✔
166
            function _FDWatcher(fd::RawFD, readable::Bool, writable::Bool)
979✔
167
                fdnum = Core.Intrinsics.bitcast(Int32, fd) + 1
979✔
168
                if fdnum <= 0
979✔
169
                    throw(ArgumentError("Passed file descriptor fd=$(fd) is not a valid file descriptor"))
1✔
170
                elseif !readable && !writable
978✔
171
                    throw(ArgumentError("must specify at least one of readable or writable to create a FDWatcher"))
×
172
                end
173

174
                iolock_begin()
978✔
175
                if fdnum > length(FDWatchers)
978✔
176
                    old_len = length(FDWatchers)
157✔
177
                    resize!(FDWatchers, fdnum)
157✔
178
                    FDWatchers[(old_len + 1):fdnum] .= nothing
465✔
179
                elseif FDWatchers[fdnum] !== nothing
821✔
180
                    this = FDWatchers[fdnum]::_FDWatcher
342✔
181
                    this.refcount = (this.refcount[1] + Int(readable), this.refcount[2] + Int(writable))
342✔
182
                    iolock_end()
342✔
183
                    return this
342✔
184
                end
185
                if ccall(:jl_uv_unix_fd_is_watched, Int32, (RawFD, Ptr{Cvoid}, Ptr{Cvoid}), fd, C_NULL, eventloop()) == 1
636✔
186
                    throw(ArgumentError("$(fd) is already being watched by libuv"))
×
187
                end
188

189
                handle = Libc.malloc(_sizeof_uv_poll)
636✔
190
                this = new(
636✔
191
                    handle,
192
                    fdnum,
193
                    (Int(readable), Int(writable)),
194
                    Base.ThreadSynchronizer(),
195
                    Int32(0),
196
                    (false, false))
197
                associate_julia_struct(handle, this)
636✔
198
                err = ccall(:uv_poll_init, Int32, (Ptr{Cvoid}, Ptr{Cvoid}, RawFD), eventloop(), handle, fd)
636✔
199
                if err != 0
636✔
200
                    Libc.free(handle)
×
201
                    throw(_UVError("FDWatcher", err))
×
202
                end
203
                finalizer(uvfinalize, this)
636✔
204
                FDWatchers[fdnum] = this
636✔
205
                iolock_end()
636✔
206
                return this
636✔
207
            end
208
        end
209

210
        function uvfinalize(t::_FDWatcher)
1,924✔
211
            iolock_begin()
1,924✔
212
            lock(t.notify)
1,924✔
213
            try
1,924✔
214
                if t.handle != C_NULL
1,924✔
215
                    disassociate_julia_struct(t)
636✔
216
                    ccall(:jl_close_uv, Cvoid, (Ptr{Cvoid},), t.handle)
636✔
217
                    @atomic :monotonic t.handle = C_NULL
636✔
218
                end
219
                t.refcount = (0, 0)
1,924✔
220
                t.active = (false, false)
1,924✔
221
                @static if Sys.isunix()
222
                    if FDWatchers[t.fdnum] === t
1,924✔
223
                        FDWatchers[t.fdnum] = nothing
636✔
224
                    end
225
                end
226
                notify(t.notify, Int32(0))
1,924✔
227
            finally
228
                unlock(t.notify)
1,924✔
229
            end
230
            iolock_end()
1,924✔
231
            nothing
400✔
232
        end
233
    end
234

235
    @static if Sys.iswindows()
236
        _FDWatcher(fd::RawFD, mask::FDEvent) = _FDWatcher(fd, mask.readable, mask.writable)
×
237
        function _FDWatcher(fd::RawFD, readable::Bool, writable::Bool)
×
238
            fdnum = Core.Intrinsics.bitcast(Int32, fd) + 1
×
239
            if fdnum <= 0
×
240
                throw(ArgumentError("Passed file descriptor fd=$(fd) is not a valid file descriptor"))
×
241
            end
242

243
            handle = Libc._get_osfhandle(fd)
×
244
            return _FDWatcher(handle, readable, writable)
×
245
        end
246
        _FDWatcher(fd::WindowsRawSocket, mask::FDEvent) = _FDWatcher(fd, mask.readable, mask.writable)
×
247
        function _FDWatcher(fd::WindowsRawSocket, readable::Bool, writable::Bool)
×
248
            if fd == Base.INVALID_OS_HANDLE
×
249
                throw(ArgumentError("Passed file descriptor fd=$(fd) is not a valid file descriptor"))
×
250
            elseif !readable && !writable
×
251
                throw(ArgumentError("must specify at least one of readable or writable to create a FDWatcher"))
×
252
            end
253

254
            handle = Libc.malloc(_sizeof_uv_poll)
×
255
            this = new(
×
256
                handle,
257
                0,
258
                (Int(readable), Int(writable)),
259
                Base.ThreadSynchronizer(),
260
                0,
261
                (false, false))
262
            associate_julia_struct(handle, this)
×
263
            iolock_begin()
×
264
            err = ccall(:uv_poll_init, Int32, (Ptr{Cvoid},  Ptr{Cvoid}, WindowsRawSocket),
×
265
                                               eventloop(), handle,     fd)
266
            iolock_end()
×
267
            if err != 0
×
268
                Libc.free(handle)
×
269
                throw(_UVError("FDWatcher", err))
×
270
            end
271
            finalizer(uvfinalize, this)
×
272
            return this
×
273
        end
274
    end
275
end
276

277
mutable struct FDWatcher
278
    # WARNING: make sure `close` has been manually called on this watcher before closing / destroying `fd`
279
    watcher::_FDWatcher
280
    mask::FDEvent
281
    function FDWatcher(fd::RawFD, readable::Bool, writable::Bool)
1✔
282
        return FDWatcher(fd, FDEvent(readable, writable, false, false))
679✔
283
    end
284
    function FDWatcher(fd::RawFD, mask::FDEvent)
285
        this = new(_FDWatcher(fd, mask), mask)
679✔
286
        finalizer(close, this)
678✔
287
        return this
×
288
    end
289
    @static if Sys.iswindows()
290
        function FDWatcher(fd::WindowsRawSocket, readable::Bool, writable::Bool)
×
291
            return FDWatcher(fd, FDEvent(readable, writable, false, false))
×
292
        end
293
        function FDWatcher(fd::WindowsRawSocket, mask::FDEvent)
×
294
            this = new(_FDWatcher(fd, mask), mask)
×
295
            finalizer(close, this)
×
296
            return this
×
297
        end
298
    end
299
end
300

301
function getproperty(fdw::FDWatcher, s::Symbol)
302
    # support deprecated field names
303
    s === :readable && return fdw.mask.readable
22,069✔
304
    s === :writable && return fdw.mask.writable
18,075✔
305
    return getfield(fdw, s)
19,971✔
306
end
307

308

309
close(t::_FDWatcher, mask::FDEvent) = close(t, mask.readable, mask.writable)
1,656✔
310
function close(t::_FDWatcher, readable::Bool, writable::Bool)
1,656✔
311
    iolock_begin()
1,656✔
312
    if t.refcount != (0, 0)
2,309✔
313
        t.refcount = (t.refcount[1] - Int(readable), t.refcount[2] - Int(writable))
1,004✔
314
    end
315
    if t.refcount == (0, 0)
2,947✔
316
        uvfinalize(t)
1,288✔
317
    else
318
        @lock t.notify notify(t.notify, Int32(0))
368✔
319
    end
320
    iolock_end()
1,656✔
321
    nothing
300✔
322
end
323

324
function close(t::FDWatcher)
678✔
325
    mask = t.mask
1,356✔
326
    t.mask = FDEvent()
1,356✔
327
    close(t.watcher, mask)
1,356✔
328
end
329

330
function uvfinalize(uv::Union{FileMonitor, FolderMonitor, PollingFileWatcher})
80✔
331
    iolock_begin()
80✔
332
    if uv.handle != C_NULL
80✔
333
        disassociate_julia_struct(uv) # close (and free) without notify
22✔
334
        ccall(:jl_close_uv, Cvoid, (Ptr{Cvoid},), uv.handle)
22✔
335
    end
336
    iolock_end()
80✔
337
end
338

339
function close(t::Union{FileMonitor, FolderMonitor, PollingFileWatcher})
340
    iolock_begin()
118✔
341
    if t.handle != C_NULL
118✔
342
        ccall(:jl_close_uv, Cvoid, (Ptr{Cvoid},), t.handle)
88✔
343
    end
344
    iolock_end()
118✔
345
end
346

347
function _uv_hook_close(uv::_FDWatcher)
×
348
    # fyi: jl_atexit_hook can cause this to get called too
349
    Libc.free(@atomicswap :monotonic uv.handle = C_NULL)
×
350
    uvfinalize(uv)
×
351
    nothing
×
352
end
353

354
function _uv_hook_close(uv::PollingFileWatcher)
7✔
355
    lock(uv.notify)
7✔
356
    try
7✔
357
        uv.active = false
7✔
358
        Libc.free(@atomicswap :monotonic uv.handle = C_NULL)
7✔
359
        notify(uv.notify, StatStruct())
7✔
360
    finally
361
        unlock(uv.notify)
7✔
362
    end
363
    nothing
7✔
364
end
365

366
function _uv_hook_close(uv::FileMonitor)
45✔
367
    lock(uv.notify)
45✔
368
    try
45✔
369
        uv.active = false
45✔
370
        Libc.free(@atomicswap :monotonic uv.handle = C_NULL)
45✔
371
        notify(uv.notify, FileEvent())
45✔
372
    finally
373
        unlock(uv.notify)
45✔
374
    end
375
    nothing
×
376
end
377

378
function _uv_hook_close(uv::FolderMonitor)
7✔
379
    lock(uv.notify)
7✔
380
    try
7✔
381
        Libc.free(@atomicswap :monotonic uv.handle = C_NULL)
7✔
382
        notify_error(uv.notify, EOFError())
7✔
383
    finally
384
        unlock(uv.notify)
7✔
385
    end
386
    nothing
7✔
387
end
388

389
isopen(fm::FileMonitor) = fm.handle != C_NULL
×
390
isopen(fm::FolderMonitor) = fm.handle != C_NULL
×
391
isopen(pfw::PollingFileWatcher) = pfw.handle != C_NULL
×
392
isopen(pfw::_FDWatcher) = pfw.refcount != (0, 0)
2,354✔
393
isopen(pfw::FDWatcher) = !pfw.mask.timedout
3,737✔
394

395
function uv_fseventscb_file(handle::Ptr{Cvoid}, filename::Ptr, events::Int32, status::Int32)
12✔
396
    t = @handle_as handle FileMonitor
12✔
397
    lock(t.notify)
12✔
398
    try
12✔
399
        if status != 0
12✔
400
            notify_error(t.notify, _UVError("FileMonitor", status))
×
401
        else
402
            t.events |= events
12✔
403
            notify(t.notify, FileEvent(events))
24✔
404
        end
405
    finally
406
        unlock(t.notify)
12✔
407
    end
408
    nothing
×
409
end
410

411
function uv_fseventscb_folder(handle::Ptr{Cvoid}, filename::Ptr, events::Int32, status::Int32)
72✔
412
    t = @handle_as handle FolderMonitor
72✔
413
    lock(t.notify)
72✔
414
    try
72✔
415
        if status != 0
72✔
416
            notify_error(t.notify, _UVError("FolderMonitor", status))
×
417
        else
418
            fname = (filename == C_NULL) ? "" : unsafe_string(convert(Cstring, filename))
144✔
419
            push!(t.channel, fname => FileEvent(events))
72✔
420
            notify(t.notify)
144✔
421
        end
422
    finally
423
        unlock(t.notify)
72✔
424
    end
425
    nothing
×
426
end
427

428
function uv_pollcb(handle::Ptr{Cvoid}, status::Int32, events::Int32)
2,266✔
429
    t = @handle_as handle _FDWatcher
2,266✔
430
    lock(t.notify)
2,266✔
431
    try
2,266✔
432
        if status != 0
2,266✔
433
            notify_error(t.notify, _UVError("FDWatcher", status))
×
434
        else
435
            t.events |= events
2,266✔
436
            if t.active[1] || t.active[2]
2,606✔
437
                if isempty(t.notify)
2,266✔
438
                    # if we keep hearing about events when nobody appears to be listening,
439
                    # stop the poll to save cycles
440
                    t.active = (false, false)
1,102✔
441
                    ccall(:uv_poll_stop, Int32, (Ptr{Cvoid},), t.handle)
1,102✔
442
                end
443
            end
444
            notify(t.notify, events)
4,532✔
445
        end
446
    finally
447
        unlock(t.notify)
2,266✔
448
    end
449
    nothing
×
450
end
451

452
function uv_fspollcb(handle::Ptr{Cvoid}, status::Int32, prev::Ptr, curr::Ptr)
5✔
453
    t = @handle_as handle PollingFileWatcher
5✔
454
    old_status = t.curr_error
5✔
455
    t.curr_error = status
5✔
456
    if status == 0
5✔
457
        t.curr_stat = StatStruct(convert(Ptr{UInt8}, curr))
3✔
458
    end
459
    if status == 0 || status != old_status
7✔
460
        prev_stat = StatStruct(convert(Ptr{UInt8}, prev))
4✔
461
        lock(t.notify)
4✔
462
        try
4✔
463
            notify(t.notify, prev_stat)
4✔
464
        finally
465
            unlock(t.notify)
4✔
466
        end
467
    end
468
    nothing
×
469
end
470

471
global uv_jl_pollcb::Ptr{Cvoid}
472
global uv_jl_fspollcb::Ptr{Cvoid}
473
global uv_jl_fseventscb_file::Ptr{Cvoid}
474
global uv_jl_fseventscb_folder::Ptr{Cvoid}
475

476
function __init__()
345✔
477
    global uv_jl_pollcb = @cfunction(uv_pollcb, Cvoid, (Ptr{Cvoid}, Cint, Cint))
345✔
478
    global uv_jl_fspollcb = @cfunction(uv_fspollcb, Cvoid, (Ptr{Cvoid}, Cint, Ptr{Cvoid}, Ptr{Cvoid}))
345✔
479
    global uv_jl_fseventscb_file = @cfunction(uv_fseventscb_file, Cvoid, (Ptr{Cvoid}, Ptr{Int8}, Int32, Int32))
345✔
480
    global uv_jl_fseventscb_folder = @cfunction(uv_fseventscb_folder, Cvoid, (Ptr{Cvoid}, Ptr{Int8}, Int32, Int32))
345✔
481

482
    Base.mkpidlock_hook = mkpidlock
345✔
483
    Base.trymkpidlock_hook = trymkpidlock
345✔
484
    Base.parse_pidfile_hook = Pidfile.parse_pidfile
345✔
485

486
    nothing
×
487
end
488

489
function start_watching(t::_FDWatcher)
2,181✔
490
    iolock_begin()
2,181✔
491
    t.handle == C_NULL && throw(ArgumentError("FDWatcher is closed"))
2,181✔
492
    readable = t.refcount[1] > 0
2,181✔
493
    writable = t.refcount[2] > 0
2,181✔
494
    if t.active[1] != readable || t.active[2] != writable
2,431✔
495
        # make sure the READABLE / WRITEABLE state is updated
496
        uv_error("FDWatcher (start)",
4,102✔
497
                 ccall(:uv_poll_start, Int32, (Ptr{Cvoid}, Int32, Ptr{Cvoid}),
498
                       t.handle,
499
                       (readable ? UV_READABLE : 0) | (writable ? UV_WRITABLE : 0),
500
                       uv_jl_pollcb::Ptr{Cvoid}))
501
        t.active = (readable, writable)
2,104✔
502
    end
503
    iolock_end()
2,181✔
504
    nothing
2,181✔
505
end
506

507
function start_watching(t::PollingFileWatcher)
8✔
508
    iolock_begin()
8✔
509
    t.handle == C_NULL && throw(ArgumentError("PollingFileWatcher is closed"))
8✔
510
    if !t.active
8✔
511
        uv_error("PollingFileWatcher (start)",
8✔
512
                 ccall(:uv_fs_poll_start, Int32, (Ptr{Cvoid}, Ptr{Cvoid}, Cstring, UInt32),
513
                       t.handle, uv_jl_fspollcb::Ptr{Cvoid}, t.file, t.interval))
514
        t.active = true
7✔
515
    end
516
    iolock_end()
7✔
517
    nothing
7✔
518
end
519

520
function stop_watching(t::PollingFileWatcher)
7✔
521
    iolock_begin()
7✔
522
    lock(t.notify)
7✔
523
    try
7✔
524
        if t.active && isempty(t.notify)
7✔
525
            t.active = false
4✔
526
            uv_error("PollingFileWatcher (stop)",
4✔
527
                     ccall(:uv_fs_poll_stop, Int32, (Ptr{Cvoid},), t.handle))
528
        end
529
    finally
530
        unlock(t.notify)
7✔
531
    end
532
    iolock_end()
7✔
533
    nothing
7✔
534
end
535

536
function start_watching(t::FileMonitor)
46✔
537
    iolock_begin()
46✔
538
    t.handle == C_NULL && throw(ArgumentError("FileMonitor is closed"))
46✔
539
    if !t.active
46✔
540
        uv_error("FileMonitor (start)",
46✔
541
                 ccall(:uv_fs_event_start, Int32, (Ptr{Cvoid}, Ptr{Cvoid}, Cstring, Int32),
542
                       t.handle, uv_jl_fseventscb_file::Ptr{Cvoid}, t.file, 0))
543
        t.active = true
33✔
544
    end
545
    iolock_end()
33✔
546
    nothing
×
547
end
548

549
function stop_watching(t::FileMonitor)
33✔
550
    iolock_begin()
33✔
551
    lock(t.notify)
33✔
552
    try
33✔
553
        if t.active && isempty(t.notify)
33✔
554
            t.active = false
6✔
555
            uv_error("FileMonitor (stop)",
6✔
556
                     ccall(:uv_fs_event_stop, Int32, (Ptr{Cvoid},), t.handle))
557
        end
558
    finally
559
        unlock(t.notify)
33✔
560
    end
561
    iolock_end()
33✔
562
    nothing
×
563
end
564

565
# n.b. this _wait may return spuriously early with a timedout event
566
function _wait(fdw::_FDWatcher, mask::FDEvent)
2,181✔
567
    iolock_begin()
2,181✔
568
    preserve_handle(fdw)
2,181✔
569
    lock(fdw.notify)
2,181✔
570
    try
2,181✔
571
        events = FDEvent(fdw.events & mask.events)
2,181✔
572
        if !isopen(fdw) # !open
2,354✔
573
            throw(EOFError())
×
574
        elseif events.timedout
2,181✔
575
            start_watching(fdw) # make sure the poll is active
2,181✔
576
            iolock_end()
2,181✔
577
            return FDEvent(wait(fdw.notify)::Int32)
2,181✔
578
        else
UNCOV
579
            iolock_end()
×
UNCOV
580
            return events
×
581
        end
582
    finally
583
        unlock(fdw.notify)
2,181✔
584
        unpreserve_handle(fdw)
2,181✔
585
    end
586
end
587

588
function wait(fdw::_FDWatcher; readable=true, writable=true)
×
589
    return wait(fdw, FDEvent(readable, writable, false, false))
×
590
end
591
function wait(fdw::_FDWatcher, mask::FDEvent)
×
592
    while true
×
593
        mask.timedout && return mask
×
594
        events = _wait(fdw, mask)
×
595
        if !events.timedout
×
596
            @lock fdw.notify fdw.events &= ~events.events
×
597
            return events
×
598
        end
599
    end
×
600
end
601

602
function wait(fdw::FDWatcher)
1,868✔
603
    isopen(fdw) || throw(EOFError())
1,868✔
604
    while true
1,869✔
605
        events = GC.@preserve fdw _wait(fdw.watcher, fdw.mask)
1,869✔
606
        isopen(fdw) || throw(EOFError())
2,079✔
607
        if !events.timedout
1,659✔
608
            @lock fdw.watcher.notify fdw.watcher.events &= ~events.events
1,658✔
609
            return events
1,658✔
610
        end
611
    end
1✔
612
end
613

614
function wait(socket::RawFD; readable=false, writable=false)
×
615
    return wait(socket, FDEvent(readable, writable, false, false))
×
616
end
617
function wait(fd::RawFD, mask::FDEvent)
×
618
    fdw = _FDWatcher(fd, mask)
×
619
    try
×
620
        return wait(fdw, mask)
×
621
    finally
622
        close(fdw, mask)
×
623
    end
624
end
625

626

627
if Sys.iswindows()
628
    function wait(socket::WindowsRawSocket; readable=false, writable=false)
×
629
        return wait(socket, FDEvent(readable, writable, false, false))
×
630
    end
631
    function wait(socket::WindowsRawSocket, mask::FDEvent)
×
632
        fdw = _FDWatcher(socket, mask)
×
633
        try
×
634
            return wait(fdw, mask)
×
635
        finally
636
            close(fdw, mask)
×
637
        end
638
    end
639
end
640

641
function wait(pfw::PollingFileWatcher)
8✔
642
    iolock_begin()
8✔
643
    preserve_handle(pfw)
8✔
644
    lock(pfw.notify)
8✔
645
    local prevstat
646
    try
8✔
647
        start_watching(pfw)
8✔
648
        iolock_end()
7✔
649
        prevstat = wait(pfw.notify)::StatStruct
7✔
650
        unlock(pfw.notify)
7✔
651
        iolock_begin()
7✔
652
        lock(pfw.notify)
8✔
653
    finally
654
        unlock(pfw.notify)
8✔
655
        unpreserve_handle(pfw)
8✔
656
    end
657
    stop_watching(pfw)
7✔
658
    iolock_end()
7✔
659
    if pfw.handle == C_NULL
7✔
660
        return prevstat, EOFError()
3✔
661
    elseif pfw.curr_error != 0
4✔
662
        return prevstat, _UVError("PollingFileWatcher", pfw.curr_error)
1✔
663
    else
664
        return prevstat, pfw.curr_stat
3✔
665
    end
666
end
667

668
function wait(m::FileMonitor)
46✔
669
    iolock_begin()
46✔
670
    preserve_handle(m)
46✔
671
    lock(m.notify)
46✔
672
    local events
673
    try
46✔
674
        start_watching(m)
46✔
675
        iolock_end()
33✔
676
        events = wait(m.notify)::FileEvent
33✔
677
        events |= FileEvent(m.events)
33✔
678
        m.events = 0
33✔
679
        unlock(m.notify)
33✔
680
        iolock_begin()
33✔
681
        lock(m.notify)
46✔
682
    finally
683
        unlock(m.notify)
46✔
684
        unpreserve_handle(m)
46✔
685
    end
686
    stop_watching(m)
33✔
687
    iolock_end()
33✔
688
    return events
33✔
689
end
690

691
function wait(m::FolderMonitor)
20✔
692
    m.handle == C_NULL && throw(EOFError())
20✔
693
    preserve_handle(m)
20✔
694
    lock(m.notify)
20✔
695
    evt = try
20✔
696
            m.handle == C_NULL && throw(EOFError())
20✔
697
            while isempty(m.channel)
30✔
698
                wait(m.notify)
10✔
699
            end
10✔
700
            popfirst!(m.channel)
20✔
701
        finally
702
            unlock(m.notify)
20✔
703
            unpreserve_handle(m)
20✔
704
        end
705
    return evt::Pair{String, FileEvent}
20✔
706
end
707

708

709
"""
710
    poll_fd(fd, timeout_s::Real=-1; readable=false, writable=false)
711

712
Monitor a file descriptor `fd` for changes in the read or write availability, and with a
713
timeout given by `timeout_s` seconds.
714

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

718
The returned value is an object with boolean fields `readable`, `writable`, and `timedout`,
719
giving the result of the polling.
720
"""
721
function poll_fd(s::Union{RawFD, Sys.iswindows() ? WindowsRawSocket : Union{}}, timeout_s::Real=-1; readable=false, writable=false)
600✔
722
    mask = FDEvent(readable, writable, false, false)
300✔
723
    mask.timedout && return mask
300✔
724
    fdw = _FDWatcher(s, mask)
300✔
725
    local timer
726
    # we need this flag to explicitly track whether we call `close` already, to update the internal refcount correctly
727
    timedout = false # TODO: make this atomic
300✔
728
    try
300✔
729
        if timeout_s >= 0
300✔
730
            # delay creating the timer until shortly before we start the poll wait
731
            timer = Timer(timeout_s) do t
300✔
732
                timedout && return
100✔
733
                timedout = true
100✔
734
                close(fdw, mask)
100✔
735
            end
736
            try
300✔
737
                while true
312✔
738
                    events = _wait(fdw, mask)
312✔
739
                    if timedout || !events.timedout
524✔
740
                        @lock fdw.notify fdw.events &= ~events.events
300✔
741
                        return events
300✔
742
                    end
743
                end
12✔
744
            catch ex
745
                ex isa EOFError || rethrow()
×
746
                return FDEvent()
×
747
            end
748
        else
749
            return wait(fdw, mask)
×
750
        end
751
    finally
752
        if @isdefined(timer)
300✔
753
            if !timedout
300✔
754
                timedout = true
200✔
755
                close(timer)
200✔
756
                close(fdw, mask)
200✔
757
            end
758
        else
759
            close(fdw, mask)
300✔
760
        end
761
    end
762
end
763

764
"""
765
    watch_file(path::AbstractString, timeout_s::Real=-1)
766

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

772
The returned value is an object with boolean fields `renamed`, `changed`, and `timedout`,
773
giving the result of watching the file.
774

775
This behavior of this function varies slightly across platforms. See
776
<https://nodejs.org/api/fs.html#fs_caveats> for more detailed information.
777
"""
778
function watch_file(s::String, timeout_s::Float64=-1.0)
45✔
779
    fm = FileMonitor(s)
45✔
780
    local timer
781
    try
44✔
782
        if timeout_s >= 0
44✔
783
            timer = Timer(timeout_s) do t
43✔
784
                close(fm)
27✔
785
            end
786
        end
787
        return wait(fm)
57✔
788
    finally
789
        close(fm)
44✔
790
        @isdefined(timer) && close(timer)
44✔
791
    end
792
end
793
watch_file(s::AbstractString, timeout_s::Real=-1) = watch_file(String(s), Float64(timeout_s))
33✔
794

795
"""
796
    watch_folder(path::AbstractString, timeout_s::Real=-1)
797

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

803
This will continuing tracking changes for `path` in the background until
804
`unwatch_folder` is called on the same `path`.
805

806
The returned value is an pair where the first field is the name of the changed file (if available)
807
and the second field is an object with boolean fields `renamed`, `changed`, and `timedout`,
808
giving the event.
809

810
This behavior of this function varies slightly across platforms. See
811
<https://nodejs.org/api/fs.html#fs_caveats> for more detailed information.
812
"""
813
watch_folder(s::AbstractString, timeout_s::Real=-1) = watch_folder(String(s), timeout_s)
×
814
function watch_folder(s::String, timeout_s::Real=-1)
54✔
815
    fm = get!(watched_folders, s) do
55✔
816
        return FolderMonitor(s)
6✔
817
    end
818
    local timer
819
    if timeout_s >= 0
51✔
820
        @lock fm.notify isempty(fm.channel) || return popfirst!(fm.channel)
90✔
821
        if timeout_s <= 0.010
10✔
822
            # for very small timeouts, we can just sleep for the whole timeout-interval
823
            (timeout_s == 0) ? yield() : sleep(timeout_s)
9✔
824
            @lock fm.notify isempty(fm.channel) || return popfirst!(fm.channel)
8✔
825
            return "" => FileEvent() # timeout
6✔
826
        else
827
            timer = Timer(timeout_s) do t
3✔
828
                @lock fm.notify notify(fm.notify)
3✔
829
            end
830
        end
831
    end
832
    # inline a copy of `wait` with added support for checking timer
833
    fm.handle == C_NULL && throw(EOFError())
4✔
834
    preserve_handle(fm)
4✔
835
    lock(fm.notify)
4✔
836
    evt = try
4✔
837
            fm.handle == C_NULL && throw(EOFError())
4✔
838
            while isempty(fm.channel)
8✔
839
                if @isdefined(timer)
7✔
840
                    isopen(timer) || return "" => FileEvent() # timeout
9✔
841
                end
842
                wait(fm.notify)
4✔
843
            end
4✔
844
            popfirst!(fm.channel)
1✔
845
        finally
846
            unlock(fm.notify)
4✔
847
            unpreserve_handle(fm)
4✔
848
            @isdefined(timer) && close(timer)
4✔
849
        end
850
    return evt::Pair{String, FileEvent}
1✔
851
end
852

853
"""
854
    unwatch_folder(path::AbstractString)
855

856
Stop background tracking of changes for `path`.
857
It is not recommended to do this while another task is waiting for
858
`watch_folder` to return on the same path, as the result may be unpredictable.
859
"""
860
unwatch_folder(s::AbstractString) = unwatch_folder(String(s))
×
861
function unwatch_folder(s::String)
1✔
862
    fm = pop!(watched_folders, s, nothing)
4✔
863
    fm === nothing || close(fm)
8✔
864
    nothing
4✔
865
end
866

867
const watched_folders = Dict{String, FolderMonitor}()
868

869
"""
870
    poll_file(path::AbstractString, interval_s::Real=5.007, timeout_s::Real=-1) -> (previous::StatStruct, current)
871

872
Monitor a file for changes by polling every `interval_s` seconds until a change occurs or
873
`timeout_s` seconds have elapsed. The `interval_s` should be a long period; the default is
874
5.007 seconds.
875

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

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

883
To determine when a file was modified, compare `current isa StatStruct && mtime(prev) != mtime(current)` to detect
884
notification of changes. However, using [`watch_file`](@ref) for this operation is preferred, since
885
it is more reliable and efficient, although in some situations it may not be available.
886
"""
887
function poll_file(s::AbstractString, interval_seconds::Real=5.007, timeout_s::Real=-1)
6✔
888
    pfw = PollingFileWatcher(s, Float64(interval_seconds))
6✔
889
    local timer
890
    try
5✔
891
        if timeout_s >= 0
5✔
892
            timer = Timer(timeout_s) do t
4✔
893
                close(pfw)
3✔
894
            end
895
        end
896
        statdiff = wait(pfw)
5✔
897
        if isa(statdiff[2], IOError)
6✔
898
            # file didn't initially exist, continue watching for it to be created (or the error to change)
899
            statdiff = wait(pfw)
1✔
900
        end
901
        return statdiff
5✔
902
    finally
903
        close(pfw)
5✔
904
        @isdefined(timer) && close(timer)
5✔
905
    end
906
end
907

908
include("pidfile.jl")
909
import .Pidfile: mkpidlock, trymkpidlock
910

911
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