Skip to content

Commit b52995d

Browse files
temporarily add Pidfile.jl internally
1 parent b4da494 commit b52995d

File tree

4 files changed

+276
-0
lines changed

4 files changed

+276
-0
lines changed

Project.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ version = "1.8.0"
99
Artifacts = "56f22d72-fd6d-98f1-02f0-08ddc0907c33"
1010
Dates = "ade2ca70-3891-5945-98fb-dc099432e06a"
1111
Downloads = "f43a241f-c20a-4ad4-852c-f6b1247861c6"
12+
FileWatching = "7b1f6079-737a-58dc-b8bc-7a2ca5c1b5ee"
1213
LibGit2 = "76f85450-5226-5b5a-8eaa-529ad045b433"
1314
Libdl = "8f399da3-3557-5675-b5ff-fb832c97cbdb"
1415
Logging = "56ddb016-857b-54e1-b83d-db4d58db5568"

src/Pidfile/LICENSE.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
The Pidfile.jl package is licensed under the MIT "Expat" License:
2+
3+
> Copyright (c) 2018: Jameson Nash.
4+
>
5+
> Permission is hereby granted, free of charge, to any person obtaining a copy
6+
> of this software and associated documentation files (the "Software"), to deal
7+
> in the Software without restriction, including without limitation the rights
8+
> to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
> copies of the Software, and to permit persons to whom the Software is
10+
> furnished to do so, subject to the following conditions:
11+
>
12+
> The above copyright notice and this permission notice shall be included in all
13+
> copies or substantial portions of the Software.
14+
>
15+
> THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
> IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
> FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
> AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
> LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
> OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
> SOFTWARE.
22+
>

src/Pidfile/Pidfile.jl

