Skip to content

Commit c9a33e5

Browse files
committed
Merge branch 'kw/fsmonitor-watchman-racefix'
A new version of fsmonitor-watchman hook has been introduced, to avoid races. * kw/fsmonitor-watchman-racefix: fsmonitor: update documentation for hook version and watchman hooks fsmonitor: add fsmonitor hook scripts for version 2 fsmonitor: handle version 2 of the hooks that will use opaque token fsmonitor: change last update timestamp on the index_state to opaque token
2 parents 56ceb64 + dfaed02 commit c9a33e5

11 files changed

+434
-87
lines changed

Documentation/config/core.txt

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,17 @@ core.fsmonitor::
6868
avoiding unnecessary processing of files that have not changed.
6969
See the "fsmonitor-watchman" section of linkgit:githooks[5].
7070

71+
core.fsmonitorHookVersion::
72+
Sets the version of hook that is to be used when calling fsmonitor.
73+
There are currently versions 1 and 2. When this is not set,
74+
version 2 will be tried first and if it fails then version 1
75+
will be tried. Version 1 uses a timestamp as input to determine
76+
which files have changes since that time but some monitors
77+
like watchman have race conditions when used with a timestamp.
78+
Version 2 uses an opaque string so that the monitor can return
79+
something that can be used to determine what files have changed
80+
without race conditions.
81+
7182
core.trustctime::
7283
If false, the ctime differences between the index and the
7384
working tree are ignored; useful when the inode change time

Documentation/githooks.txt

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -490,9 +490,16 @@ fsmonitor-watchman
490490
~~~~~~~~~~~~~~~~~~
491491

492492
This hook is invoked when the configuration option `core.fsmonitor` is
493-
set to `.git/hooks/fsmonitor-watchman`. It takes two arguments, a version
494-
(currently 1) and the time in elapsed nanoseconds since midnight,
495-
January 1, 1970.
493+
set to `.git/hooks/fsmonitor-watchman` or `.git/hooks/fsmonitor-watchmanv2`
494+
depending on the version of the hook to use.
495+
496+
Version 1 takes two arguments, a version (1) and the time in elapsed
497+
nanoseconds since midnight, January 1, 1970.
498+
499+
Version 2 takes two arguments, a version (2) and a token that is used
500+
for identifying changes since the token. For watchman this would be
501+
a clock id. This version must output to stdout the new token followed
502+
by a NUL before the list of files.
496503

497504
The hook should output to stdout the list of all files in the working
498505
directory that may have changed since the requested time. The logic

cache.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -324,7 +324,7 @@ struct index_state {
324324
struct hashmap dir_hash;
325325
struct object_id oid;
326326
struct untracked_cache *untracked;
327-
uint64_t fsmonitor_last_update;
327+
char *fsmonitor_last_update;
328328
struct ewah_bitmap *fsmonitor_dirty;
329329
struct mem_pool *ce_mem_pool;
330330
struct progress *progress;

fsmonitor.c

Lines changed: 94 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,10 @@
66
#include "run-command.h"
77
#include "strbuf.h"
88

9-
#define INDEX_EXTENSION_VERSION (1)
10-
#define HOOK_INTERFACE_VERSION (1)
9+
#define INDEX_EXTENSION_VERSION1 (1)
10+
#define INDEX_EXTENSION_VERSION2 (2)
11+
#define HOOK_INTERFACE_VERSION1 (1)
12+
#define HOOK_INTERFACE_VERSION2 (2)
1113

1214
struct trace_key trace_fsmonitor = TRACE_KEY_INIT(FSMONITOR);
1315

@@ -24,6 +26,22 @@ static void fsmonitor_ewah_callback(size_t pos, void *is)
2426
ce->ce_flags &= ~CE_FSMONITOR_VALID;
2527
}
2628

