PageRenderTime 26ms CodeModel.GetById 23ms RepoModel.GetById 1ms app.codeStats 0ms

/src/com/facebook/buck/util/WatchmanWatcher.java

https://gitlab.com/smartether/buck
Java | 437 lines | 345 code | 46 blank | 46 comment | 44 complexity | 6139fb2af095cc0f5f0b83db187a2c17 MD5 | raw file
  1. /*
  2. * Copyright 2013-present Facebook, Inc.
  3. *
  4. * Licensed under the Apache License, Version 2.0 (the "License"); you may
  5. * not use this file except in compliance with the License. You may obtain
  6. * a copy of the License at
  7. *
  8. * http://www.apache.org/licenses/LICENSE-2.0
  9. *
  10. * Unless required by applicable law or agreed to in writing, software
  11. * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
  12. * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
  13. * License for the specific language governing permissions and limitations
  14. * under the License.
  15. */
  16. package com.facebook.buck.util;
  17. import com.facebook.buck.event.BuckEventBus;
  18. import com.facebook.buck.event.WatchmanStatusEvent;
  19. import com.facebook.buck.io.MorePaths;
  20. import com.facebook.buck.io.PathOrGlobMatcher;
  21. import com.facebook.buck.io.ProjectWatch;
  22. import com.facebook.buck.io.Watchman;
  23. import com.facebook.buck.io.Watchman.Capability;
  24. import com.facebook.buck.io.WatchmanClient;
  25. import com.facebook.buck.io.WatchmanCursor;
  26. import com.facebook.buck.io.WatchmanDiagnostic;
  27. import com.facebook.buck.io.WatchmanDiagnosticEvent;
  28. import com.facebook.buck.io.WatchmanQuery;
  29. import com.facebook.buck.log.Logger;
  30. import com.google.common.annotations.VisibleForTesting;
  31. import com.google.common.base.Preconditions;
  32. import com.google.common.collect.ImmutableList;
  33. import com.google.common.collect.ImmutableMap;
  34. import com.google.common.collect.ImmutableSet;
  35. import com.google.common.collect.Lists;
  36. import com.google.common.eventbus.EventBus;
  37. import java.io.File;
  38. import java.io.IOException;
  39. import java.nio.file.Path;
  40. import java.nio.file.Paths;
  41. import java.nio.file.StandardWatchEventKinds;
  42. import java.nio.file.WatchEvent;
  43. import java.util.LinkedHashMap;
  44. import java.util.List;
  45. import java.util.Map;
  46. import java.util.Optional;
  47. import java.util.Set;
  48. import java.util.concurrent.atomic.AtomicBoolean;
  49. import java.util.concurrent.TimeUnit;
  50. import javax.annotation.Nullable;
  51. /**
  52. * Queries Watchman for changes to a path.
  53. */
  54. public class WatchmanWatcher {
  55. // Action to take if Watchman indicates a fresh instance (which happens
  56. // both on the first buckd command as well as if Watchman needs to recrawl
  57. // for any reason).
  58. public enum FreshInstanceAction {
  59. NONE,
  60. POST_OVERFLOW_EVENT,
  61. ;
  62. };
  63. // The type of cursor used to communicate with Watchman
  64. public enum CursorType {
  65. NAMED,
  66. CLOCK_ID,
  67. };
  68. private static final Logger LOG = Logger.get(WatchmanWatcher.class);
  69. private static final long DEFAULT_TIMEOUT_MILLIS = TimeUnit.SECONDS.toMillis(10);
  70. private final EventBus fileChangeEventBus;
  71. private final WatchmanClient watchmanClient;
  72. private final ImmutableMap<Path, WatchmanQuery> queries;
  73. private Map<Path, WatchmanCursor> cursors;
  74. private final long timeoutMillis;
  75. public WatchmanWatcher(
  76. ImmutableMap<Path, ProjectWatch> projectWatch,
  77. EventBus fileChangeEventBus,
  78. ImmutableSet<PathOrGlobMatcher> ignorePaths,
  79. Watchman watchman,
  80. Map<Path, WatchmanCursor> cursors) {
  81. this(
  82. fileChangeEventBus,
  83. watchman.getWatchmanClient().get(),
  84. DEFAULT_TIMEOUT_MILLIS,
  85. createQueries(
  86. projectWatch,
  87. ignorePaths,
  88. watchman.getCapabilities()),
  89. cursors);
  90. }
  91. @VisibleForTesting
  92. WatchmanWatcher(EventBus fileChangeEventBus,
  93. WatchmanClient watchmanClient,
  94. long timeoutMillis,
  95. ImmutableMap<Path, WatchmanQuery> queries,
  96. Map<Path, WatchmanCursor> cursors) {
  97. this.fileChangeEventBus = fileChangeEventBus;
  98. this.watchmanClient = watchmanClient;
  99. this.timeoutMillis = timeoutMillis;
  100. this.queries = queries;
  101. this.cursors = cursors;
  102. }
  103. @VisibleForTesting
  104. static ImmutableMap<Path, WatchmanQuery> createQueries(
  105. ImmutableMap<Path, ProjectWatch> projectWatches,
  106. ImmutableSet<PathOrGlobMatcher> ignorePaths,
  107. Set<Capability> watchmanCapabilities) {
  108. ImmutableMap.Builder<Path, WatchmanQuery> watchmanQueryBuilder = ImmutableMap.builder();
  109. for (Map.Entry<Path, ProjectWatch> entry : projectWatches.entrySet()) {
  110. watchmanQueryBuilder.put(
  111. entry.getKey(),
  112. createQuery(entry.getValue(), ignorePaths, watchmanCapabilities));
  113. }
  114. return watchmanQueryBuilder.build();
  115. }
  116. @VisibleForTesting
  117. static WatchmanQuery createQuery(
  118. ProjectWatch projectWatch,
  119. ImmutableSet<PathOrGlobMatcher> ignorePaths,
  120. Set<Capability> watchmanCapabilities) {
  121. String watchRoot = projectWatch.getWatchRoot();
  122. Optional<String> watchPrefix = projectWatch.getProjectPrefix();
  123. // Exclude any expressions added to this list.
  124. List<Object> excludeAnyOf = Lists.newArrayList("anyof");
  125. // Exclude all directories.
  126. excludeAnyOf.add(Lists.newArrayList("type", "d"));
  127. Path projectRoot = Paths.get(watchRoot);
  128. projectRoot = projectRoot.resolve(watchPrefix.orElse(""));
  129. // Exclude all files under directories in project.ignorePaths.
  130. //
  131. // Note that it's OK to exclude .git in a query (event though it's
  132. // not currently OK to exclude .git in .watchmanconfig). This id
  133. // because watchman's .git cookie magic is done before the query
  134. // is applied.
  135. for (PathOrGlobMatcher ignorePathOrGlob : ignorePaths) {
  136. switch (ignorePathOrGlob.getType()) {
  137. case PATH:
  138. Path ignorePath = ignorePathOrGlob.getPath();
  139. if (ignorePath.isAbsolute()) {
  140. ignorePath = MorePaths.relativize(projectRoot, ignorePath);
  141. }
  142. if (watchmanCapabilities.contains(Capability.DIRNAME)) {
  143. excludeAnyOf.add(
  144. Lists.newArrayList(
  145. "dirname",
  146. ignorePath.toString()));
  147. } else {
  148. excludeAnyOf.add(
  149. Lists.newArrayList(
  150. "match",
  151. ignorePath.toString() + File.separator + "*",
  152. "wholename"));
  153. }
  154. break;
  155. case GLOB:
  156. String ignoreGlob = ignorePathOrGlob.getGlob();
  157. excludeAnyOf.add(
  158. Lists.newArrayList(
  159. "match",
  160. ignoreGlob,
  161. "wholename",
  162. ImmutableMap.of("includedotfiles", true)));
  163. break;
  164. default:
  165. throw new RuntimeException(
  166. String.format("Unsupported type: '%s'", ignorePathOrGlob.getType()));
  167. }
  168. }
  169. // Note that we use LinkedHashMap so insertion order is preserved. That
  170. // helps us write tests that don't depend on the undefined order of HashMap.
  171. Map<String, Object> sinceParams = new LinkedHashMap<>();
  172. sinceParams.put(
  173. "expression",
  174. Lists.newArrayList(
  175. "not",
  176. excludeAnyOf));
  177. sinceParams.put("empty_on_fresh_instance", true);
  178. sinceParams.put("fields", Lists.newArrayList("name", "exists", "new"));
  179. if (watchPrefix.isPresent()) {
  180. sinceParams.put("relative_root", watchPrefix.get());
  181. }
  182. return WatchmanQuery.of(watchRoot, sinceParams);
  183. }
  184. @VisibleForTesting
  185. ImmutableList<Object> getWatchmanQuery(Path cellPath) {
  186. if (queries.containsKey(cellPath) && cursors.containsKey(cellPath)) {
  187. return queries.get(cellPath).toList(cursors.get(cellPath).get());
  188. }
  189. return ImmutableList.of();
  190. }
  191. /**
  192. * Query Watchman for file change events. If too many events are pending or an error occurs
  193. * an overflow event is posted to the EventBus signalling that events may have been lost
  194. * (and so typically caches must be cleared to avoid inconsistency). Interruptions and
  195. * IOExceptions are propagated to callers, but typically if overflow events are handled
  196. * conservatively by subscribers then no other remedial action is required.
  197. *
  198. * Any diagnostics posted by Watchman are added to watchmanDiagnosticCache.
  199. */
  200. public void postEvents(
  201. BuckEventBus buckEventBus,
  202. FreshInstanceAction freshInstanceAction
  203. ) throws IOException, InterruptedException {
  204. // Speculatively set to false
  205. AtomicBoolean filesHaveChanged = new AtomicBoolean(false);
  206. for (Path cellPath : queries.keySet()) {
  207. WatchmanQuery query = queries.get(cellPath);
  208. WatchmanCursor cursor = cursors.get(cellPath);
  209. if (query != null && cursor != null) {
  210. postEvents(buckEventBus, freshInstanceAction, query, cursor, filesHaveChanged);
  211. }
  212. }
  213. if (!filesHaveChanged.get()) {
  214. buckEventBus.post(WatchmanStatusEvent.zeroFileChanges());
  215. }
  216. }
  217. @SuppressWarnings("unchecked")
  218. private void postEvents(
  219. BuckEventBus buckEventBus,
  220. FreshInstanceAction freshInstanceAction,
  221. WatchmanQuery query,
  222. WatchmanCursor cursor,
  223. AtomicBoolean filesHaveChanged) throws IOException, InterruptedException {
  224. try {
  225. Optional<? extends Map<String, ? extends Object>> queryResponse =
  226. watchmanClient.queryWithTimeout(
  227. TimeUnit.MILLISECONDS.toNanos(timeoutMillis),
  228. query.toList(cursor.get()).toArray());
  229. if (!queryResponse.isPresent()) {
  230. LOG.warn(
  231. "Could not get response from Watchman for query %s within %d ms",
  232. query,
  233. timeoutMillis);
  234. postWatchEvent(
  235. createOverflowEvent("Query to Watchman timed out after " + timeoutMillis + "ms"));
  236. filesHaveChanged.set(true);
  237. return;
  238. }
  239. Map<String, ? extends Object> response = queryResponse.get();
  240. String error = (String) response.get("error");
  241. if (error != null) {
  242. // This message is not de-duplicated via WatchmanDiagnostic.
  243. WatchmanWatcherException e = new WatchmanWatcherException(error);
  244. LOG.error(
  245. e,
  246. "Error in Watchman output. Posting an overflow event to flush the caches");
  247. postWatchEvent(createOverflowEvent("Watchman Error occurred - " + e.getMessage()));
  248. throw e;
  249. }
  250. if (cursor.get().startsWith("c:")) {
  251. // Update the clockId
  252. String newCursor = Optional
  253. .ofNullable((String) response.get("clock"))
  254. .orElse(Watchman.NULL_CLOCK);
  255. LOG.debug("Updating Watchman Cursor from %s to %s", cursor.get(), newCursor);
  256. cursor.set(newCursor);
  257. }
  258. String warning = (String) response.get("warning");
  259. if (warning != null) {
  260. buckEventBus.post(
  261. new WatchmanDiagnosticEvent(
  262. WatchmanDiagnostic.of(WatchmanDiagnostic.Level.WARNING, warning)));
  263. }
  264. Boolean isFreshInstance = (Boolean) response.get("is_fresh_instance");
  265. if (isFreshInstance != null && isFreshInstance) {
  266. LOG.debug(
  267. "Watchman indicated a fresh instance (fresh instance action %s)",
  268. freshInstanceAction);
  269. switch (freshInstanceAction) {
  270. case NONE:
  271. break;
  272. case POST_OVERFLOW_EVENT:
  273. postWatchEvent(createOverflowEvent("New Buck instance"));
  274. break;
  275. }
  276. filesHaveChanged.set(true);
  277. return;
  278. }
  279. List<Map<String, Object>> files = (List<Map<String, Object>>) response.get("files");
  280. if (files != null) {
  281. for (Map<String, Object> file : files) {
  282. String fileName = (String) file.get("name");
  283. if (fileName == null) {
  284. LOG.warn("Filename missing from Watchman file response %s", file);
  285. postWatchEvent(createOverflowEvent("Filename missing from Watchman response"));
  286. filesHaveChanged.set(true);
  287. return;
  288. }
  289. PathEventBuilder builder = new PathEventBuilder();
  290. builder.setPath(Paths.get(fileName));
  291. Boolean fileNew = (Boolean) file.get("new");
  292. if (fileNew != null && fileNew) {
  293. builder.setCreationEvent();
  294. }
  295. Boolean fileExists = (Boolean) file.get("exists");
  296. if (fileExists != null && !fileExists) {
  297. builder.setDeletionEvent();
  298. }
  299. postWatchEvent(builder.build());
  300. }
  301. if (!files.isEmpty() || freshInstanceAction == FreshInstanceAction.NONE) {
  302. filesHaveChanged.set(true);
  303. }
  304. LOG.debug("Posted %d Watchman events.", files.size());
  305. } else {
  306. if (freshInstanceAction == FreshInstanceAction.NONE) {
  307. filesHaveChanged.set(true);
  308. }
  309. }
  310. } catch (InterruptedException e) {
  311. String message = "Watchman communication interrupted";
  312. LOG.warn(e, message);
  313. // Events may have been lost, signal overflow.
  314. postWatchEvent(createOverflowEvent(message));
  315. Thread.currentThread().interrupt();
  316. throw e;
  317. } catch (IOException e) {
  318. String message = "I/O error talking to Watchman";
  319. LOG.error(e, message);
  320. // Events may have been lost, signal overflow.
  321. postWatchEvent(createOverflowEvent(message + " - " + e.getMessage()));
  322. throw e;
  323. }
  324. }
  325. private void postWatchEvent(WatchEvent<?> event) {
  326. LOG.warn("Posting WatchEvent: %s", event);
  327. fileChangeEventBus.post(event);
  328. }
  329. @VisibleForTesting
  330. public static WatchEvent<Object> createOverflowEvent(final String reason) {
  331. return new WatchEvent<Object>() {
  332. @Override
  333. public Kind<Object> kind() {
  334. return StandardWatchEventKinds.OVERFLOW;
  335. }
  336. @Override
  337. public int count() {
  338. return 1;
  339. }
  340. @Override
  341. @Nullable
  342. public Object context() {
  343. return reason;
  344. }
  345. @Override
  346. public String toString() {
  347. return "Watchman Overflow WatchEvent " + kind();
  348. }
  349. };
  350. }
  351. private static class PathEventBuilder {
  352. private WatchEvent.Kind<Path> kind;
  353. @Nullable private Path path;
  354. PathEventBuilder() {
  355. this.kind = StandardWatchEventKinds.ENTRY_MODIFY;
  356. }
  357. public void setCreationEvent() {
  358. if (kind != StandardWatchEventKinds.ENTRY_DELETE) {
  359. kind = StandardWatchEventKinds.ENTRY_CREATE;
  360. }
  361. }
  362. public void setDeletionEvent() {
  363. kind = StandardWatchEventKinds.ENTRY_DELETE;
  364. }
  365. public void setPath(Path path) {
  366. this.path = path;
  367. }
  368. public WatchEvent<Path> build() {
  369. Preconditions.checkNotNull(path);
  370. return new WatchEvent<Path>() {
  371. @Override
  372. public Kind<Path> kind() {
  373. return kind;
  374. }
  375. @Override
  376. public int count() {
  377. return 1;
  378. }
  379. @Override
  380. @Nullable
  381. public Path context() {
  382. return path;
  383. }
  384. @Override
  385. public String toString() {
  386. return "Watchman Path WatchEvent " + kind + " " + path;
  387. }
  388. };
  389. }
  390. }
  391. }