Lines changed: 252 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,252 @@
1+
2+
# This is a fork of vtjnash/Pidfile.jl
3+
# This is strictly internal and there is no guarantee about its existence across any julia version
4+
5+
__precompile__()
6+
module PidFiles
7+
8+
9+
export mkpidlock
10+
11+
using Base:
12+
IOError, UV_EEXIST, UV_ESRCH,
13+
Process
14+
15+
using Base.Filesystem:
16+
File, open, JL_O_CREAT, JL_O_RDWR, JL_O_RDONLY, JL_O_EXCL,
17+
samefile
18+
19+
using FileWatching: watch_file
20+
using Base.Sys: iswindows
21+
22+
"""
23+
mkpidlock([f::Function], at::String, [pid::Cint, proc::Process]; kwopts...)
24+
25+
Create a pidfile lock for the path "at" for the current process
26+
or the process identified by pid or proc. Can take a function to execute once locked,
27+
for usage in `do` blocks, after which the lock will be automatically closed. If the lock fails
28+
and `wait` is false, then an error is thrown.
29+
30+
Optional keyword arguments:
31+
- `mode`: file access mode (modified by the process umask). Defaults to world-readable.
32+
- `poll_interval`: Specify the maximum time to between attempts (if `watch_file` doesn't work)
33+
- `stale_age`: Delete an existing pidfile (ignoring the lock) if its mtime is older than this.
34+
The file won't be deleted until 25x longer than this if the pid in the file appears that it may be valid.
35+
By default this is disabled (`stale_age` = 0), but a typical recommended value would be about 3-5x an
36+
estimated normal completion time.
37+
- `wait`: If true, block until we get the lock, if false, raise error if lock fails.
38+
"""
39+
function mkpidlock end
40+
41+
mutable struct LockMonitor
42+
path::String
43+
fd::File
44+
45+
global function mkpidlock(at::String, pid::Cint; kwopts...)
46+
local lock
47+
at = abspath(at)
48+
fd = open_exclusive(at; kwopts...)
49+
try
50+
write_pidfile(fd, pid)
51+
lock = new(at, fd)
52+
finalizer(close, lock)
53+
catch ex
54+
close(fd)
55+
rm(at)
56+
rethrow(ex)
57+
end
58+
return lock
59+
end
60+
end
61+
62+
mkpidlock(at::String; kwopts...) = mkpidlock(at, getpid(); kwopts...)
63+
mkpidlock(f::Function, at::String; kwopts...) = mkpidlock(f, at, getpid(); kwopts...)
64+
65+
function mkpidlock(f::Function, at::String, pid::Cint; kwopts...)
66+
lock = mkpidlock(at, pid; kwopts...)
67+
try
68+
return f()
69+
finally
70+
close(lock)
71+
end
72+
end
73+
74+
# TODO: enable this when we update libuv
75+
#Base.getpid(proc::Process) = ccall(:uv_process_get_pid, Cint, (Ptr{Void},), proc.handle)
76+
#function mkpidlock(at::String, proc::Process; kwopts...)
77+
# lock = mkpidlock(at, getpid(proc))
78+
# @schedule begin
79+
# wait(proc)
80+
# close(lock)
81+
# end
82+
# return lock
83+
#end
84+
85+
86+
"""
87+
write_pidfile(io, pid)
88+
89+
Write our pidfile format to an open IO descriptor.
90+
"""
91+
function write_pidfile(io::IO, pid::Cint)
92+
print(io, "$pid $(gethostname())")
93+
end
94+
95+
"""
96+
parse_pidfile(file::Union{IO, String}) => (pid, hostname, age)
97+
98+
Attempt to parse our pidfile format,
99+
replaced an element with (0, "", 0.0), respectively, for any read that failed.
100+
"""
101+
function parse_pidfile(io::IO)
102+
fields = split(read(io, String), ' ', limit = 2)
103+
pid = tryparse(Cuint, fields[1])
104+
pid === nothing && (pid = Cuint(0))
105+
hostname = (length(fields) == 2) ? fields[2] : ""
106+
when = mtime(io)
107+
age = time() - when
108+
return (pid, hostname, age)
109+
end
110+
111+
function parse_pidfile(path::String)
112+
try
113+
existing = open(path, JL_O_RDONLY)
114+
try
115+
return parse_pidfile(existing)
116+
finally
117+
close(existing)
118+
end
119+
catch ex
120+
isa(ex, EOFError) || isa(ex, IOError) || rethrow(ex)
121+
return (Cuint(0), "", 0.0)
122+
end
123+
end
124+
125+
"""
126+
isvalidpid(hostname::String, pid::Cuint) :: Bool
127+
128+
Attempt to conservatively estimate whether pid is a valid process id.
129+
"""
130+
function isvalidpid(hostname::AbstractString, pid::Cuint)
131+
# can't inspect remote hosts
132+
(hostname == "" || hostname == gethostname()) || return true
133+
# pid < 0 is never valid (must be a parser error or different OS),
134+
# and would have a completely different meaning when passed to kill
135+
!iswindows() && pid > typemax(Cint) && return false
136+
# (similarly for pid 0)
137+
pid == 0 && return false
138+
# see if the process id exists by querying kill without sending a signal
139+
# and checking if it returned ESRCH (no such process)
140+
return ccall(:uv_kill, Cint, (Cuint, Cint), pid, 0) != UV_ESRCH
141+
end
142+
143+
"""
144+
stale_pidfile(path::String, stale_age::Real) :: Bool
145+
146+
Helper function for `open_exclusive` for deciding if a pidfile is stale.
147+
"""
148+
function stale_pidfile(path::String, stale_age::Real)
149+
pid, hostname, age = parse_pidfile(path)
150+
age < -stale_age && @warn "filesystem time skew detected" path=path
151+
if age > stale_age
152+
if (age > stale_age * 25) || !isvalidpid(hostname, pid)
153+
return true
154+
end
155+
end
156+
return false
157+
end
158+
159+
"""
160+
tryopen_exclusive(path::String, mode::Integer = 0o444) :: Union{Void, File}
161+
162+
Try to create a new file for read-write advisory-exclusive access,
163+
return nothing if it already exists.
164+
"""
165+
function tryopen_exclusive(path::String, mode::Integer = 0o444)
166+
try
167+
return open(path, JL_O_RDWR | JL_O_CREAT | JL_O_EXCL, mode)
168+
catch ex
169+
(isa(ex, IOError) && ex.code == UV_EEXIST) || rethrow(ex)
170+
end
171+
return nothing
172+
end
173+
174+
"""
175+
open_exclusive(path::String; mode, poll_interval, stale_age) :: File
176+
177+
Create a new a file for read-write advisory-exclusive access.
178+
If `wait` is `false` then error out if the lock files exist
179+
otherwise block until we get the lock.
180+
181+
For a description of the keyword arguments, see [`mkpidlock`](@ref).
182+
"""
183+
function open_exclusive(path::String;
184+
mode::Integer = 0o444 #= read-only =#,
185+
poll_interval::Real = 10 #= seconds =#,
186+
wait::Bool = true #= return on failure if false =#,
187+
stale_age::Real = 0 #= disabled =#)
188+
# fast-path: just try to open it
189+
file = tryopen_exclusive(path, mode)
190+
file === nothing || return file
191+
if !wait
192+
if file === nothing && stale_age > 0
193+
if stale_age > 0 && stale_pidfile(path, stale_age)
194+
@warn "attempting to remove probably stale pidfile" path=path
195+
try
196+
rm(path)
197+
catch ex
198+
isa(ex, IOError) || rethrow(ex)
199+
end
200+
end
201+
file = tryopen_exclusive(path, mode)
202+
end
203+
if file === nothing
204+
error("Failed to get pidfile lock for $(repr(path)).")
205+
else
206+
return file
207+
end
208+
end
209+
# fall-back: wait for the lock
210+
211+
while true
212+
# start the file-watcher prior to checking for the pidfile existence
213+
t = @async try
214+
watch_file(path, poll_interval)
215+
catch ex
216+
isa(ex, IOError) || rethrow(ex)
217+
sleep(poll_interval) # if the watch failed, convert to just doing a sleep
218+
end
219+
# now try again to create it
220+
file = tryopen_exclusive(path, mode)
221+
file === nothing || return file
222+
Base.wait(t) # sleep for a bit before trying again
223+
if stale_age > 0 && stale_pidfile(path, stale_age)
224+
# if the file seems stale, try to remove it before attempting again
225+
# set stale_age to zero so we won't attempt again, even if the attempt fails
226+
stale_age -= stale_age
227+
@warn "attempting to remove probably stale pidfile" path=path
228+
try
229+
rm(path)
230+
catch ex
231+
isa(ex, IOError) || rethrow(ex)
232+
end
233+
end
234+
end
235+
end
236+
237+
"""
238+
close(lock::LockMonitor)
239+
240+
Release a pidfile lock.
241+
"""
242+
function Base.close(lock::LockMonitor)
243+
isopen(lock.fd) || return false
244+
havelock = samefile(stat(lock.fd), stat(lock.path))
245+
close(lock.fd)
246+
if havelock # try not to delete someone else's lock
247+
rm(lock.path)
248+
end
249+
return havelock
250+
end
251+
252+
end # module

src/Pkg.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ can_fancyprint(io::IO) = (io isa Base.TTY) && (get(ENV, "CI", nothing) != "true"
4343

4444
include("../ext/LazilyInitializedFields/LazilyInitializedFields.jl")
4545

46+
include("Pidfile/Pidfile.jl")
4647
include("utils.jl")
4748
include("MiniProgressBars.jl")
4849
include("GitTools.jl")

0 commit comments

Comments
 (0)