Jaco Kroon
Jaco Kroon

Reputation: 71

Given that I've got a file open, is there a way to determine if other processes also has the file open

I've got a file descriptor (obtained using open(2)). At some point in time I can determine that the file has been unlinked (fstat(2), check st_nlinks). However, before I can close my file descriptor I would like to verify that no other process still has the file open (for writing at least).

inotify can (and does) give me some nice events to work from, eg, IN_ATTRIB when st_nlinks change, and IN_CLOSE_WRITE when another process closes the file (and had it open for writing, this however does not mean NO ONE still has it open for writing).

Basically I'm tracking a log file, and need to make sure that I get all info that has been written, even after renames, but once the file has been renamed and/or removed, and all writers have closed, there is no further point in me keeping my file descriptor open.

(Avoiding the races w.r.t. opening the file and making sure that the inotify descriptor references the same file is outside of the scope of this question.)

Upvotes: 3

Views: 226

Answers (1)

Jaco Kroon
Jaco Kroon

Reputation: 71

Not foolproof, but if you can track renames using inotify should be "good enough", obviously a few race conditions (as per standard when dealing with anything inotify). Improving on this requires a stat() on every non-deleted file referenced from /proc and doesn't viably improve the solution in my opinion.

First, step, given a pid and fd (as strings because that's the way we get them from readdir(3), get the mode it's opened for:

static
int getflags(const char* pidn, const char* fdn)
{
    char fnamebfr[525];
    char bfr[1 << 13];
    sprintf(fnamebfr, "/proc/%s/fdinfo/%s", pidn, fdn);
    int fd = open(fnamebfr, O_RDONLY);
    if (fd < 0) {
        if (errno != ENOENT)
            perror(fnamebfr);
        return 0;
    }
    int r = read(fd, bfr, sizeof(bfr));
    if (r < 0) {
        perror(fnamebfr);
        r = 0;
    } else if (r == sizeof(bfr)) {
        r = 0;
        fprintf(stderr, "bfr in %s is too small.\n", __FUNCTION__);
    } else {
        bfr[r] = 0;
        r = 0;
        char *fb = strstr(bfr, flagsstr);
        if (!fb) {
            fprintf(stderr, "Error locating '%s' in %s, content:\n%s\n", flagsstr, fnamebfr, bfr);
        } else {
            char *nptr;
            fb += strlen(flagsstr);
            r = strtol(fb, &nptr, 8);
            if (*nptr != '\n')
                fprintf(stderr, "Parse warning for strtol, endp=\n%s\nbfr=%s\n", nptr, bfr);
        }
    }

    close(fd);

    return r;
}

Note: If the file got closed inbetween walking /proc//fd/ we will return 0 here (O_RDONLY), if the file descriptor got re-used ... then we return the completely wrong flags.

Now we can walk /proc/ looking for O_WRONLY or O_RDWR in those flags:

static int fileopenforwrite(const char* path)
{
    size_t tlen = strlen(path);
    DIR *proc, *fd;
    struct dirent *procent, *fdent;
    int _fd;
    proc = opendir("/proc");

    if (!proc) {
        perror("/proc");
        return -1;
    }

    while ((procent = readdir(proc))) {
        char *endptr;
        char fdpath[MAXNAMLEN + 10];

        strtol(procent->d_name, &endptr, 10);
        if (*endptr)
            continue; /* not a pid */
        sprintf(fdpath, "/proc/%s/fd", procent->d_name);
        _fd = open(fdpath, O_RDONLY);
        if (_fd < 0) {
            if (errno != ENOENT) /* process terminated + waited */
                perror(fdpath);
            fd = fdopendir(_fd);
            if (!fd) {
                perror(fdpath);
                close(_fd);
                continue;
            }
            while ((fdent = readdir(fd))) {
                if (fdent->d_type == DT_DIR)
                    continue; /* skip . and .. */
                char readbuf[MAXNAMLEN + 11];
                ssize_t r = readlinkat(_fd, fdent->d_name, readbuf, sizeof(readbuf));
                if (r < 0) {
                    perror(fdpath);
                } else if (r >= (int)sizeof(readbuf)) {
                    fprintf(stderr, "Bufferoverflow in %s (%s).", __FUNCTION__, __FILE__);
                } else {
                    readbuf[r] = 0;
                    if (strncmp(path, readbuf, tlen) == 0 &&
                            (!readbuf[tlen] || strcmp(" (deleted)", readbuf + tlen) == 0))
                    {
                        /* We have an FD to the file, now we want to know if it's
                         * open for writing */
                        int f = getflags(procent->d_name, fdent->d_name);
                        if (f & (O_RDONLY | O_RDWR)) {
                            closedir(fd);
                            closedir(proc);
                            return 0;
                        }
                    }
                }
            }
            closedir(fd); /* implicitly closes _fd */
        }
    }
    closedir(proc);

    return 1;
}

This will return 0 on yes, 1 on false, -1 on unable to open /proc.

If you use this, obviously clean it up to your requirements.

Upvotes: 1

Related Questions