Search code examples
clockingfile-descriptorlseek

File locking + Fscanf/Lseek


I have a file called "input.txt" that holds some values. I'm writing program that will find minimum value in that file, and replace that minimum with number given as command line argument - if that command line argument is bigger then minimum. These values represent room temperatures so that fact can be used in order to find minimum. In addition, that part of the file (where new number replaces minimum) needs to be locked.

Example:

$ ./prog 23

FILE: 21 25 19 22 24

FILE: 21 25 23 22 24

$ ./prog 29

FILE: 21 25 23 22 24

FILE: 29 25 23 22 24

Code:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <errno.h>
#include <stdint.h>

/* Function that handles errors */
void fatal_error(const char *message){

    perror(message);
    exit(EXIT_FAILURE);
}

int main(int argc, char **argv){

    if(argc != 2)
        fatal_error("Bad arguments.\n");

    /* Fetching command line argument */
    int temp = atoi(argv[1]);

    /* Opening file and checking for errors */
    FILE *file = fopen("input.txt", "r+");
    if(!file)
        fatal_error("Unable to open file.\n");

    /* Finding minimum in the file */
    int min = 200;
    int value;
    while(fscanf(file, "%d", &value) != EOF)
        if(value < min)
            min = value;

    /* Exiting if nothing needs to change */
    if(temp <= min)
        return 0;

    /* Creating file descriptor from stream and checking for errors */
    int fdOpen = fileno(file); 
    if(fdOpen == -1)
        fatal_error("Unable to open file descriptor.\n");

    /* Moving offset to the beginning of the file */
    off_t of = lseek(fdOpen,0,SEEK_SET);
    printf("Ofset pre petlje: %jd\n", (intmax_t)of);
    while(1){

        /* I'm reading file all over again */
        if(fscanf(file, "%d", &value) == EOF)
            fatal_error("Reached end of file.\n");

        /* If I reached minimum */
        if(value ==  min){

            /* I lock that part of the file - temperatures are two digit numbers*/
            struct flock lock;
            lock.l_type = F_WRLCK;
            lock.l_whence = SEEK_CUR;
            lock.l_start = -2;
            lock.l_len = 2;

            /* I create lock */
            if(fcntl(fdOpen,F_SETLK, &lock) == -1){

                if(errno == EACCES || errno == EAGAIN)
                    fatal_error("File is locked.\n");
            }

            /* Moving two positions back from current position */
            off_t offset;
            if((offset = lseek(fdOpen, -2, SEEK_CUR)) == -1)
                fatal_error("lseek error.\n");

            /* Inserting read command line value into file */
            fprintf(file, "%d", temp);

            /* Unlocking */
            lock.l_type = F_UNLCK;
            if(fcntl(fdOpen, F_SETLK, &lock) == -1)
                fatal_error("Unable to destroy lock.\n");

            /* Closing file descriptor, and breaking loop */
            close(fdOpen);
            break;
        }
    }
    return 0;
}

This however doesn't work. File won't change. The problem is - I know this is correct way to do it. I have written basically the same program that changes for example every appearance of word "aa" to "bb". It is basically the same concept.

I've tried:

  • I've tried fetching offset before and right after fscanf() in while() loop. Before first fscanf offset is set to 0 - beginning of the file. After first fscanf offset is set to end of the file, and offset remains at the end of the file after each iteration.
  • I've tried using ftell() in these same printf() and ftell() gives me correct offset. However that ftell() in while loop still gives end of the file. I've also tried using fseek() instead of lseek() (Even tho I know fseek() uses lseek() in its implementation). Still, same result.
  • I've tried using char* buffer instead of values. Basically to use fscanf(file,"%s",buffer), convert that value to check whether read value is minimum, and afterwards I've used fprintf(file,"%s",buffer) to write that. However same result.
  • I've tried locking whole file (I thought - maybe there's the problem), however, same result.

Code:

Here is second program I mentioned that uses same concept. This code works, but I've tried printing offset here too, and offset is also at the end of the file.

int main(int argc, char **argv){

    if(argc != 4)
        fatal_error("You must enter exactly 4 arguments.\n");

    FILE *f = fopen(argv[1], "r+");
    if(!f)
        fatal_error("Unable to open file for reading and writing.\n");

    int fd = fileno(f);
    if(fd == -1)
        fatal_error("Unable to fetch file descriptor for file.\n");

    char word[MAX_LEN + 1];
    int word_len = strlen(argv[2]);
    while(fscanf(f,"%s",word) != EOF){

        printf("%jd\n", lseek(fd,0,SEEK_CUR));
        if(!strcmp(argv[2],word)){

            struct flock lock;
            lock.l_type = F_WRLCK;
            lock.l_whence = SEEK_CUR;
            lock.l_start = -word_len;
            lock.l_len = word_len;

            if(fcntl(fd, F_SETLKW, &lock) == -1)
                fatal_error("File locking failed.\n");

            if(lseek(fd, -word_len, SEEK_CUR) == -1)
                fatal_error("Lseek error.\n");
            fprintf(f, "%s", argv[3]);

            lock.l_type = F_UNLCK;
            if(fcntl(fd, F_SETLK, &lock) == -1)
                fatal_error("Failed to release lock.\n");
        }
    }
}

