3

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?

Aleksandar Makragić
  • 1,957
  • 17
  • 32
  • Please specify the constraint on the numbers (room temperatures ), are they always 2 digits positive integer ? which could simplify the issue a lot. and that there is only one minimum within the file. or specify the scenario if there are multiple minimum values. – dvhh Jun 23 '16 at 05:29
  • Did you replace the first lseek with fseek? Not clearing the EOF from the first loop seem like a problem when using lseek there. – lossleader Jun 23 '16 at 12:07

1 Answers1

1

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);
}
krzydyn
  • 1,012
  • 9
  • 19
  • You modified code that already works. First code is the problem. I can't figure out why it won't work and second one does work. – Aleksandar Makragić Jun 21 '16 at 09:22
  • "The way you want to replace values will work ONLY when both (source and replacement text) have the same length." - This is not true at all, I can lock whole file if I want and replace small part of it. – Aleksandar Makragić Jun 21 '16 at 09:23
  • What happens if file content "ab cd ef" you replace "cd" with "xyz" ? What I mean the content will be corrupter. If replacement have other length than source, then you have to rewrite file to the end. – krzydyn Jun 21 '16 at 09:37
  • Like I said, I understand what second code does, and when it works, when it doesn't work. There is assumption that you can replace two letters word with another two letter words. – Aleksandar Makragić Jun 21 '16 at 09:48
  • Ok, fine. Not let's narrow to the reason your first program is not working as you want - see the updated answer. – krzydyn Jun 21 '16 at 09:51
  • If FILE interface has its own buffering - why does second code work? Please read one more time what I tried with ftell(), fseek(). – Aleksandar Makragić Jun 21 '16 at 09:54
  • before seccong loop you are using: `off_t of = lseek(fdOpen,0,SEEK_SET);` which is not in sync with farther `fscanf`. More over I noticed that `fscanf` not always return EOF, and it is more obust if check `fscanf(..) == 1`. I added code to my answer, so can compare to yours. – krzydyn Jun 21 '16 at 10:26
  • I know, you should check return value and then check `feof()` - however, that is not cause of the problem. If `off_t of = lseek(fdOpen,0,SEEK_SET);` doesn't change FILE* offset, once again, why does `lseek(fd, -word_len, SEEK_CUR)` work in second code? – Aleksandar Makragić Jun 22 '16 at 13:57
  • 1
    The second code works because you don't close `fdOpen`. When you add such (as you have in first code) Then the behavior will be the same - file will not change because data written with `fprintf` is actually not written until `fflush` or `fclose`. – krzydyn Jun 23 '16 at 08:34
  • Files are fully buffered, I know that. I've basically used `fflush(NULL)`, `fclose()` and `close()` outside `while(1)` loop and everything works. I don't understand why, even tho I haven't close file stream, buffers should clear out when OS closes process, right? When you close file descriptor made from file stream, do you also kill that file stream? – Aleksandar Makragić Jun 23 '16 at 08:55
  • When process is exiting - some cleaup code makes a call to `fclose(NULL)`. But if you did `close(fdOpen)` before, then `fclose` will do nothing because they share the same value and after `close` this value is simply invalid. Note the other problem - locking a file region relatively to SEEK_CUR will not lock the part of file you want to modify – krzydyn Jun 23 '16 at 09:38
  • "locking a file region relatively to SEEK_CUR will not lock the part of file you want to modify" - why? – Aleksandar Makragić Jun 23 '16 at 14:25
  • Because `ftell(f)` and `lseek(fd,0,SEEK_CUR)` return different values at the point before flock if you don't set absolute position. – krzydyn Jun 23 '16 at 21:54