From: Zach Beane on
I need to work with a fresh, unique file that is deleted automatically
when I'm done with it. Here are the functions I came up with, I'd love
to hear any feedback.

(defparameter *alphabet*
(concatenate 'string
"abcdefghijklmnopqrstuvwxyz"
"0123456789"
"ABCDEFGHIJKLMNOPQRSTUVWXYZ"))

(defun random-string (length)
"Return a random string with LENGTH characters."
(let ((string (make-string length)))
(map-into string (lambda (char)
(declare (ignore char))
(aref *alphabet* (random (length *alphabet*))))
string)))

(defun call-with-temporary-open-file (template fun &rest open-args
&key element-type external-format)
"Call FUN with two arguments: an open output stream and a file
name. When it returns, the file is deleted. TEMPLATE should be a
pathname that can be used as a basis for the temporary file's
location."
(declare (ignorable element-type external-format))
(flet ((new-name ()
(make-pathname :name (concatenate 'string
(pathname-name template)
"-"
(random-string 8))
:defaults template)))
(let (try stream)
(tagbody
:retry
(setf try (new-name))
(unwind-protect
(progn
(setf stream (apply #'open try
:if-exists nil
:direction :output
open-args))
(unless stream
(go :retry))
(funcall fun stream try))
(when stream
(close stream)
(ignore-errors (delete-file try))))))))

;Zach
From: Madhu on

* Zach Beane <87r5orycwq.fsf(a)hangup.portland.xach.com> :
Wrote on Thu, 11 Feb 2010 11:07:01 -0500:

| I need to work with a fresh, unique file that is deleted automatically
| when I'm done with it. Here are the functions I came up with, I'd
| love to hear any feedback.
|
| (defun call-with-temporary-open-file (template fun &rest open-args
| &key element-type external-format)
| "Call FUN with two arguments: an open output stream and a file
| name. When it returns, the file is deleted. TEMPLATE should be a
| pathname that can be used as a basis for the temporary file's
| location."
| (declare (ignorable element-type external-format))
| (flet ((new-name ()
| (make-pathname :name (concatenate 'string
| (pathname-name template)
| "-"
| (random-string 8))
| :defaults template)))
| (let (try stream)
| (tagbody
| :retry
| (setf try (new-name))
| (unwind-protect
| (progn
| (setf stream (apply #'open try
| :if-exists nil
| :direction :output
| open-args))
| (unless stream
| (go :retry))
| (funcall fun stream try))
| (when stream
| (close stream)
| (ignore-errors (delete-file try))))))))


I'd prefer a slightly different level of abstraction - Split the job
into two parts 1) the creation of a unique temporary file and 2) its use
by the programmer via standard WITH-OPEN-FILE/OPEN macros. This would
punt the complexity introduced in massaging OPEN-ARGS, etc., to `where
it belongs'

The idiom I'd use is

(let (file)
(unwind-protect (with-open-file (stream (setq file (HCL:MAKE-TEMP-FILE))
:direction :io ...)
;; use stream
)
(when file (delete-file file))))


(On Lispworks, There is an [undocumented?] function HCL:MAKE-TEMP-FILE)

--
Madhu
From: Zach Beane on
Madhu <enometh(a)meer.net> writes:

> I'd prefer a slightly different level of abstraction - Split the job
> into two parts 1) the creation of a unique temporary file and 2) its use
> by the programmer via standard WITH-OPEN-FILE/OPEN macros. This would
> punt the complexity introduced in massaging OPEN-ARGS, etc., to `where
> it belongs'
>
> The idiom I'd use is
>
> (let (file)
> (unwind-protect (with-open-file (stream (setq file (HCL:MAKE-TEMP-FILE))
> :direction :io ...)
> ;; use stream
> )
> (when file (delete-file file))))

It seems like this approach suffers from a race condition; I aimed to
avoid that. With SBCL on Unix, :if-exists :error and :if-exists nil both
eventually use the O_EXCL flag to open(), which guarantees (modulo NFS
and the like) the stream is opened on a fresh file.

