You probably stumbled upon this issue on Windows platforms: files created by fopen()
are by default locked for deletion or renaming.
Ie. if you do this:
FILE *my_file = fopen("foo.bar", "wb");
...
then the foo.bar
file will be locked until the process closes the file (or dies). No renaming. No deleting.
This default feature is rather different from what we have on POSIX-like systems, and on Linux especially, where any file can basically be renamed or deleted while being opened. Once a file is being opened, its directory reference has basically no influence, except the implicit reference count (ie. deleting the file will only decrement the refcount, and closing the last handle will remove the underlying inodes)
This is actually a cool feature: you can have a file opened, and immediately unlinked, to have an anonymous file where you may execute read/write operations without bothering handling the cleanup if the process is killed, for example. You can also create a log file, redirect stdout/stderr, and later want to rotate the file, without being hit by permission denied
errors. This is especially true when all references to the file haven’t been closed yet - say, if you have stdio redirected to a log file, and if you have a child process spawned and logging in the same log file.
On, Windows, the infamous ERROR_ACCESS_DENIED
will kick you in the face as soon as you try to do the same.
There is actually a quite simple solution if you want to get a fopen()
semantic which is closer to the POSIX one: use the low-level CreateFile
function with the FILE_SHARE_DELETE
sharing mode.
The documentation says:
- FILE_SHARE_DELETE Enables subsequent open operations on a file or device to request delete access. Otherwise, other processes cannot open the file or device if they request delete access. If this flag is not specified, but the file or device has been opened for delete access, the function fails. Note Delete access allows both delete and rename operations.
Hey, that’s exactly what we are looking for! But we want a nice FILE
pointer, not an horrible HANDLE
object. Is it possible ?
Yes!:
-
the
HANDLE
object can be attached to a low-level C library handle through_open_osfhandle()
-
the low-level C library handle can be attached to a
FILE*
object through_fdopen()
You just have to take some care about the passed fopen()
flags to open the file with proper flags, and your “compatible” function should be something like:
#include <stdio.h>
#include <fcntl.h>
#include <io.h>
#include <windows.h>
/**
* Opens the file whose name is the string pointed to by file and associates
* a stream with it.
**/
static FILE* fopen_unixlike(const char *file, const char *mode) {
DWORD dwDesiredAccess = 0;
DWORD dwCreationDisposition = 0;
const DWORD dwFlagsAndAttributes = FILE_ATTRIBUTE_NORMAL;
const DWORD dwShareMode = FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE;
size_t i;
HANDLE handle;
int fd;
/* Infer flags. */
for(i = 0; mode[i] != '\0'; i++) {
switch(mode[i]) {
case 'r':
dwDesiredAccess |= GENERIC_READ;
dwCreationDisposition = OPEN_EXISTING;
break;
case 'a':
dwDesiredAccess |= GENERIC_WRITE;
dwCreationDisposition = CREATE_NEW;
break;
case 'w':
dwDesiredAccess |= GENERIC_WRITE;
dwCreationDisposition = CREATE_ALWAYS;
break;
case '+':
dwDesiredAccess |= GENERIC_READ;
dwDesiredAccess |= GENERIC_WRITE;
break;
}
}
/* Create the file. */
handle = CreateFileA(file,
dwDesiredAccess,
dwShareMode,
NULL,
dwCreationDisposition,
dwFlagsAndAttributes,
NULL);
if (handle == INVALID_HANDLE_VALUE) {
return NULL;
}
/* Associates a C run-time file descriptor with a file HANDLE. */
fd = _open_osfhandle((intptr_t) handle, _O_BINARY);
if (fd == -1) {
CloseHandle(handle);
return NULL;
}
/* Associates a stream with a C run-time file descriptor. */
return _fdopen(fd, mode);
}
You can test this function using the example below:
/* Comment to test. */
#define fopen fopen_unixlike
int main(int argc, char* argv[]) {
FILE *fp = fopen("log.txt", "wb");
int i;
for(i = 0; i < 10; i++) {
if (fprintf(fp, "this is the log line #%d\r\n", i) > 0
&& fflush(fp) == 0) {
printf("successfully written line #%d, press a key\n", i);
_fgetchar();
} else {
fprintf(stderr, "error writting line #%d, press a key\n", i);
_fgetchar();
break;
}
}
return 0;
}
The “log.txt” file will be deletable and movable without troubles, and you may even successfully write to the deleted file.
TL;DR: I wish I had known sooner. Darn.