aboutsummaryrefslogtreecommitdiff
path: root/libbuild2/depdb.cxx
diff options
context:
space:
mode:
Diffstat (limited to 'libbuild2/depdb.cxx')
-rw-r--r--libbuild2/depdb.cxx399
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";
+ }
+ }
+}