As you can see it is exactly same concept. Second program works, first one doesn't.

I'm very confused now. If I create file descriptor from file stream, and then use lseek() on that file descriptor to change offset, does offset of stream also change? And if you use fscanf() to read something from stream, do offset_t change just as much as you read from file? Is there any difference in changing offset if I use fscanf() with format specifier %d and %s?


Solution

  • The way you want to replace values will work ONLY when both (source and replacement text) have the same length in you case lenght(aa)==length(bb). Mainly you should be careful using FILE* and int fd descriptor and always close the file before exit.

    Calling close(fd) before fclose(f) will cause the buffered data not to be written.

    The other problem - locking a file region relatively to SEEK_CUR will not lock the part of file you want to modify

    Here you have code a little modified that is working:

    int main(int argc, char **argv){
    
        if(argc != 4)
            fatal_error("You must enter exactly 3 arguments.\n");
    
        if(strlen(argv[2])!=strlen(argv[3]))
            fatal_error("src&dst words must be the length.\n");
    
        FILE *f = fopen(argv[1], "r+");
        if(!f)
            fatal_error("Unable to open file for reading and writing.\n");
    
        int fd = fileno(f);
        if(fd == -1)
            fatal_error("Unable to fetch file descriptor for file.\n");
    
        char word[MAX_LEN + 1];
        int word_len = strlen(argv[2]);
        while(fscanf(f,"%s",word) != EOF){
    
            printf("%jd\n", ftell(f));
            if(!strcmp(argv[2],word)){
    
                struct flock lock;
                lock.l_type = F_WRLCK;
                lock.l_whence = SEEK_SET;
                lock.l_start = ftell(f)-word_len;
                lock.l_len = word_len;
    
                if(fcntl(fd, F_SETLKW, &lock) == -1)
                    fatal_error("File locking failed.\n");
    
                fseek(f,-word_len,SEEK_CUR); //FILE* based seek
                fprintf(f, "%s", argv[3]);
                fflush(f); //sync output
    
                lock.l_type = F_UNLCK;
                if(fcntl(fd, F_SETLK, &lock) == -1)
                    fatal_error("Failed to release lock.\n");
            }
        }
        fclose(f); // close the file
    }
    

    Update1: FILE interface has its own buffering, which is not in sync with int fd. So the main problem is using lseek, while fseek should be used

    Update2: code with loop looking for min value

    int main(int argc, char **argv){
    
        if(argc != 3)
            fatal_error("You must enter exactly 2 arguments.\n");
    
        if(strlen(argv[2]) != 2)
            fatal_error("replace num must have 2 digits.\n");
    
        FILE *f = fopen(argv[1], "r+");
        if(!f)
            fatal_error("Unable to open file for reading and writing.\n");
    
        int fd = fileno(f);
        if(fd == -1)
            fatal_error("Unable to fetch file descriptor for file.\n");
    
        // search for minimum
        int word_len = strlen(argv[2]);
        int value, minValue;
        long minOffs=-1;
        while(fscanf(f,"%d",&value) == 1){ //compare number of parsed items
            printf("test value %d\n", value);
            if (minValue > value) {
                minValue = value;
                minOffs = ftell(f) - word_len;
            }
        }
    
        // replace if found
        if (minOffs >= 0) {
            printf("replacing v=%d at %ld\n", minValue, minOffs);
            struct flock lock;
            memset(&lock, 0, sizeof(lock));
            lock.l_type = F_WRLCK;
            lock.l_whence = SEEK_SET;
            lock.l_start = minOffs;
            lock.l_len = word_len;
    
            fseek(f,minOffs,SEEK_SET);
            if(fcntl(fd, F_SETLK, &lock) == -1)
                fatal_error("File locking failed.\n");
    
            fprintf(f, "%s", argv[2]);
            fflush(f); //sync output
    
            lock.l_type = F_UNLCK;
            if(fcntl(fd, F_SETLK, &lock) == -1)
                fatal_error("Failed to release lock.\n");
        }
        fclose(f);
    }