// error handling from system calls, efficiency
// don't use libc call variants in HW1 unless allowed

// need to #include all sorts of headers for the functions called below

// note: most man pages have a short section on "return value", saying what
// happens when the function succeeds; and then followed by a LONG section
// listed many possible errors that could occur.  The lesson is: there's
// usually only one way in which a function succeeds, but many ways it can
// fail.  Your code for handling errors, should be correspondingly longer
// than the code that handles the situation when everything workds great.

// errors are not all the same: some errors are more severe than others, and
// it may also depend on your applications.  Some apps can treat some errors
// as so serious that the app has to abort, some can be worked around, some
// just logging an informative message, and some even ignored.  It all
// depends on the application and error in question.

main()
{
  char *filename = "foo.txt";
  int fd;
  int ret; // also allocated automatically on the stack.
  char buf[100]; // automatic var/buffer, allocated on the stack

  fd = open(filename, O_RDONLY);
  // note: in POSIX, syscalls return -1 upon failure.  Non-syscalls may
  // indicate a failure in some other way!  However, some older OSs may
  // return some other negative number to indicate failure.
  if (fd < 0) {
    // bad uninformative message
    //    printf("program failed");
    //    printf("failed to open file \"%s\"\n", filename);
    //    printf("failed to open file \"%s\" (errno=%d)\n", filename, errno);
    //    printf("failed to open file \"%s\": %s\n",
    //	   filename, strerror(errno));
    // strerror translates error codes to short hard coded strings
    // e.g., "ENOENT" -> "No such file or directory"
    fprintf(stderr, "%s: %s\n", filename, strerror(errno));

    // aside: in unix there are three default channels, special file
    // descriptors that the OS opens automatically for every process that
    // starts up:
    // fd 0 -- stdin: used to read input from the terminal
    // fd 1 -- stdout: used to display "normal" output to the terminal
    // fd 2 -- stderr: used to display "error" output to the terminal
    // you can capture, redirect, or disable any of these FDs.

    // shortcut func perror(3), exactly as above fprintf line
    perror(filename);

    // recall that "errno" (from libc) is set to the error number of the
    // last failed system call.  It does NOT get reset or changed by any
    // other function, or by a syscall that succeeded.

    exit(1); // serious issue, must be able to read input file
    // note: existing w/ non-zero value, to indicate to the caller that this
    // program failed.  Important for wrapper programs/scripts, to determine
    // if sub-programs being called succeed or fail.
  }

  // after presumably a successful open(2) above, we now need to read data
  // from the file.
  // from man page: ssize_t read(int fd, void *buf, size_t count);
  // read(2) returns an int noting the "number of bytes successfully read"
  // inputs: the fd you want to read from, a buffer for the OS to copy the
  // bytes into, and how many bytes you want to read (count)
  ret = read(fd, buf, 100);
  // possible return codes into 'ret'
  // 100: got exactly how many bytes asked to read
  // 40: if I get to the EOF, there may not be 100 bytes to read
  // any value from 1..99 means that read(2) returned fewer bytes than
  // expected.  This is called a "partial read".
  // 0: reached the EOF.  Note a partial read is NOT guaranteed to indicate
  // EOF.
  // <0: got an error, look at errno to figure out why
  //	EBADF: bad fd (never opened, closed, opened for different perms)
  //	EIO: unspcified hardware failure
  //	ENOMEM: there was a mem shortage inside the OS
  //	EAGAIN: a transient error, the OS telling the program to "try again
  //            later" and you might get some data.  More common when the fd
  //		points to a network socket.
  // What if OS returned a number > 100?!  That would not be your fault, b/c
  // POSIX says this can't happen.  If happens, it may mean a serious
  // hardware failure, possible mem corrupion in your program, or even an
  // OS bug(1).  Probably no need to check for this all the time, but
  // perhaps consider it a debugging code to enable in case you suspect a
  // problem.
  if (ret < 0) {
    // handle transient errors like EGAIN
    // you may want to sleep(3) a bit, maybe log a message, then retry.  How
    // many times to retry: depends on application.  Practically speaking,
    // the OS won't make you retry too many times (perhaps just a few).
    // else
    perror(....); // considered an error
    exit(1);
  }
  if (ret == 0) { // EOF
    // go to the next phase of your program, close this fd, etc.
  }
  if (re == 100) { // got all the bytes we wanted
    // do what's needed
  } else { // partial read, got some bytes, fewer than 100
    // what to do on partial reads?  Depends on program.  If program can
    // process data in any units, you can keep going.  But if program
    // expects units of some fixed size, then you my need to keep reading
    // until you have enough data to process; or create a buffer and "pad it
    // with zeros".

    // if your prog is ok w/ partial reads, and no special handling is
    // needed, put a comment for future programers.
  }

  // how much data to read?
  // 1. what if I declared my buffer as
  char buf[1];
  // then read as
  ret = read(fd, buf, 1);
  // above is bad: issuing too many syscalls, slows program+OS (but works)

  // 2. what if I declared my buffer as
  char buf[1000000000000]; // 10^12 (TiB) -- allocated on the stack
  // then read as
  ret = read(fd, buf, 1000000000000);
  // above is also bad: allocating a huge mem buffer, asking OS to copy too
  // much data all at once.  You may actualy get a failure here, in that
  // your program may not enen start if you tried to alloc such a huge
  // buffer on the stack

  // slightly better, don't alloc huge bufs on stack, use malloc(3)
  char *buf;
  buf = malloc(1000000000000);
  if (buf == NULL) {
    fprintf(stderr, "failed to alloc buf as 1000000000000\n");
    exit(1);
  }
  ret = read(fd, buf, 1000000000000);
  // above code still bad b/c you shouldn't try to read in huge chunks.
  // but generally: the max size of your STACK segment is more limited by
  // the OS than your HEAP segment.  So for any unit size, you have a better
  // chance of allocating it using malloc than declaring it on the stack
  // (aka an "automatic variable", as compared to "dynamically allocated"
  // memory via malloc).  Don't forget to call free(buf) when done.

  // 3. what's the right size?
  // in any computer system, there are some native unit sizes that are more
  // appropriate.
  // Networking: MTU size, eg. 1500
  // Storage block devices: "sector" or block size, historically 512B, more
  // modern storage (e.g., SSDs) use 4KB.
  // CPU memory architectures: mem pages are 4KB by default (Intel).  Some
  // odd processors may support larger pages (8KB, 16KB, 32KB).

  // You can call getpagesize(2) to find out the native page size on your
  // system.  Or call stat(2) to find out the st_blksize that's native to
  // the file system the file lives on.

  // any of the above native units would be a good default size to read.  In
  // practice, you can get more performance by setting you read size on the
  // file to be a small multiple, power of 2, of the native unit.  E.g., if
  // the native unit is 4KB, you can try and read 2x4KB, or 4*4KB, etc.


  // don't hardcode numbers like 100 etc.  Use a macro if you have to:

#define MAX_READ_SIZE 100

}