29+
static int fsmonitor_hook_version(void)
30+
{
31+
int hook_version;
32+
33+
if (git_config_get_int("core.fsmonitorhookversion", &hook_version))
34+
return -1;
35+
36+
if (hook_version == HOOK_INTERFACE_VERSION1 ||
37+
hook_version == HOOK_INTERFACE_VERSION2)
38+
return hook_version;
39+
40+
warning("Invalid hook version '%i' in core.fsmonitorhookversion. "
41+
"Must be 1 or 2.", hook_version);
42+
return -1;
43+
}
44+
2745
int read_fsmonitor_extension(struct index_state *istate, const void *data,
2846
unsigned long sz)
2947
{
@@ -32,17 +50,26 @@ int read_fsmonitor_extension(struct index_state *istate, const void *data,
3250
uint32_t ewah_size;
3351
struct ewah_bitmap *fsmonitor_dirty;
3452
int ret;
53+
uint64_t timestamp;
54+
struct strbuf last_update = STRBUF_INIT;
3555

36-
if (sz < sizeof(uint32_t) + sizeof(uint64_t) + sizeof(uint32_t))
56+
if (sz < sizeof(uint32_t) + 1 + sizeof(uint32_t))
3757
return error("corrupt fsmonitor extension (too short)");
3858

3959
hdr_version = get_be32(index);
4060
index += sizeof(uint32_t);
41-
if (hdr_version != INDEX_EXTENSION_VERSION)
61+
if (hdr_version == INDEX_EXTENSION_VERSION1) {
62+
timestamp = get_be64(index);
63+
strbuf_addf(&last_update, "%"PRIu64"", timestamp);
64+
index += sizeof(uint64_t);
65+
} else if (hdr_version == INDEX_EXTENSION_VERSION2) {
66+
strbuf_addstr(&last_update, index);
67+
index += last_update.len + 1;
68+
} else {
4269
return error("bad fsmonitor version %d", hdr_version);
70+
}
4371

44-
istate->fsmonitor_last_update = get_be64(index);
45-
index += sizeof(uint64_t);
72+
istate->fsmonitor_last_update = strbuf_detach(&last_update, NULL);
4673

4774
ewah_size = get_be32(index);
4875
index += sizeof(uint32_t);
@@ -79,7 +106,6 @@ void fill_fsmonitor_bitmap(struct index_state *istate)
79106
void write_fsmonitor_extension(struct strbuf *sb, struct index_state *istate)
80107
{
81108
uint32_t hdr_version;
82-
uint64_t tm;
83109
uint32_t ewah_start;
84110
uint32_t ewah_size = 0;
85111
int fixup = 0;
@@ -89,11 +115,12 @@ void write_fsmonitor_extension(struct strbuf *sb, struct index_state *istate)
89115
BUG("fsmonitor_dirty has more entries than the index (%"PRIuMAX" > %u)",
90116
(uintmax_t)istate->fsmonitor_dirty->bit_size, istate->cache_nr);
91117

92-
put_be32(&hdr_version, INDEX_EXTENSION_VERSION);
118+
put_be32(&hdr_version, INDEX_EXTENSION_VERSION2);
93119
strbuf_add(sb, &hdr_version, sizeof(uint32_t));
94120

95-
put_be64(&tm, istate->fsmonitor_last_update);
96-
strbuf_add(sb, &tm, sizeof(uint64_t));
121+
strbuf_addstr(sb, istate->fsmonitor_last_update);
122+
strbuf_addch(sb, 0); /* Want to keep a NUL */
123+
97124
fixup = sb->len;
98125
strbuf_add(sb, &ewah_size, sizeof(uint32_t)); /* we'll fix this up later */
99126

@@ -110,9 +137,9 @@ void write_fsmonitor_extension(struct strbuf *sb, struct index_state *istate)
110137
}
111138

112139
/*
113-
* Call the query-fsmonitor hook passing the time of the last saved results.
140+
* Call the query-fsmonitor hook passing the last update token of the saved results.
114141
*/
115-
static int query_fsmonitor(int version, uint64_t last_update, struct strbuf *query_result)
142+
static int query_fsmonitor(int version, const char *last_update, struct strbuf *query_result)
116143
{
117144
struct child_process cp = CHILD_PROCESS_INIT;
118145

@@ -121,7 +148,7 @@ static int query_fsmonitor(int version, uint64_t last_update, struct strbuf *que
121148

122149
argv_array_push(&cp.args, core_fsmonitor);
123150
argv_array_pushf(&cp.args, "%d", version);
124-
argv_array_pushf(&cp.args, "%" PRIuMAX, (uintmax_t)last_update);
151+
argv_array_pushf(&cp.args, "%s", last_update);
125152
cp.use_shell = 1;
126153
cp.dir = get_git_work_tree();
127154

@@ -148,14 +175,18 @@ static void fsmonitor_refresh_callback(struct index_state *istate, const char *n
148175
void refresh_fsmonitor(struct index_state *istate)
149176
{
150177
struct strbuf query_result = STRBUF_INIT;
151-
int query_success = 0;
152-
size_t bol; /* beginning of line */
178+
int query_success = 0, hook_version = -1;
179+
size_t bol = 0; /* beginning of line */
153180
uint64_t last_update;
181+
struct strbuf last_update_token = STRBUF_INIT;
154182
char *buf;
155183
unsigned int i;
156184

157185
if (!core_fsmonitor || istate->fsmonitor_has_run_once)
158186
return;
187+
188+
hook_version = fsmonitor_hook_version();
189+
159190
istate->fsmonitor_has_run_once = 1;
160191

161192
trace_printf_key(&trace_fsmonitor, "refresh fsmonitor");
@@ -164,26 +195,60 @@ void refresh_fsmonitor(struct index_state *istate)
164195
* should be inclusive to ensure we don't miss potential changes.
165196
*/
166197
last_update = getnanotime();
198+
if (hook_version == HOOK_INTERFACE_VERSION1)
199+
strbuf_addf(&last_update_token, "%"PRIu64"", last_update);
167200

168201
/*
169-
* If we have a last update time, call query_fsmonitor for the set of
170-
* changes since that time, else assume everything is possibly dirty
202+
* If we have a last update token, call query_fsmonitor for the set of
203+
* changes since that token, else assume everything is possibly dirty
171204
* and check it all.
172205
*/
173206
if (istate->fsmonitor_last_update) {
174-
query_success = !query_fsmonitor(HOOK_INTERFACE_VERSION,
175-
istate->fsmonitor_last_update, &query_result);
207+
if (hook_version == -1 || hook_version == HOOK_INTERFACE_VERSION2) {
208+
query_success = !query_fsmonitor(HOOK_INTERFACE_VERSION2,
209+
istate->fsmonitor_last_update, &query_result);
210+
211+
if (query_success) {
212+
if (hook_version < 0)
213+
hook_version = HOOK_INTERFACE_VERSION2;
214+
215+
/*
216+
* First entry will be the last update token
217+
* Need to use a char * variable because static
218+
* analysis was suggesting to use strbuf_addbuf
219+
* but we don't want to copy the entire strbuf
220+
* only the the chars up to the first NUL
221+
*/
222+
buf = query_result.buf;
223+
strbuf_addstr(&last_update_token, buf);
224+
if (!last_update_token.len) {
225+
warning("Empty last update token.");
226+
query_success = 0;
227+
} else {
228+
bol = last_update_token.len + 1;
229+
}
230+
} else if (hook_version < 0) {
231+
hook_version = HOOK_INTERFACE_VERSION1;
232+
if (!last_update_token.len)
233+
strbuf_addf(&last_update_token, "%"PRIu64"", last_update);
234+
}
235+
}
236+
237+
if (hook_version == HOOK_INTERFACE_VERSION1) {
238+
query_success = !query_fsmonitor(HOOK_INTERFACE_VERSION1,
239+
istate->fsmonitor_last_update, &query_result);
240+
}
241+
176242
trace_performance_since(last_update, "fsmonitor process '%s'", core_fsmonitor);
177243
trace_printf_key(&trace_fsmonitor, "fsmonitor process '%s' returned %s",
178244
core_fsmonitor, query_success ? "success" : "failure");
179245
}
180246

181247
/* a fsmonitor process can return '/' to indicate all entries are invalid */
182-
if (query_success && query_result.buf[0] != '/') {
248+
if (query_success && query_result.buf[bol] != '/') {
183249
/* Mark all entries returned by the monitor as dirty */
184250
buf = query_result.buf;
185-
bol = 0;
186-
for (i = 0; i < query_result.len; i++) {
251+
for (i = bol; i < query_result.len; i++) {
187252
if (buf[i] != '\0')
188253
continue;
189254
fsmonitor_refresh_callback(istate, buf + bol);
@@ -217,18 +282,21 @@ void refresh_fsmonitor(struct index_state *istate)
217282
}
218283
strbuf_release(&query_result);
219284

220-
/* Now that we've updated istate, save the last_update time */
221-
istate->fsmonitor_last_update = last_update;
285+
/* Now that we've updated istate, save the last_update_token */
286+
FREE_AND_NULL(istate->fsmonitor_last_update);
287+
istate->fsmonitor_last_update = strbuf_detach(&last_update_token, NULL);
222288
}
223289

224290
void add_fsmonitor(struct index_state *istate)
225291
{
226292
unsigned int i;
293+
struct strbuf last_update = STRBUF_INIT;
227294

228295
if (!istate->fsmonitor_last_update) {
229296
trace_printf_key(&trace_fsmonitor, "add fsmonitor");
230297
istate->cache_changed |= FSMONITOR_CHANGED;
231-
istate->fsmonitor_last_update = getnanotime();
298+
strbuf_addf(&last_update, "%"PRIu64"", getnanotime());
299+
istate->fsmonitor_last_update = strbuf_detach(&last_update, NULL);
232300

233301
/* reset the fsmonitor state */
234302
for (i = 0; i < istate->cache_nr; i++)
@@ -250,7 +318,7 @@ void remove_fsmonitor(struct index_state *istate)
250318
if (istate->fsmonitor_last_update) {
251319
trace_printf_key(&trace_fsmonitor, "remove fsmonitor");
252320
istate->cache_changed |= FSMONITOR_CHANGED;
253-
istate->fsmonitor_last_update = 0;
321+
FREE_AND_NULL(istate->fsmonitor_last_update);
254322
}
255323
}
256324

t/helper/test-dump-fsmonitor.c

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ int cmd__dump_fsmonitor(int ac, const char **av)
1313
printf("no fsmonitor\n");
1414
return 0;
1515
}
16-
printf("fsmonitor last update %"PRIuMAX"\n", (uintmax_t)istate->fsmonitor_last_update);
16+
printf("fsmonitor last update %s\n", istate->fsmonitor_last_update);
1717

1818
for (i = 0; i < istate->cache_nr; i++)
1919
printf((istate->cache[i]->ce_flags & CE_FSMONITOR_VALID) ? "+" : "-");

t/t7519-status-fsmonitor.sh

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,11 +32,12 @@ write_integration_script () {
3232
echo "$0: exactly 2 arguments expected"
3333
exit 2
3434
fi
35-
if test "$1" != 1
35+
if test "$1" != 2
3636
then
3737
echo "Unsupported core.fsmonitor hook version." >&2
3838
exit 1
3939
fi
40+
printf "last_update_token\0"
4041
printf "untracked\0"
4142
printf "dir1/untracked\0"
4243
printf "dir2/untracked\0"
@@ -107,6 +108,7 @@ EOF
107108
# test that "update-index --fsmonitor-valid" sets the fsmonitor valid bit
108109
test_expect_success 'update-index --fsmonitor-valid" sets the fsmonitor valid bit' '
109110
write_script .git/hooks/fsmonitor-test<<-\EOF &&
111+
printf "last_update_token\0"
110112
EOF
111113
git update-index --fsmonitor &&
112114
git update-index --fsmonitor-valid dir1/modified &&
@@ -167,6 +169,7 @@ EOF
167169
# test that newly added files are marked valid
168170
test_expect_success 'newly added files are marked valid' '
169171
write_script .git/hooks/fsmonitor-test<<-\EOF &&
172+
printf "last_update_token\0"
170173
EOF
171174
git add new &&
172175
git add dir1/new &&
@@ -207,6 +210,7 @@ EOF
207210
# test that *only* files returned by the integration script get flagged as invalid
208211
test_expect_success '*only* files returned by the integration script get flagged as invalid' '
209212
write_script .git/hooks/fsmonitor-test<<-\EOF &&
213+
printf "last_update_token\0"
210214
printf "dir1/modified\0"
211215
EOF
212216
clean_repo &&
@@ -276,6 +280,7 @@ do
276280
# (if enabled) files unless it is told about them.
277281
test_expect_success "status doesn't detect unreported modifications" '
278282
write_script .git/hooks/fsmonitor-test<<-\EOF &&
283+
printf "last_update_token\0"
279284
:>marker
280285
EOF
281286
clean_repo &&

t/t7519/fsmonitor-all

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ fi
1717

1818
if test "$1" != 1
1919
then
20-
echo "Unsupported core.fsmonitor hook version." >&2
2120
exit 1
2221
fi
2322

t/t7519/fsmonitor-all-v2

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
#!/usr/bin/perl
2+
3+
use strict;
4+
use warnings;
5+
#
6+
# An test hook script to integrate with git to test fsmonitor.
7+
#
8+
# The hook is passed a version (currently 2) and since token
9+
# formatted as a string and outputs to stdout all files that have been
10+
# modified since the given time. Paths must be relative to the root of
11+
# the working tree and separated by a single NUL.
12+
#
13+
#echo "$0 $*" >&2
14+
my ($version, $last_update_token) = @ARGV;
15+
16+
if ($version ne 2) {
17+
print "Unsupported query-fsmonitor hook version '$version'.\n";
18+
exit 1;
19+
}
20+
21+
print "last_update_token\0/\0"

t/t7519/fsmonitor-watchman

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,7 @@ if ($version == 1) {
2626
# subtract one second to make sure watchman will return all changes
2727
$time = int ($time / 1000000000) - 1;
2828
} else {
29-
die "Unsupported query-fsmonitor hook version '$version'.\n" .
30-
"Falling back to scanning...\n";
29+
exit 1;
3130
}
3231

3332
my $git_work_tree;

0 commit comments

Comments
 (0)