Skip to content

Commit 0cc2552

Browse files
ndmitchellfacebook-github-bot
authored andcommitted
Move Watchman integration into the library
Summary: Want things using open source to be able to use watchman as well. Reviewed By: SamChou19815 Differential Revision: D73776774 fbshipit-source-id: 10225d73087068423d813f899ab2926dff4bd5ba
1 parent 8208a12 commit 0cc2552

File tree

4 files changed

+171
-0
lines changed

4 files changed

+171
-0
lines changed

pyrefly/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ tar = "0.4.44"
7575
zstd = { version = "0.13", features = ["experimental", "zstdmt"] }
7676

7777
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
78+
watchman_client = "0.9.0"
7879
which = "4.2.4"
7980
zstd = { version = "0.13", features = ["experimental", "zstdmt"] }
8081

pyrefly/lib/lib.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,10 +77,14 @@ pub mod library {
7777
pub use crate::util::args::clap_env;
7878
pub use crate::util::args::get_args_expanded;
7979
pub use crate::util::globs;
80+
#[cfg(not(target_arch = "wasm32"))]
8081
pub use crate::util::notify_watcher::NotifyWatcher;
8182
pub use crate::util::thread_pool::init_thread_pool;
8283
pub use crate::util::trace::init_tracing;
84+
#[cfg(not(target_arch = "wasm32"))]
8385
pub use crate::util::watcher::Watcher;
86+
#[cfg(not(target_arch = "wasm32"))]
87+
pub use crate::util::watchman::Watchman;
8488
}
8589
}
8690
}

pyrefly/lib/util/mod.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ pub mod lock;
2020
pub mod locked_map;
2121
pub mod memory;
2222
pub mod no_hash;
23+
#[cfg(not(target_arch = "wasm32"))]
2324
pub mod notify_watcher;
2425
pub mod prelude;
2526
pub mod recurser;
@@ -32,4 +33,6 @@ pub mod upgrade_lock;
3233
pub mod upward_search;
3334
pub mod visit;
3435
pub mod watcher;
36+
#[cfg(not(target_arch = "wasm32"))]
37+
pub mod watchman;
3538
pub mod with_hash;

pyrefly/lib/util/watchman.rs

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
/*
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
use std::path::Path;
9+
use std::path::PathBuf;
10+
11+
use anyhow::Context as _;
12+
use async_trait::async_trait;
13+
use notify::Event;
14+
use tracing::debug;
15+
use tracing::info;
16+
use watchman_client::CanonicalPath;
17+
use watchman_client::Connector;
18+
use watchman_client::Subscription;
19+
use watchman_client::SubscriptionData;
20+
use watchman_client::expr::Expr;
21+
use watchman_client::pdu::Clock;
22+
use watchman_client::pdu::FileType;
23+
use watchman_client::pdu::SubscribeRequest;
24+
use watchman_client::pdu::SyncTimeout;
25+
26+
use crate::util::watcher::Watcher;
27+
28+
// The "new" field is marked as deprecated, but buck2 uses it and
29+
// I'm unaware of issues due to its use there.
30+
//
31+
// Putting this in it own mod was the best way to scope the allow(deprecated).
32+
#[allow(deprecated)]
33+
mod watchman_query {
34+
use std::path::Path;
35+
36+
use notify::Event;
37+
use notify::EventKind;
38+
use notify::event::CreateKind;
39+
use notify::event::EventAttributes;
40+
use notify::event::ModifyKind;
41+
use notify::event::RemoveKind;
42+
use serde::Deserialize;
43+
use watchman_client::prelude::*;
44+
45+
query_result_type! {
46+
pub struct SubscriptionFields {
47+
name: NameField,
48+
file_type: FileTypeField,
49+
exists: ExistsField,
50+
new: NewField,
51+
}
52+
}
53+
54+
impl SubscriptionFields {
55+
pub fn into_event(self, root: &Path) -> Option<Event> {
56+
match *self.file_type {
57+
FileType::BlockSpecial
58+
| FileType::CharSpecial
59+
| FileType::Fifo
60+
| FileType::Socket
61+
| FileType::SolarisDoor
62+
| FileType::Unknown => {
63+
return None;
64+
}
65+
FileType::Directory | FileType::Regular | FileType::Symlink => {}
66+
};
67+
68+
let kind = match (*self.exists, *self.new) {
69+
(true, true) => EventKind::Create(CreateKind::Any),
70+
(false, _) => EventKind::Remove(RemoveKind::Any),
71+
(true, false) => EventKind::Modify(ModifyKind::Any),
72+
};
73+
74+
Some(Event {
75+
kind,
76+
paths: vec![root.join(self.name.as_path())],
77+
attrs: EventAttributes::new(),
78+
})
79+
}
80+
}
81+
}
82+
83+
pub struct Watchman {
84+
root: PathBuf,
85+
subscription: Subscription<watchman_query::SubscriptionFields>,
86+
}
87+
88+
#[async_trait]
89+
impl Watcher for Watchman {
90+
async fn wait(&mut self) -> anyhow::Result<Vec<Event>> {
91+
loop {
92+
match self
93+
.subscription
94+
.next()
95+
.await
96+
.context("Failed to get watchman response")?
97+
{
98+
SubscriptionData::Canceled => {
99+
return Err(anyhow::anyhow!("Watchman subscription was canceled"));
100+
}
101+
SubscriptionData::StateEnter { .. } | SubscriptionData::StateLeave { .. } => {
102+
continue;
103+
}
104+
SubscriptionData::FilesChanged(changes) => {
105+
if let Some(files) = changes.files {
106+
info!("Received watchman changes on {} files", files.len());
107+
return Ok(files
108+
.into_iter()
109+
.filter_map(|file| file.into_event(&self.root))
110+
.collect());
111+
} else {
112+
continue;
113+
}
114+
}
115+
}
116+
}
117+
}
118+
}
119+
120+
impl Watchman {
121+
pub async fn new(path: &Path) -> anyhow::Result<Self> {
122+
let root = CanonicalPath::canonicalize(path)?;
123+
let watchman_client = Connector::new()
124+
.connect()
125+
.await
126+
.context("Failed to connect to watchman client")?;
127+
let root = watchman_client
128+
.resolve_root(root)
129+
.await
130+
.context("Failed to resolve root directory for watchman client")?;
131+
let clock = watchman_client
132+
.clock(&root, SyncTimeout::Default)
133+
.await
134+
.context("Failed to obtain initial clock from watchman client")?;
135+
let (subscription, initial_response) = watchman_client
136+
.subscribe::<watchman_query::SubscriptionFields>(
137+
&root,
138+
SubscribeRequest {
139+
// If `empty_on_fresh_instance` is false (default), and if `since` is None,
140+
// watchman is going to crawl & return the entire file tree on the first subscription
141+
// response, which can be very slow. Explicitly setting the `since` clock can
142+
// significantly speed up the setup.
143+
since: Some(Clock::Spec(clock)),
144+
relative_root: Some(PathBuf::from(".")),
145+
expression: Some(Expr::All(vec![
146+
Expr::FileType(FileType::Regular),
147+
Expr::Suffix(vec![PathBuf::from("py"), PathBuf::from("pyi")]),
148+
])),
149+
..SubscribeRequest::default()
150+
},
151+
)
152+
.await
153+
.context("Failed to subscribe to changes")?;
154+
debug!(
155+
"Successfully established watchman subscription. Initial response: {:#?}",
156+
initial_response
157+
);
158+
Ok(Watchman {
159+
root: root.path(),
160+
subscription,
161+
})
162+
}
163+
}

0 commit comments

Comments
 (0)