From 7c1fdc6decd7ff5e77a56b0d675be3ad3fcb07ad Mon Sep 17 00:00:00 2001 From: Wez Furlong Date: Wed, 21 Feb 2018 15:30:02 -0800 Subject: [PATCH] detect when cookie dir has gone awol Summary: I got here because `btrfs` doesn't send `IN_DELETE_SELF` notifications when a watched subvolume is unmounted. Until that is addresses in the kernel we tackle this from a different angle. We periodically perform a synchronized `clock` call against each of the watches. When that happens and a btrfs volume has been deleted then the cookie sync will error out because the directory structure no longer exists. Let's catch that case and generate a recrawl; the recrawl will discover the removal and cancel the watch. And while we're in here, let's also deal with just the vcs subdir going away. Refs: https://github.com/facebook/watchman/issues/25 Refs: https://github.com/facebook/watchman/issues/501 Reviewed By: simpkins Differential Revision: D7014364 fbshipit-source-id: 9acd20efa843563626b73c6f6e34c3787dd28a39 --- root/sync.cpp | 45 +++++++++++++++++++++++++++++--- tests/integration/test_cookie.py | 38 +++++++++++++++++++++++++++ 2 files changed, 79 insertions(+), 4 deletions(-) create mode 100644 tests/integration/test_cookie.py diff --git a/root/sync.cpp b/root/sync.cpp index 9449bce125c1..08208da70f49 100644 --- a/root/sync.cpp +++ b/root/sync.cpp @@ -1,13 +1,28 @@ /* Copyright 2012-present Facebook, Inc. * Licensed under the Apache License, Version 2.0 */ -#include "watchman.h" #include "InMemoryView.h" +#include "watchman.h" +#include "watchman_error_category.h" + +using namespace watchman; + +namespace { +class NeedRecrawl : public std::runtime_error { + public: + using std::runtime_error::runtime_error; +}; +} // namespace bool watchman_root::syncToNow(std::chrono::milliseconds timeout) { w_perf_t sample("sync_to_now"); - - auto res = view()->syncToNow(timeout); + bool res; + try { + res = view()->syncToNow(timeout); + } catch (const NeedRecrawl& exc) { + scheduleRecrawl(exc.what()); + return false; + } // We want to know about all timeouts if (!res) { @@ -35,7 +50,29 @@ bool watchman_root::syncToNow(std::chrono::milliseconds timeout) { * time, false otherwise. */ bool watchman::InMemoryView::syncToNow(std::chrono::milliseconds timeout) { - return cookies_.syncToNow(timeout); + try { + return cookies_.syncToNow(timeout); + } catch (const std::system_error& exc) { + // Note that timeouts in syncToNow are reported as a `false` return + // value, so if we get any exception here then something must be + // really wrong. In practice the most likely cause is that + // the cookie dir no longer exists. The sanest sounding thing + // to do in this situation is schedule a recrawl. + + if (exc.code() == watchman::error_code::no_such_file_or_directory || + exc.code() == watchman::error_code::not_a_directory) { + if (cookies_.cookieDir() == root_path) { + throw NeedRecrawl("root dir was removed and we didn't get notified"); + } else { + // The cookie dir was a VCS subdir and it got deleted. Let's + // focus instead on the parent dir and recursively retry. + cookies_.setCookieDir(root_path); + return cookies_.syncToNow(timeout); + } + } + + throw; + } } /* vim:ts=2:sw=2:et: diff --git a/tests/integration/test_cookie.py b/tests/integration/test_cookie.py new file mode 100644 index 000000000000..cb3889f06abc --- /dev/null +++ b/tests/integration/test_cookie.py @@ -0,0 +1,38 @@ +# vim:ts=4:sw=4:et: +# Copyright 2018-present Facebook, Inc. +# Licensed under the Apache License, Version 2.0 + +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function +# no unicode literals + +import WatchmanTestCase +import os +import pywatchman + + +@WatchmanTestCase.expand_matrix +class TestCookie(WatchmanTestCase.WatchmanTestCase): + + def test_delete_cookie_dir(self): + root = self.mkdtemp() + cookie_dir = os.path.join(root, '.hg') + os.mkdir(cookie_dir) + self.touchRelative(root, 'foo') + + self.watchmanCommand('watch-project', root) + self.assertFileList(root, files=['foo', '.hg']) + + os.rmdir(cookie_dir) + self.assertFileList(root, files=['foo']) + os.unlink(os.path.join(root, 'foo')) + self.assertFileList(root, files=[]) + os.rmdir(root) + with self.assertRaises(pywatchman.WatchmanError) as ctx: + self.assertFileList(root, files=[]) + reason = str(ctx.exception) + self.assertTrue( + ('No such file' in reason) or + ('unable to resolve root' in reason), + msg=reason)