// file : libbuild2/depdb.cxx -*- C++ -*- // 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. // // Note also that utime() on Windows is a bad idea (see touch_file() for // details). // 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)); if (t_mt == timestamp_nonexistent) fail << "target file " << t << " does not exist at the end of recipe"; 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)); if (t_mt == timestamp_nonexistent) fail << "target file " << t << " does not exist at the end of recipe"; 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"; } } }