xavier roche's homework

random thoughts and sometimes pieces of code

Creating Deletable and Movable Files on Windows

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:

1
2
  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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
#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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/* 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.

Comments