change locking of preferences files under Windows
to avoid the problem that `rename-file-or-directory' cannot replace a file that is currently open for reading (unlike Unix)
This commit is contained in:
parent
5998ecc564
commit
3e2af2f39e
|
@ -7,6 +7,8 @@
|
||||||
|
|
||||||
get-preference
|
get-preference
|
||||||
put-preferences
|
put-preferences
|
||||||
|
preferences-lock-file-mode
|
||||||
|
make-handle-get-preference-locked
|
||||||
|
|
||||||
fold-files
|
fold-files
|
||||||
find-files
|
find-files
|
||||||
|
@ -139,19 +141,93 @@
|
||||||
(unless (eq? table (weak-box-value pref-cache))
|
(unless (eq? table (weak-box-value pref-cache))
|
||||||
(set! pref-cache (make-weak-box table)))))
|
(set! pref-cache (make-weak-box table)))))
|
||||||
|
|
||||||
(define (get-prefs flush-mode filename)
|
|
||||||
|
(define make-lock-file-name
|
||||||
|
(case-lambda
|
||||||
|
[(path) (let-values ([(dir name dir?) (split-path path)])
|
||||||
|
(make-lock-file-name dir name))]
|
||||||
|
[(dir name)
|
||||||
|
(build-path dir
|
||||||
|
(bytes->path-element
|
||||||
|
(bytes-append
|
||||||
|
(if (eq? 'windows (system-type))
|
||||||
|
#"_"
|
||||||
|
#".")
|
||||||
|
#"LOCK"
|
||||||
|
(path-element->bytes name))))]))
|
||||||
|
|
||||||
|
(define (preferences-lock-file-mode)
|
||||||
|
(case (system-type)
|
||||||
|
[(windows) 'file-lock]
|
||||||
|
[else 'exists]))
|
||||||
|
|
||||||
|
(define (call-with-file-lock who kind get-lock-file thunk lock-there)
|
||||||
|
(case (preferences-lock-file-mode)
|
||||||
|
[(file-lock)
|
||||||
|
(let ([lock-file (get-lock-file)])
|
||||||
|
;; Create the lock file if it doesn't exist:
|
||||||
|
(unless (file-exists? lock-file)
|
||||||
|
(with-handlers ([exn:fail:filesystem:exists? (lambda (exn) 'ok)])
|
||||||
|
(close-output-port (open-output-file lock-file #:exists 'error))))
|
||||||
|
((call-with-input-file*
|
||||||
|
lock-file
|
||||||
|
(lambda (p)
|
||||||
|
(if (port-try-file-lock? p kind)
|
||||||
|
;; got lock:
|
||||||
|
(let ([v (dynamic-wind
|
||||||
|
void
|
||||||
|
thunk
|
||||||
|
(lambda ()
|
||||||
|
(port-file-unlock p)))])
|
||||||
|
(lambda () v))
|
||||||
|
;; didn't get lock:
|
||||||
|
(if lock-there
|
||||||
|
(lambda () (lock-there lock-file))
|
||||||
|
(error who
|
||||||
|
"~a ~a: ~e"
|
||||||
|
"some other process has a lock"
|
||||||
|
"on the preferences lock file"
|
||||||
|
lock-file)))))))]
|
||||||
|
[else ; = 'exists
|
||||||
|
;; Only a write lock is needed, and the file lock
|
||||||
|
;; is implemented by the presence of the file:
|
||||||
|
(case kind
|
||||||
|
[(shared) (thunk)]
|
||||||
|
[(exclusive)
|
||||||
|
(let ([lock-file (get-lock-file)])
|
||||||
|
(with-handlers ([exn:fail:filesystem:exists?
|
||||||
|
(lambda (x)
|
||||||
|
(if lock-there
|
||||||
|
(lock-there lock-file)
|
||||||
|
(error who
|
||||||
|
"~a, ~a: ~e"
|
||||||
|
"some other process has the preference-file lock"
|
||||||
|
"as indicated by the existence of the lock file"
|
||||||
|
lock-file)))])
|
||||||
|
;; Grab lock:
|
||||||
|
(close-output-port (open-output-file lock-file #:exists 'error)))
|
||||||
|
(dynamic-wind
|
||||||
|
void
|
||||||
|
thunk
|
||||||
|
(lambda ()
|
||||||
|
;; Release lock:
|
||||||
|
(delete-file lock-file))))])]))
|
||||||
|
|
||||||
|
(define (get-prefs flush-mode filename use-lock? lock-there)
|
||||||
(define (read-prefs default-pref-file)
|
(define (read-prefs default-pref-file)
|
||||||
(with-handlers ([exn:fail:filesystem? (lambda (x) null)])
|
(with-handlers ([exn:fail:filesystem? (lambda (x) null)])
|
||||||
(let* ([pref-file
|
(let-values ([(pref-file use-lock?)
|
||||||
(or filename
|
(if filename
|
||||||
|
(values filename use-lock?)
|
||||||
(let ([f default-pref-file])
|
(let ([f default-pref-file])
|
||||||
(if (file-exists? f)
|
(if (file-exists? f)
|
||||||
;; Using `file-exists?' means there's technically a
|
;; Using `file-exists?' means there's technically a
|
||||||
;; race condition, but something has gone really wrong
|
;; race condition, but something has gone really wrong
|
||||||
;; if the file disappears.
|
;; if the file disappears.
|
||||||
f
|
(values f use-lock?)
|
||||||
;; Look for old PLT Scheme pref file:
|
;; Look for old PLT Scheme pref file:
|
||||||
(let ([alt-f (case (system-type)
|
(let ([alt-f
|
||||||
|
(case (system-type)
|
||||||
[(windows)
|
[(windows)
|
||||||
(build-path (find-system-path 'pref-dir)
|
(build-path (find-system-path 'pref-dir)
|
||||||
'up "PLT Scheme" "plt-prefs.ss")]
|
'up "PLT Scheme" "plt-prefs.ss")]
|
||||||
|
@ -161,22 +237,33 @@
|
||||||
[(unix)
|
[(unix)
|
||||||
(expand-user-path "~/.plt-scheme/plt-prefs.ss")])])
|
(expand-user-path "~/.plt-scheme/plt-prefs.ss")])])
|
||||||
(if (file-exists? alt-f)
|
(if (file-exists? alt-f)
|
||||||
alt-f
|
(values alt-f #f)
|
||||||
;; Last chance: check for a "defaults" collection:
|
;; Last chance: check for a "defaults" collection:
|
||||||
;; (error here in case there's no "defaults"
|
;; (error here in case there's no "defaults"
|
||||||
;; bails out through above `with-handlers')
|
;; bails out through above `with-handlers')
|
||||||
|
(values
|
||||||
(collection-file-path "racket-prefs.rktd"
|
(collection-file-path "racket-prefs.rktd"
|
||||||
"defaults"))))))]
|
"defaults")
|
||||||
[prefs (with-pref-params
|
#f))))))])
|
||||||
|
(let ([prefs (with-pref-params
|
||||||
(lambda ()
|
(lambda ()
|
||||||
(with-input-from-file pref-file read)))])
|
(if use-lock?
|
||||||
|
(call-with-file-lock
|
||||||
|
'get-preferences
|
||||||
|
'shared
|
||||||
|
(lambda ()
|
||||||
|
(make-lock-file-name pref-file))
|
||||||
|
(lambda ()
|
||||||
|
(with-input-from-file pref-file read))
|
||||||
|
lock-there)
|
||||||
|
(with-input-from-file pref-file read))))])
|
||||||
;; Make sure file content had the right shape:
|
;; Make sure file content had the right shape:
|
||||||
(if (and (list? prefs)
|
(if (and (list? prefs)
|
||||||
(andmap (lambda (x)
|
(andmap (lambda (x)
|
||||||
(and (pair? x) (pair? (cdr x)) (null? (cddr x))))
|
(and (pair? x) (pair? (cdr x)) (null? (cddr x))))
|
||||||
prefs))
|
prefs))
|
||||||
prefs
|
prefs
|
||||||
null))))
|
null)))))
|
||||||
(let* ([fn (path->complete-path
|
(let* ([fn (path->complete-path
|
||||||
(or filename
|
(or filename
|
||||||
(find-system-path 'pref-file)))]
|
(find-system-path 'pref-file)))]
|
||||||
|
@ -193,17 +280,49 @@
|
||||||
(pref-cache-install! fn fn f)
|
(pref-cache-install! fn fn f)
|
||||||
f))))
|
f))))
|
||||||
|
|
||||||
|
(define (make-handle-get-preference-locked delay
|
||||||
|
name
|
||||||
|
[fail-thunk (lambda () #f)]
|
||||||
|
[refresh-cache? 'timestamp]
|
||||||
|
[filename #f]
|
||||||
|
#:lock-there [lock-there #f]
|
||||||
|
#:max-delay [max-delay 0.2])
|
||||||
|
(lambda (lock-filename)
|
||||||
|
(sleep delay)
|
||||||
|
(get-preference name fail-thunk refresh-cache? filename
|
||||||
|
#:lock-there (let ([new-delay (* 2 delay)])
|
||||||
|
(if (new-delay . < . max-delay)
|
||||||
|
(make-handle-get-preference-locked
|
||||||
|
new-delay
|
||||||
|
name fail-thunk refresh-cache? filename
|
||||||
|
#:lock-there lock-there
|
||||||
|
#:max-delay max-delay)
|
||||||
|
lock-there)))))
|
||||||
|
|
||||||
(define (get-preference name [fail-thunk (lambda () #f)]
|
(define (get-preference name [fail-thunk (lambda () #f)]
|
||||||
[refresh-cache? 'timestamp]
|
[refresh-cache? 'timestamp]
|
||||||
[filename #f])
|
[filename #f]
|
||||||
|
#:lock-there [lock-there
|
||||||
|
(make-handle-get-preference-locked
|
||||||
|
0.01
|
||||||
|
name
|
||||||
|
fail-thunk
|
||||||
|
refresh-cache?
|
||||||
|
filename)]
|
||||||
|
#:use-lock? [use-lock? #t])
|
||||||
(unless (symbol? name)
|
(unless (symbol? name)
|
||||||
(raise-type-error 'get-preference "symbol" name))
|
(raise-type-error 'get-preference "symbol" name))
|
||||||
(unless (and (procedure? fail-thunk)
|
(unless (and (procedure? fail-thunk)
|
||||||
(procedure-arity-includes? fail-thunk 0))
|
(procedure-arity-includes? fail-thunk 0))
|
||||||
(raise-type-error 'get-preference "procedure (arity 0)" fail-thunk))
|
(raise-type-error 'get-preference "procedure (arity 0)" fail-thunk))
|
||||||
(let ([f (get-prefs refresh-cache? filename)])
|
((let/ec esc
|
||||||
|
(let ([f (get-prefs refresh-cache? filename use-lock?
|
||||||
|
(and lock-there
|
||||||
|
(lambda (file)
|
||||||
|
(esc (lambda () (lock-there file))))))])
|
||||||
|
(lambda ()
|
||||||
(let ([m (assq name f)])
|
(let ([m (assq name f)])
|
||||||
(if m (cadr m) (fail-thunk)))))
|
(if m (cadr m) (fail-thunk))))))))
|
||||||
|
|
||||||
(define (put-preferences names vals [lock-there #f] [filename #f])
|
(define (put-preferences names vals [lock-there #f] [filename #f])
|
||||||
(unless (and (list? names) (andmap symbol? names))
|
(unless (and (list? names) (andmap symbol? names))
|
||||||
|
@ -226,28 +345,14 @@
|
||||||
(make-directory* dir))
|
(make-directory* dir))
|
||||||
(values
|
(values
|
||||||
filename
|
filename
|
||||||
(build-path dir
|
(make-lock-file-name dir name)
|
||||||
(bytes->path-element
|
|
||||||
(bytes-append
|
|
||||||
(if (eq? 'windows (system-type))
|
|
||||||
#"_"
|
|
||||||
#".")
|
|
||||||
#"LOCK"
|
|
||||||
(path-element->bytes name))))
|
|
||||||
dir))))])
|
dir))))])
|
||||||
(with-handlers ([exn:fail:filesystem:exists?
|
(call-with-file-lock
|
||||||
(lambda (x)
|
'put-preferences
|
||||||
(if lock-there
|
'exclusive
|
||||||
(lock-there lock-file)
|
(lambda () lock-file)
|
||||||
(error 'put-preferences
|
|
||||||
"some other process has the preference-file lock, as indicated by the existence of the lock file: ~e"
|
|
||||||
lock-file)))])
|
|
||||||
;; Grab lock:
|
|
||||||
(close-output-port (open-output-file lock-file #:exists 'error))
|
|
||||||
(dynamic-wind
|
|
||||||
void
|
|
||||||
(lambda ()
|
(lambda ()
|
||||||
(let ([f (get-prefs #t filename)])
|
(let ([f (get-prefs #t filename #f #f)])
|
||||||
(set! f (let loop ([f f][a null])
|
(set! f (let loop ([f f][a null])
|
||||||
(cond
|
(cond
|
||||||
[(null? f) (reverse
|
[(null? f) (reverse
|
||||||
|
@ -299,9 +404,7 @@
|
||||||
tmp-file
|
tmp-file
|
||||||
f)
|
f)
|
||||||
(rename-file-or-directory tmp-file pref-file #t)))))
|
(rename-file-or-directory tmp-file pref-file #t)))))
|
||||||
(lambda ()
|
lock-there)))
|
||||||
;; Release lock:
|
|
||||||
(delete-file lock-file))))))
|
|
||||||
|
|
||||||
;; fold-files : (pathname sym alpha -> alpha) alpha pathname/#f -> alpha
|
;; fold-files : (pathname sym alpha -> alpha) alpha pathname/#f -> alpha
|
||||||
(define (fold-files f init [path #f] [follow-links? #t])
|
(define (fold-files f init [path #f] [follow-links? #t])
|
||||||
|
|
|
@ -853,10 +853,20 @@ desired access and flags (probably using the @racket['truncate] flag;
|
||||||
see @racket[open-output-file]) and to delete it when it is no longer
|
see @racket[open-output-file]) and to delete it when it is no longer
|
||||||
needed.}
|
needed.}
|
||||||
|
|
||||||
|
|
||||||
@defproc[(get-preference [name symbol?]
|
@defproc[(get-preference [name symbol?]
|
||||||
[failure-thunk (-> any) (lambda () #f)]
|
[failure-thunk (-> any) (lambda () #f)]
|
||||||
[flush-mode any/c 'timestamp]
|
[flush-mode any/c 'timestamp]
|
||||||
[filename (or/c string-path? #f) #f])
|
[filename (or/c string-path? #f) #f]
|
||||||
|
[#:lock-there
|
||||||
|
lock-there
|
||||||
|
(or/c (path? . -> . any) #f)
|
||||||
|
(make-handle-get-preference-locked 0.01
|
||||||
|
name
|
||||||
|
fail-thunk
|
||||||
|
refresh-cache?
|
||||||
|
filename)]
|
||||||
|
[#:use-lock? use-lock? #t])
|
||||||
any]{
|
any]{
|
||||||
|
|
||||||
Extracts a preference value from the file designated by
|
Extracts a preference value from the file designated by
|
||||||
|
@ -887,6 +897,16 @@ then the cache is used only if the file has a timestamp that is the
|
||||||
same as the last time the file was read. Otherwise, the file is
|
same as the last time the file was read. Otherwise, the file is
|
||||||
re-consulted.
|
re-consulted.
|
||||||
|
|
||||||
|
Under platforms for which @racket[preferences-lock-file-mode] returns
|
||||||
|
@racket['file-lock] and when @racket[use-lock?] is true,
|
||||||
|
preference-file reading is guarded by a lock; multiple readers can
|
||||||
|
share the lock, but writers take the lock exclusively. If the
|
||||||
|
preferences file cannot be read because the lock is unavailable,
|
||||||
|
@racket[lock-there] is called on the path of the lock file; if
|
||||||
|
@racket[lock-there] is @racket[#f], an exception is raised. The
|
||||||
|
default @racket[lock-there] handler retries about 5 times (with
|
||||||
|
increasing delays between each attempt) before raising an exception.
|
||||||
|
|
||||||
See also @racket[put-preferences]. For a more elaborate preference
|
See also @racket[put-preferences]. For a more elaborate preference
|
||||||
system, see @racket[preferences:get].
|
system, see @racket[preferences:get].
|
||||||
|
|
||||||
|
@ -924,21 +944,66 @@ whose @racket[write] output is @racket[read]able (i.e., the
|
||||||
writing preferences).
|
writing preferences).
|
||||||
|
|
||||||
Current preference values are read from the preference file before
|
Current preference values are read from the preference file before
|
||||||
updating, and an update ``lock'' is held starting before the file
|
updating, and a write lock is held starting before the file
|
||||||
read, and lasting until after the preferences file is updated. The
|
read, and lasting until after the preferences file is updated. The
|
||||||
lock is implemented by the existence of a file in the same directory
|
lock is implemented by the existence of a file in the same directory
|
||||||
as the preference file. If the directory of the preferences file does
|
as the preference file; see @racket[preferences-lock-file-mode] for
|
||||||
|
more information. If the directory of the preferences file does
|
||||||
not already exist, it is created.
|
not already exist, it is created.
|
||||||
|
|
||||||
If the update lock is already held (i.e., the lock file exists), then
|
If the write lock is already held, then
|
||||||
@racket[locked-proc] is called with a single argument: the path of the lock
|
@racket[locked-proc] is called with a single argument: the path of the lock
|
||||||
file. The default @racket[locked-proc] reports an error; an alternative
|
file. The default @racket[locked-proc] reports an error; an alternative
|
||||||
thunk might wait a while and try again, or give the user the choice to
|
thunk might wait a while and try again, or give the user the choice to
|
||||||
delete the lock file (in case a previous update attempt encountered
|
delete the lock file (in case a previous update attempt encountered
|
||||||
disaster).
|
disaster and locks are implemented by the presence of the lock file).
|
||||||
|
|
||||||
If @racket[filename] is @racket[#f] or not supplied, and the
|
If @racket[filename] is @racket[#f] or not supplied, and the
|
||||||
preference file does not already exist, then values read from the
|
preference file does not already exist, then values read from the
|
||||||
@filepath{defaults} collection (if any) are written for preferences
|
@filepath{defaults} collection (if any) are written for preferences
|
||||||
that are not mentioned in @racket[names].}
|
that are not mentioned in @racket[names].}
|
||||||
|
|
||||||
|
|
||||||
|
@defproc[(preferences-lock-file-mode) (or/c 'exists 'file-lock)]{
|
||||||
|
|
||||||
|
Reports the way that the lock file is used to implement
|
||||||
|
preference-file locking on the current platform.
|
||||||
|
|
||||||
|
The @racket['exists] mode is currently used on all platforms except
|
||||||
|
Windows. In @racket['exists] mode, the existence of the lock file
|
||||||
|
indicates that a write lock is held, and readers need no lock (because
|
||||||
|
the preferences file is atomically updated via
|
||||||
|
@racket[rename-file-or-directory]).
|
||||||
|
|
||||||
|
The @racket['file-lock] mode is currently used under Windows. In
|
||||||
|
@racket['file-lock] mode, shared and exclusive locks (in the sense of
|
||||||
|
@racket[port-try-file-lock?]) on the lock file reflect reader and
|
||||||
|
writer locks on the preference-file content. (The preference file
|
||||||
|
itself is not locked, because a lock would interfere with replacing
|
||||||
|
the file via @racket[rename-file-or-directory].)}
|
||||||
|
|
||||||
|
|
||||||
|
@defproc[(make-handle-get-preference-locked
|
||||||
|
[delay real?]
|
||||||
|
[name symbol?]
|
||||||
|
[failure-thunk (-> any) (lambda () #f)]
|
||||||
|
[flush-mode any/c 'timestamp]
|
||||||
|
[filename (or/c string-path? #f) #f]
|
||||||
|
[#:lock-there lock-there (or/c (path? . -> . any) #f) #f]
|
||||||
|
[#:max-delay max-delay real? 0.2])
|
||||||
|
(path-string? . -> . any)]{
|
||||||
|
|
||||||
|
Creates a procedure suitable for use as the @racket[#:lock-there]
|
||||||
|
argument to @racket[get-preference], where the @racket[name],
|
||||||
|
@racket[failure-thunk], @racket[flush-mode], and @racket[filename]
|
||||||
|
are all passed on to @racket[get-preference] by the result procedure
|
||||||
|
to retry the preferences lookup.
|
||||||
|
|
||||||
|
Before calling @racket[get-preference], the result procedure uses
|
||||||
|
@racket[(sleep delay)] to pause. Then, if @racket[(* 2 delay)] is less
|
||||||
|
than @racket[max-delay], the result procedure calls
|
||||||
|
@racket[make-handle-get-preference-locked] to generate a new retry
|
||||||
|
procedure to pass to @racket[get-preference], but with a
|
||||||
|
@racket[delay] of @racket[(* 2 delay)]. If @racket[(* 2 delay)] is not
|
||||||
|
less than @racket[max-delay], then @racket[get-preference] is called
|
||||||
|
with the given @racket[lock-there], instead.}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user