| b69ab31 | | | 1 | /** |
| b69ab31 | | | 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. |
| b69ab31 | | | 3 | * |
| b69ab31 | | | 4 | * This source code is licensed under the MIT license found in the |
| b69ab31 | | | 5 | * LICENSE file in the root directory of this source tree. |
| b69ab31 | | | 6 | */ |
| b69ab31 | | | 7 | |
| b69ab31 | | | 8 | import type {Logger} from './logger'; |
| b69ab31 | | | 9 | |
| b69ab31 | | | 10 | import watchman from 'fb-watchman'; |
| b69ab31 | | | 11 | import {EventEmitter} from 'node:events'; |
| b69ab31 | | | 12 | import path from 'node:path'; |
| b69ab31 | | | 13 | import {type ServerSideTracker} from './analytics/serverSideTracker'; |
| b69ab31 | | | 14 | import {firstOfIterable, serializeAsyncCall, sleep} from './utils'; |
| b69ab31 | | | 15 | |
| b69ab31 | | | 16 | export type WatchmanSubscriptionOptions = { |
| b69ab31 | | | 17 | fields?: Array<string>; |
| b69ab31 | | | 18 | expression?: Array<unknown>; |
| b69ab31 | | | 19 | since?: string; |
| b69ab31 | | | 20 | defer?: Array<string>; |
| b69ab31 | | | 21 | defer_vcs?: boolean; |
| b69ab31 | | | 22 | relative_root?: string; |
| b69ab31 | | | 23 | empty_on_fresh_instance?: boolean; |
| b69ab31 | | | 24 | }; |
| b69ab31 | | | 25 | |
| b69ab31 | | | 26 | export type WatchmanSubscription = { |
| b69ab31 | | | 27 | root: string; |
| b69ab31 | | | 28 | /** |
| b69ab31 | | | 29 | * The relative path from subscriptionRoot to subscriptionPath. |
| b69ab31 | | | 30 | * This is the 'relative_path' as described at |
| b69ab31 | | | 31 | * https://facebook.github.io/watchman/docs/cmd/watch-project.html#using-watch-project. |
| b69ab31 | | | 32 | * Notably, this value should be undefined if subscriptionRoot is the same as |
| b69ab31 | | | 33 | * subscriptionPath. |
| b69ab31 | | | 34 | */ |
| b69ab31 | | | 35 | pathFromSubscriptionRootToSubscriptionPath: string | undefined; |
| b69ab31 | | | 36 | path: string; |
| b69ab31 | | | 37 | name: string; |
| b69ab31 | | | 38 | subscriptionCount: number; |
| b69ab31 | | | 39 | options: WatchmanSubscriptionOptions; |
| b69ab31 | | | 40 | emitter: EventEmitter; |
| b69ab31 | | | 41 | }; |
| b69ab31 | | | 42 | |
| b69ab31 | | | 43 | export type WatchmanSubscriptionResponse = { |
| b69ab31 | | | 44 | root: string; |
| b69ab31 | | | 45 | subscription: string; |
| b69ab31 | | | 46 | files?: Array<FileChange>; |
| b69ab31 | | | 47 | 'state-enter'?: string; |
| b69ab31 | | | 48 | 'state-leave'?: string; |
| b69ab31 | | | 49 | canceled?: boolean; |
| b69ab31 | | | 50 | clock?: string; |
| b69ab31 | | | 51 | is_fresh_instance?: boolean; |
| b69ab31 | | | 52 | }; |
| b69ab31 | | | 53 | |
| b69ab31 | | | 54 | export type FileChange = { |
| b69ab31 | | | 55 | name: string; |
| b69ab31 | | | 56 | new: boolean; |
| b69ab31 | | | 57 | exists: boolean; |
| b69ab31 | | | 58 | }; |
| b69ab31 | | | 59 | |
| b69ab31 | | | 60 | const WATCHMAN_SETTLE_TIME_MS = 2500; |
| b69ab31 | | | 61 | const DEFAULT_WATCHMAN_RECONNECT_DELAY_MS = 100; |
| b69ab31 | | | 62 | const MAXIMUM_WATCHMAN_RECONNECT_DELAY_MS = 60 * 1000; |
| b69ab31 | | | 63 | |
| b69ab31 | | | 64 | export class Watchman { |
| b69ab31 | | | 65 | private client: watchman.Client; |
| b69ab31 | | | 66 | |
| b69ab31 | | | 67 | private serializedReconnect: () => Promise<void>; |
| b69ab31 | | | 68 | private reconnectDelayMs: number = DEFAULT_WATCHMAN_RECONNECT_DELAY_MS; |
| b69ab31 | | | 69 | private subscriptions: Map<string, WatchmanSubscription> = new Map(); |
| b69ab31 | | | 70 | private lastKnownClockTimes: Map<string, string> = new Map(); |
| b69ab31 | | | 71 | |
| b69ab31 | | | 72 | public readonly status: 'initializing' | 'reconnecting' | 'healthy' | 'ended' | 'errored' = |
| b69ab31 | | | 73 | 'initializing'; |
| b69ab31 | | | 74 | |
| 4fe1f34 | | | 75 | public onStatusChange: ((status: typeof this.status) => void) | undefined; |
| 4fe1f34 | | | 76 | |
| b69ab31 | | | 77 | constructor( |
| b69ab31 | | | 78 | private logger: Logger, |
| b69ab31 | | | 79 | private tracker: ServerSideTracker, |
| b69ab31 | | | 80 | ) { |
| b69ab31 | | | 81 | this.client = new watchman.Client({ |
| b69ab31 | | | 82 | // find watchman using PATH |
| b69ab31 | | | 83 | watchmanBinaryPath: undefined, |
| b69ab31 | | | 84 | }); |
| b69ab31 | | | 85 | this.initWatchmanClient(); |
| b69ab31 | | | 86 | this.serializedReconnect = serializeAsyncCall(async () => { |
| b69ab31 | | | 87 | let tries = 0; |
| b69ab31 | | | 88 | while (true) { |
| b69ab31 | | | 89 | try { |
| b69ab31 | | | 90 | // eslint-disable-next-line no-await-in-loop |
| b69ab31 | | | 91 | await this.reconnectClient(); |
| b69ab31 | | | 92 | return; |
| b69ab31 | | | 93 | } catch (error) { |
| b69ab31 | | | 94 | this.logger.warn( |
| b69ab31 | | | 95 | `reconnectClient failed (try #${tries}):`, |
| b69ab31 | | | 96 | error instanceof Error ? error.message : error, |
| b69ab31 | | | 97 | ); |
| b69ab31 | | | 98 | tries++; |
| b69ab31 | | | 99 | |
| b69ab31 | | | 100 | this.reconnectDelayMs *= 2; // exponential backoff |
| b69ab31 | | | 101 | if (this.reconnectDelayMs > MAXIMUM_WATCHMAN_RECONNECT_DELAY_MS) { |
| b69ab31 | | | 102 | this.reconnectDelayMs = MAXIMUM_WATCHMAN_RECONNECT_DELAY_MS; |
| b69ab31 | | | 103 | } |
| b69ab31 | | | 104 | |
| b69ab31 | | | 105 | this.logger.info( |
| b69ab31 | | | 106 | 'Calling reconnectClient from _serializedReconnect in %dms', |
| b69ab31 | | | 107 | this.reconnectDelayMs, |
| b69ab31 | | | 108 | ); |
| b69ab31 | | | 109 | // eslint-disable-next-line no-await-in-loop |
| b69ab31 | | | 110 | await sleep(this.reconnectDelayMs); |
| b69ab31 | | | 111 | } |
| b69ab31 | | | 112 | } |
| b69ab31 | | | 113 | }); |
| b69ab31 | | | 114 | } |
| b69ab31 | | | 115 | |
| b69ab31 | | | 116 | setStatus(status: typeof this.status): void { |
| b69ab31 | | | 117 | this.logger.log('Watchman status: ', status); |
| b69ab31 | | | 118 | (this.status as string) = status; |
| 4fe1f34 | | | 119 | this.onStatusChange?.(status); |
| b69ab31 | | | 120 | } |
| b69ab31 | | | 121 | |
| b69ab31 | | | 122 | public async watchDirectoryRecursive( |
| b69ab31 | | | 123 | localDirectoryPath: string, |
| b69ab31 | | | 124 | rawSubscriptionName: string, |
| b69ab31 | | | 125 | subscriptionOptions?: WatchmanSubscriptionOptions, |
| b69ab31 | | | 126 | ): Promise<WatchmanSubscription> { |
| b69ab31 | | | 127 | // Subscriptions should be unique by name and by folder |
| b69ab31 | | | 128 | const subscriptionName = this.fixupName(localDirectoryPath, rawSubscriptionName); |
| b69ab31 | | | 129 | const existingSubscription = this.getSubscription(subscriptionName); |
| b69ab31 | | | 130 | if (existingSubscription) { |
| b69ab31 | | | 131 | existingSubscription.subscriptionCount++; |
| b69ab31 | | | 132 | |
| b69ab31 | | | 133 | return existingSubscription; |
| b69ab31 | | | 134 | } else { |
| b69ab31 | | | 135 | const {watch: watchRoot, relative_path: relativePath} = |
| b69ab31 | | | 136 | await this.watchProject(localDirectoryPath); |
| b69ab31 | | | 137 | const clock = await this.clock(watchRoot); |
| b69ab31 | | | 138 | const options: WatchmanSubscriptionOptions = { |
| b69ab31 | | | 139 | ...subscriptionOptions, |
| b69ab31 | | | 140 | // Do not add `mode` here, it is very unfriendly to watches on Eden (see https://fburl.com/0z023yy0) |
| b69ab31 | | | 141 | fields: |
| b69ab31 | | | 142 | subscriptionOptions != null && subscriptionOptions.fields != null |
| b69ab31 | | | 143 | ? subscriptionOptions.fields |
| b69ab31 | | | 144 | : ['name', 'new', 'exists'], |
| b69ab31 | | | 145 | since: clock, |
| b69ab31 | | | 146 | }; |
| b69ab31 | | | 147 | if (relativePath) { |
| b69ab31 | | | 148 | options.relative_root = relativePath; |
| b69ab31 | | | 149 | } |
| b69ab31 | | | 150 | // Try this thing out where we always set empty_on_fresh_instance. Eden will be a lot happier |
| b69ab31 | | | 151 | // if we never ask Watchman to do something that results in a glob(**) near the root. |
| b69ab31 | | | 152 | options.empty_on_fresh_instance = true; |
| b69ab31 | | | 153 | |
| b69ab31 | | | 154 | // relativePath is undefined if watchRoot is the same as directoryPath. |
| b69ab31 | | | 155 | const subscription: WatchmanSubscription = { |
| b69ab31 | | | 156 | root: watchRoot, |
| b69ab31 | | | 157 | pathFromSubscriptionRootToSubscriptionPath: relativePath, |
| b69ab31 | | | 158 | path: localDirectoryPath, |
| b69ab31 | | | 159 | name: subscriptionName, |
| b69ab31 | | | 160 | subscriptionCount: 1, |
| b69ab31 | | | 161 | options, |
| b69ab31 | | | 162 | emitter: new EventEmitter(), |
| b69ab31 | | | 163 | }; |
| b69ab31 | | | 164 | this.setSubscription(subscriptionName, subscription); |
| b69ab31 | | | 165 | await this.subscribe(watchRoot, subscriptionName, options); |
| b69ab31 | | | 166 | this.logger.log('watchman subscription %s established', subscriptionName); |
| b69ab31 | | | 167 | this.setStatus('healthy'); |
| b69ab31 | | | 168 | |
| b69ab31 | | | 169 | return subscription; |
| b69ab31 | | | 170 | } |
| b69ab31 | | | 171 | } |
| b69ab31 | | | 172 | |
| b69ab31 | | | 173 | public async unwatch(path: string, name: string): Promise<void> { |
| b69ab31 | | | 174 | const subscriptionName = this.fixupName(path, name); |
| b69ab31 | | | 175 | const subscription = this.getSubscription(subscriptionName); |
| b69ab31 | | | 176 | |
| b69ab31 | | | 177 | if (subscription == null) { |
| b69ab31 | | | 178 | this.logger.error(`No watcher entity found with path [${path}] name [${name}]`); |
| b69ab31 | | | 179 | return; |
| b69ab31 | | | 180 | } |
| b69ab31 | | | 181 | |
| b69ab31 | | | 182 | if (--subscription.subscriptionCount === 0) { |
| b69ab31 | | | 183 | await this.unsubscribe(subscription.path, subscription.name).catch(err => { |
| b69ab31 | | | 184 | // Directory maybe doesn't exist anymore. Don't propagate error - we want to clean |
| b69ab31 | | | 185 | // up subscription anyway so we don't get stuck reconnecting. |
| b69ab31 | | | 186 | this.logger.error(`error unsubscribing watchman subscription ${subscriptionName}: ${err}`); |
| b69ab31 | | | 187 | }); |
| b69ab31 | | | 188 | this.deleteSubscription(subscriptionName); |
| b69ab31 | | | 189 | this.logger.log(`watchman subscription destroyed: ${subscriptionName}`); |
| b69ab31 | | | 190 | } |
| b69ab31 | | | 191 | } |
| b69ab31 | | | 192 | |
| b69ab31 | | | 193 | private initWatchmanClient(): void { |
| b69ab31 | | | 194 | this.client.on('end', () => { |
| b69ab31 | | | 195 | this.setStatus('ended'); |
| b69ab31 | | | 196 | this.logger.info('Watchman client ended'); |
| b69ab31 | | | 197 | this.client.removeAllListeners(); |
| b69ab31 | | | 198 | this.serializedReconnect(); |
| b69ab31 | | | 199 | }); |
| b69ab31 | | | 200 | this.client.on('error', (error: Error) => { |
| b69ab31 | | | 201 | const statusBeforeError = this.status; |
| b69ab31 | | | 202 | this.logger.error('Error while talking to watchman: ', error); |
| b69ab31 | | | 203 | this.tracker.error( |
| b69ab31 | | | 204 | 'WatchmanEvent', |
| b69ab31 | | | 205 | 'WatchmanError', |
| b69ab31 | | | 206 | `Error while talking to watchman ${error}`, |
| b69ab31 | | | 207 | ); |
| b69ab31 | | | 208 | this.setStatus('errored'); |
| b69ab31 | | | 209 | // If Watchman encounters an error in the middle of a command, it may never finish! |
| b69ab31 | | | 210 | // The client must be immediately killed here so that the command fails and |
| b69ab31 | | | 211 | // `serializeAsyncCall` can be unblocked. Otherwise, we end up in a deadlock. |
| b69ab31 | | | 212 | this.client.removeAllListeners(); |
| b69ab31 | | | 213 | this.client.end(); |
| b69ab31 | | | 214 | if (statusBeforeError === 'initializing') { |
| b69ab31 | | | 215 | // If we get an error while we're first initializing watchman, it probably means |
| b69ab31 | | | 216 | // it's not installed properly. No use spamming reconnection failures too. |
| b69ab31 | | | 217 | return; |
| b69ab31 | | | 218 | } |
| b69ab31 | | | 219 | // Those are errors in deserializing a stream of changes. |
| b69ab31 | | | 220 | // The only possible recovery here is reconnecting a new client, |
| b69ab31 | | | 221 | // but the failed to serialize events will be missed. |
| b69ab31 | | | 222 | this.serializedReconnect(); |
| b69ab31 | | | 223 | }); |
| b69ab31 | | | 224 | this.client.on('subscription', this.onSubscriptionResult.bind(this)); |
| b69ab31 | | | 225 | } |
| b69ab31 | | | 226 | |
| b69ab31 | | | 227 | private async reconnectClient(): Promise<void> { |
| b69ab31 | | | 228 | // If we got an error after making a subscription, the reconnect needs to |
| b69ab31 | | | 229 | // remove that subscription to try again, so it doesn't keep leaking subscriptions. |
| b69ab31 | | | 230 | this.logger.info('Ending existing watchman client to reconnect a new one'); |
| b69ab31 | | | 231 | this.setStatus('reconnecting'); |
| b69ab31 | | | 232 | this.client.removeAllListeners(); |
| b69ab31 | | | 233 | this.client.end(); |
| b69ab31 | | | 234 | this.client = new watchman.Client({ |
| b69ab31 | | | 235 | // find watchman using PATH |
| b69ab31 | | | 236 | watchmanBinaryPath: undefined, |
| b69ab31 | | | 237 | }); |
| b69ab31 | | | 238 | this.logger.error('Watchman client disconnected, reconnecting a new client!'); |
| b69ab31 | | | 239 | this.initWatchmanClient(); |
| b69ab31 | | | 240 | this.logger.info('Watchman client re-initialized, restoring subscriptions'); |
| b69ab31 | | | 241 | await this.restoreSubscriptions(); |
| b69ab31 | | | 242 | } |
| b69ab31 | | | 243 | |
| b69ab31 | | | 244 | private async restoreSubscriptions(): Promise<void> { |
| b69ab31 | | | 245 | const watchSubscriptions = Array.from(this.subscriptions.values()); |
| b69ab31 | | | 246 | const numSubscriptions = watchSubscriptions.length; |
| b69ab31 | | | 247 | this.logger.info(`Attempting to restore ${numSubscriptions} Watchman subscriptions.`); |
| b69ab31 | | | 248 | let numRestored = 0; |
| b69ab31 | | | 249 | await Promise.all( |
| b69ab31 | | | 250 | watchSubscriptions.map(async (subscription: WatchmanSubscription, index: number) => { |
| b69ab31 | | | 251 | // Note that this call to `watchman watch-project` could fail if the |
| b69ab31 | | | 252 | // subscription.path has been unmounted/deleted. |
| b69ab31 | | | 253 | await this.watchProject(subscription.path); |
| b69ab31 | | | 254 | |
| b69ab31 | | | 255 | // We have already missed the change events from the disconnect time, |
| b69ab31 | | | 256 | // watchman could have died, so the last clock result is not valid. |
| b69ab31 | | | 257 | await sleep(WATCHMAN_SETTLE_TIME_MS); |
| b69ab31 | | | 258 | |
| b69ab31 | | | 259 | // Register the subscriptions after the filesystem settles. |
| b69ab31 | | | 260 | const {name, options, root} = subscription; |
| b69ab31 | | | 261 | |
| b69ab31 | | | 262 | // Assuming we had previously connected and gotten an event, we can |
| b69ab31 | | | 263 | // reconnect `since` that time, so that we get any events we missed. |
| b69ab31 | | | 264 | subscription.options.since = this.lastKnownClockTimes.get(root) || (await this.clock(root)); |
| b69ab31 | | | 265 | |
| b69ab31 | | | 266 | this.logger.info(`Subscribing to ${name}: (${index + 1}/${numSubscriptions})`); |
| b69ab31 | | | 267 | await this.subscribe(root, name, options); |
| b69ab31 | | | 268 | ++numRestored; |
| b69ab31 | | | 269 | this.logger.info(`Subscribed to ${name}: (${numRestored}/${numSubscriptions}) complete.`); |
| b69ab31 | | | 270 | }), |
| b69ab31 | | | 271 | ); |
| b69ab31 | | | 272 | if (numRestored > 0 && numRestored === numSubscriptions) { |
| b69ab31 | | | 273 | this.logger.info('Successfully reconnected all %d subscriptions.', numRestored); |
| b69ab31 | | | 274 | // if everything got restored, reset the reconnect backoff time |
| b69ab31 | | | 275 | this.reconnectDelayMs = DEFAULT_WATCHMAN_RECONNECT_DELAY_MS; |
| b69ab31 | | | 276 | this.setStatus('healthy'); |
| b69ab31 | | | 277 | } |
| b69ab31 | | | 278 | } |
| b69ab31 | | | 279 | |
| b69ab31 | | | 280 | private getSubscription(entryPath: string): WatchmanSubscription | undefined { |
| b69ab31 | | | 281 | return this.subscriptions.get(path.normalize(entryPath)); |
| b69ab31 | | | 282 | } |
| b69ab31 | | | 283 | |
| b69ab31 | | | 284 | private setSubscription(entryPath: string, subscription: WatchmanSubscription): void { |
| b69ab31 | | | 285 | const key = path.normalize(entryPath); |
| b69ab31 | | | 286 | this.subscriptions.set(key, subscription); |
| b69ab31 | | | 287 | } |
| b69ab31 | | | 288 | |
| b69ab31 | | | 289 | private deleteSubscription(entryPath: string): void { |
| b69ab31 | | | 290 | const key = path.normalize(entryPath); |
| b69ab31 | | | 291 | this.subscriptions.delete(key); |
| b69ab31 | | | 292 | } |
| b69ab31 | | | 293 | |
| b69ab31 | | | 294 | private onSubscriptionResult(response: WatchmanSubscriptionResponse): void { |
| b69ab31 | | | 295 | const subscription = this.getSubscription(response.subscription); |
| b69ab31 | | | 296 | if (subscription == null) { |
| b69ab31 | | | 297 | this.logger.error('Subscription not found for response:!', response); |
| b69ab31 | | | 298 | return; |
| b69ab31 | | | 299 | } |
| b69ab31 | | | 300 | |
| b69ab31 | | | 301 | // save the clock time of this event in case we disconnect in the future |
| b69ab31 | | | 302 | if (response != null && response.root != null && response.clock != null) { |
| b69ab31 | | | 303 | this.lastKnownClockTimes.set(response.root, response.clock); |
| b69ab31 | | | 304 | } |
| b69ab31 | | | 305 | if (response.is_fresh_instance === true) { |
| b69ab31 | | | 306 | this.logger.warn( |
| b69ab31 | | | 307 | `Watch for ${response.root} (${response.subscription}) returned an empty fresh instance.`, |
| b69ab31 | | | 308 | ); |
| b69ab31 | | | 309 | subscription.emitter.emit('fresh-instance'); |
| b69ab31 | | | 310 | } else if (Array.isArray(response.files)) { |
| b69ab31 | | | 311 | subscription.emitter.emit('change', response.files); |
| b69ab31 | | | 312 | } else if (response.canceled === true) { |
| b69ab31 | | | 313 | this.logger.info(`Watch for ${response.root} was deleted: triggering a reconnect.`); |
| b69ab31 | | | 314 | // Ending the client will trigger a reconnect. |
| b69ab31 | | | 315 | this.client.end(); |
| b69ab31 | | | 316 | } else { |
| b69ab31 | | | 317 | // Only log state transitions once per watchmanClient, since otherwise we'll just get 8x of this message spammed |
| b69ab31 | | | 318 | if (firstOfIterable(this.subscriptions.values()) === subscription) { |
| b69ab31 | | | 319 | // TODO(most): use state messages to decide on when to send updates. |
| b69ab31 | | | 320 | const stateEnter = response['state-enter']; |
| b69ab31 | | | 321 | const stateLeave = response['state-leave']; |
| b69ab31 | | | 322 | const stateMessage = |
| b69ab31 | | | 323 | stateEnter != null ? `Entering ${stateEnter}` : `Leaving ${stateLeave}`; |
| b69ab31 | | | 324 | const numSubscriptions = this.subscriptions.size; |
| b69ab31 | | | 325 | this.logger.info(`Subscription state: ${stateMessage} (${numSubscriptions})`); |
| b69ab31 | | | 326 | } |
| b69ab31 | | | 327 | } |
| b69ab31 | | | 328 | } |
| b69ab31 | | | 329 | |
| b69ab31 | | | 330 | private fixupName(path: string, name: string): string { |
| b69ab31 | | | 331 | const refinedPath = path.replace(/\\/g, '-').replace(/\//g, '-'); |
| b69ab31 | | | 332 | return `${refinedPath}-${name}`; |
| b69ab31 | | | 333 | } |
| b69ab31 | | | 334 | |
| b69ab31 | | | 335 | private unsubscribe(subscriptionPath: string, subscriptionName: string): Promise<unknown> { |
| b69ab31 | | | 336 | return this.command('unsubscribe', subscriptionPath, subscriptionName); |
| b69ab31 | | | 337 | } |
| b69ab31 | | | 338 | |
| b69ab31 | | | 339 | private async watchProject( |
| b69ab31 | | | 340 | directoryPath: string, |
| b69ab31 | | | 341 | ): Promise<{watch: string; relative_path: string}> { |
| b69ab31 | | | 342 | const response = (await this.command('watch-project', directoryPath)) as { |
| b69ab31 | | | 343 | watch: string; |
| b69ab31 | | | 344 | relative_path: string; |
| b69ab31 | | | 345 | warning?: string; |
| b69ab31 | | | 346 | }; |
| b69ab31 | | | 347 | if (response.warning) { |
| b69ab31 | | | 348 | this.logger.error('watchman warning: ', response.warning); |
| b69ab31 | | | 349 | } |
| b69ab31 | | | 350 | return response; |
| b69ab31 | | | 351 | } |
| b69ab31 | | | 352 | |
| b69ab31 | | | 353 | private async clock(directoryPath: string): Promise<string> { |
| b69ab31 | | | 354 | const {clock} = (await this.command('clock', directoryPath)) as {clock: string}; |
| b69ab31 | | | 355 | return clock; |
| b69ab31 | | | 356 | } |
| b69ab31 | | | 357 | |
| b69ab31 | | | 358 | private subscribe( |
| b69ab31 | | | 359 | watchRoot: string, |
| b69ab31 | | | 360 | subscriptionName: string | undefined, |
| b69ab31 | | | 361 | options: WatchmanSubscriptionOptions, |
| b69ab31 | | | 362 | ): Promise<WatchmanSubscription> { |
| b69ab31 | | | 363 | this.logger.info( |
| b69ab31 | | | 364 | `Creating Watchman subscription ${String(subscriptionName)} under ${watchRoot}`, |
| b69ab31 | | | 365 | JSON.stringify(options), |
| b69ab31 | | | 366 | ); |
| b69ab31 | | | 367 | return this.command( |
| b69ab31 | | | 368 | 'subscribe', |
| b69ab31 | | | 369 | watchRoot, |
| b69ab31 | | | 370 | subscriptionName, |
| b69ab31 | | | 371 | options, |
| b69ab31 | | | 372 | ) as Promise<WatchmanSubscription>; |
| b69ab31 | | | 373 | } |
| b69ab31 | | | 374 | |
| b69ab31 | | | 375 | /** |
| b69ab31 | | | 376 | * Promisify calls to watchman client. |
| b69ab31 | | | 377 | */ |
| b69ab31 | | | 378 | private async command(...args: Array<unknown>): Promise<unknown> { |
| b69ab31 | | | 379 | try { |
| b69ab31 | | | 380 | return await new Promise((resolve, reject) => { |
| b69ab31 | | | 381 | this.client.command(args, (error, response) => (error ? reject(error) : resolve(response))); |
| b69ab31 | | | 382 | }); |
| b69ab31 | | | 383 | } catch (error) { |
| b69ab31 | | | 384 | this.logger.error('Watchman command error: ', args, error); |
| b69ab31 | | | 385 | throw error; |
| b69ab31 | | | 386 | } |
| b69ab31 | | | 387 | } |
| b69ab31 | | | 388 | } |