diff options
Diffstat (limited to 'libbuild2/depdb.cxx')
-rw-r--r-- | libbuild2/depdb.cxx | 399 |
1 files changed, 399 insertions, 0 deletions
diff --git a/libbuild2/depdb.cxx b/libbuild2/depdb.cxx new file mode 100644 index 0000000..32e5916 --- /dev/null +++ b/libbuild2/depdb.cxx @@ -0,0 +1,399 @@ +// file : libbuild2/depdb.cxx -*- C++ -*- +// copyright : Copyright (c) 2014-2019 Code Synthesis Ltd +// license : MIT; see accompanying LICENSE file + +#include <libbuild2/depdb.hxx> + +#ifdef _WIN32 +# include <libbutl/win32-utility.hxx> +#endif + +#include <libbuild2/filesystem.hxx> // mtime() +#include <libbuild2/diagnostics.hxx> + +using namespace std; +using namespace butl; + +namespace build2 +{ + depdb_base:: + depdb_base (const path& p, timestamp mt) + { + fdopen_mode om (fdopen_mode::out | fdopen_mode::binary); + ifdstream::iostate em (ifdstream::badbit); + + if (mt == timestamp_nonexistent) + { + state_ = state::write; + om |= fdopen_mode::create | fdopen_mode::exclusive; + em |= ifdstream::failbit; + } + else + { + state_ = state::read; + om |= fdopen_mode::in; + } + + auto_fd fd; + try + { + fd = fdopen (p, om); + } + catch (const io_error&) + { + bool c (state_ == state::write); + + diag_record dr (fail); + dr << "unable to " << (c ? "create" : "open") << ' ' << p; + + if (c) + dr << info << "did you forget to add fsdir{} prerequisite for " + << "output directory?"; + + dr << endf; + } + + // Open the corresponding stream. Note that if we throw after that, the + // corresponding member will not be destroyed. This is the reason for the + // depdb/base split. + // + if (state_ == state::read) + { + new (&is_) ifdstream (move (fd), em); + buf_ = static_cast<fdbuf*> (is_.rdbuf ()); + } + else + { + new (&os_) ofdstream (move (fd), em); + buf_ = static_cast<fdbuf*> (os_.rdbuf ()); + } + } + + depdb:: + depdb (path_type&& p, timestamp mt) + : depdb_base (p, mt), + path (move (p)), + mtime (mt != timestamp_nonexistent ? mt : timestamp_unknown), + touch (false) + { + // Read/write the database format version. + // + if (state_ == state::read) + { + string* l (read ()); + if (l == nullptr || *l != "1") + write ('1'); + } + else + write ('1'); + } + + depdb:: + depdb (path_type p) + : depdb (move (p), build2::mtime (p)) + { + } + + void depdb:: + change (bool trunc) + { + assert (state_ != state::write); + + // Transfer the file descriptor from ifdstream to ofdstream. Note that the + // steps in this dance must be carefully ordered to make sure we don't + // call any destructors twice in the face of exceptions. + // + auto_fd fd (is_.release ()); + + // Consider this scenario: we are overwriting an old line (so it ends with + // a newline and the "end marker") but the operation failed half way + // through. Now we have the prefix from the new line, the suffix from the + // old, and everything looks valid. So what we need is to somehow + // invalidate the old content so that it can never combine with (partial) + // new content to form a valid line. One way to do that would be to + // truncate the file. + // + if (trunc) + try + { + fdtruncate (fd.get (), pos_); + } + catch (const io_error& e) + { + fail << "unable to truncate " << path << ": " << e; + } + + // Note: the file descriptor position can be beyond the pos_ value due to + // the ifdstream buffering. That's why we need to seek to switch from + // reading to writing. + // + try + { + fdseek (fd.get (), pos_, fdseek_mode::set); + } + catch (const io_error& e) + { + fail << "unable to rewind " << path << ": " << e; + } + + // @@ Strictly speaking, ofdstream can throw which will leave us in a + // non-destructible state. Unlikely but possible. + // + is_.~ifdstream (); + new (&os_) ofdstream (move (fd), + ofdstream::badbit | ofdstream::failbit, + pos_); + buf_ = static_cast<fdbuf*> (os_.rdbuf ()); + + state_ = state::write; + mtime = timestamp_unknown; + } + + string* depdb:: + read_ () + { + // Save the start position of this line so that we can overwrite it. + // + pos_ = buf_->tellg (); + + try + { + // Note that we intentionally check for eof after updating the write + // position. + // + if (state_ == state::read_eof) + return nullptr; + + getline (is_, line_); // Calls line_.erase(). + + // The line should always end with a newline. If it doesn't, then this + // line (and the rest of the database) is assumed corrupted. Also peek + // at the character after the newline. We should either have the next + // line or '\0', which is our "end marker", that is, it indicates the + // database was properly closed. + // + ifdstream::int_type c; + if (is_.fail () || // Nothing got extracted. + is_.eof () || // Eof reached before delimiter. + (c = is_.peek ()) == ifdstream::traits_type::eof ()) + { + // Preemptively switch to writing. While we could have delayed this + // until the user called write(), if the user calls read() again (for + // whatever misguided reason) we will mess up the overwrite position. + // + change (); + return nullptr; + } + + // Handle the "end marker". Note that the caller can still switch to the + // write mode on this line. And, after calling read() again, write to + // the next line (i.e., start from the "end marker"). + // + if (c == '\0') + state_ = state::read_eof; + } + catch (const io_error& e) + { + fail << "unable to read from " << path << ": " << e; + } + + return &line_; + } + + bool depdb:: + skip () + { + if (state_ == state::read_eof) + return true; + + assert (state_ == state::read); + + // The rest is pretty similar in logic to read_() above. + // + pos_ = buf_->tellg (); + + try + { + // Keep reading lines checking for the end marker after each newline. + // + ifdstream::int_type c; + do + { + if ((c = is_.get ()) == '\n') + { + if ((c = is_.get ()) == '\0') + { + state_ = state::read_eof; + return true; + } + } + } while (c != ifdstream::traits_type::eof ()); + } + catch (const io_error& e) + { + fail << "unable to read from " << path << ": " << e; + } + + // Invalid database so change over to writing. + // + change (); + return false; + } + + void depdb:: + write (const char* s, size_t n, bool nl) + { + // Switch to writing if we are still reading. + // + if (state_ != state::write) + change (); + + try + { + os_.write (s, static_cast<streamsize> (n)); + + if (nl) + os_.put ('\n'); + } + catch (const io_error& e) + { + fail << "unable to write to " << path << ": " << e; + } + } + + void depdb:: + write (char c, bool nl) + { + // Switch to writing if we are still reading. + // + if (state_ != state::write) + change (); + + try + { + os_.put (c); + + if (nl) + os_.put ('\n'); + } + catch (const io_error& e) + { + fail << "unable to write to " << path << ": " << e; + } + } + + void depdb:: + close () + { + // If we are at eof, then it means all lines are good, there is the "end + // marker" at the end, and we don't need to do anything, except, maybe + // touch the file. Otherwise, if we are still in the read mode, truncate + // the rest, and then add the "end marker" (we cannot have anything in the + // write mode since we truncate in change()). + // + if (state_ == state::read_eof) + { + if (!touch) + try + { + is_.close (); + return; + } + catch (const io_error& e) + { + fail << "unable to close " << path << ": " << e; + } + + // While there are utime(2)/utimensat(2) (and probably something similar + // for Windows), for now we just overwrite the "end marker". Hopefully + // no implementation will be smart enough to recognize this is a no-op + // and skip updating mtime (which would probably be incorrect, spec- + // wise). And this could even be faster since we already have the file + // descriptor. Or it might be slower since so far we've only been + // reading. + // + pos_ = buf_->tellg (); // The last line is accepted. + change (false /* truncate */); // Write end marker below. + } + else if (state_ != state::write) + { + pos_ = buf_->tellg (); // The last line is accepted. + change (true /* truncate */); + } + + if (mtime_check ()) + start_ = system_clock::now (); + + try + { + os_.put ('\0'); // The "end marker". + os_.close (); + } + catch (const io_error& e) + { + fail << "unable to flush " << path << ": " << e; + } + + // On some platforms (currently confirmed on FreeBSD running as VMs) one + // can sometimes end up with a modification time that is a bit after the + // call to close(). And in some tight cases this can mess with our + // "protocol" that a valid depdb should be no older than the target it is + // for. + // + // Note that this does not seem to be related to clock adjustments but + // rather feels like the modification time is set when the changes + // actually hit some lower-level layer (e.g., OS or filesystem + // driver). One workaround that appears to work is to query the + // mtime. This seems to force that layer to commit to a timestamp. + // +#if defined(__FreeBSD__) + mtime = build2::mtime (path); // Save for debugging/check below. +#endif + } + + void depdb:: + check_mtime_ (const path_type& t, timestamp e) + { + // We could call the static version but then we would have lost additional + // information for some platforms. + // + timestamp t_mt (build2::mtime (t)); + timestamp d_mt (build2::mtime (path)); + + if (d_mt > t_mt) + { + if (e == timestamp_unknown) + e = system_clock::now (); + + fail << "backwards modification times detected:\n" + << " " << start_ << " sequence start\n" +#if defined(__FreeBSD__) + << " " << mtime << " close mtime\n" +#endif + << " " << d_mt << " " << path.string () << '\n' + << " " << t_mt << " " << t.string () << '\n' + << " " << e << " sequence end"; + } + } + + void depdb:: + check_mtime_ (timestamp s, + const path_type& d, + const path_type& t, + timestamp e) + { + using build2::mtime; + + timestamp t_mt (mtime (t)); + timestamp d_mt (mtime (d)); + + if (d_mt > t_mt) + { + fail << "backwards modification times detected:\n" + << " " << s << " sequence start\n" + << " " << d_mt << " " << d.string () << '\n' + << " " << t_mt << " " << t.string () << '\n' + << " " << e << " sequence end"; + } + } +} |