I'd like to know if hcl:make-temp-file makes that a non-issue, somehow.

Zach
From: Madhu on

* Zach Beane <87k4ujy8fu.fsf(a)hangup.portland.xach.com> :
Wrote on Thu, 11 Feb 2010 12:43:33 -0500:

| Madhu <enometh(a)meer.net> writes:
|
|> I'd prefer a slightly different level of abstraction - Split the job
|> into two parts 1) the creation of a unique temporary file and 2) its
|> use by the programmer via standard WITH-OPEN-FILE/OPEN macros. This
|> would punt the complexity introduced in massaging OPEN-ARGS, etc., to
|> `where it belongs'
|>
|> The idiom I'd use is
|>
|> (let (file)
|> (unwind-protect (with-open-file (stream (setq file (HCL:MAKE-TEMP-FILE))
|> :direction :io ...)
|> ;; use stream
|> )
|> (when file (delete-file file))))
|
| It seems like this approach suffers from a race condition; I aimed to
| avoid that. With SBCL on Unix, :if-exists :error and :if-exists nil both
| eventually use the O_EXCL flag to open(), which guarantees (modulo NFS
| and the like) the stream is opened on a fresh file.

I'm not sure I understand what race contition you are seeing here,
because the call to open(2) should NOT use the O_EXCL flag (i.e. create
a fresh file) if you pass :IF-DOES-NOT-EXIST :ERROR, If it did that, it
would be a bug in the implementation IMO.

Note in the sketch above that the call to WITH-OPEN-FILE could or rather
SHOULD be called with :IF-DOES-NOT-EXIST :ERROR, and the assumption is
that WITH-OPEN-FILE opens an existing file which the MAKE-TEMP-FILE
created.

Only we (the callers of MAKE-TEMP-FILE) are responsible for deleting the
created file, and no other file will be created with that name until we
have deleted it.

| I'd like to know if hcl:make-temp-file makes that a non-issue, somehow.

If you split the job into the 2 pieces, MAKE-TEMP-FILE just needs to
call creat(2), and it can possibly do this without going through CL:OPEN

--
Madhu
From: Zach Beane on
Madhu <enometh(a)meer.net> writes:

> |> (let (file)
> |> (unwind-protect (with-open-file (stream (setq file (HCL:MAKE-TEMP-FILE))
> |> :direction :io ...)
> |> ;; use stream
> |> )
> |> (when file (delete-file file))))
> |
> | It seems like this approach suffers from a race condition; I aimed to
> | avoid that. With SBCL on Unix, :if-exists :error and :if-exists nil both
> | eventually use the O_EXCL flag to open(), which guarantees (modulo NFS
> | and the like) the stream is opened on a fresh file.
>
> I'm not sure I understand what race contition you are seeing here,

I'm referring to
http://www.dwheeler.com/secure-programs/Secure-Programs-HOWTO/avoid-race.html
which says:

Once you create the file atomically, you must alway use the returned
file descriptor (or file stream, if created from the file descriptor
using routines like fdopen()). You must never re-open the file, or use
any operations that use the filename as a parameter - always use the
file descriptor or associated stream. Otherwise, the tmpwatch race
issues noted above will cause problems.

> because the call to open(2) should NOT use the O_EXCL flag (i.e. create
> a fresh file) if you pass :IF-DOES-NOT-EXIST :ERROR, If it did that, it
> would be a bug in the implementation IMO.

Right, when I mentioned O_EXCL, I was referring to my version, which
uses ":if-exists nil", not :if-does-not-exist <anything>.

> If you split the job into the 2 pieces, MAKE-TEMP-FILE just needs to
> call creat(2), and it can possibly do this without going through CL:OPEN

Is there a CL operation that corresponds closely to creat(2) like
CL:OPEN with :if-exists :error-or-nil corresponds closely to open(2)
with O_EXCL?

Zach