Skip to content

Commit b2c7ac7

Browse files
committed
feat(server): add file system monitor
- Populate artifact_paths table during check ins. - Add file watching via the notify crate to track changes to invalidate the artifact_paths table. - Added commands for inspecting/manipulating the current watches. - Added server configuration to disable the file system monitor.
1 parent 538cc1a commit b2c7ac7

24 files changed

+822
-27
lines changed

Cargo.lock

+2
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ indicatif = "0.17"
6868
indoc = "2"
6969
itertools = "0.12"
7070
libc = "0.2"
71+
lru = "0.12"
7172
lsp-types = { version = "0.95" }
7273
mime = "0.3"
7374
notify = "6"

packages/cli/Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ serde_json = { workspace = true }
3838
serde_with = { workspace = true }
3939
tangram_client = { workspace = true }
4040
tangram_server = { workspace = true }
41+
time = { workspace = true }
4142
tokio = { workspace = true }
4243
tokio-util = { workspace = true }
4344
tracing = { workspace = true }

packages/cli/src/commands.rs

+1
Original file line numberDiff line numberDiff line change
@@ -23,3 +23,4 @@ pub mod run;
2323
pub mod server;
2424
pub mod tree;
2525
pub mod upgrade;
26+
pub mod watch;

packages/cli/src/commands/checkin.rs

+6-1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ use tangram_client as tg;
77
pub struct Args {
88
/// The path to check in.
99
pub path: Option<PathBuf>,
10+
11+
/// Toggle whether the server will add a file watcher for this path.
12+
#[arg(long)]
13+
pub watch: Option<bool>,
1014
}
1115

1216
impl Cli {
@@ -19,9 +23,10 @@ impl Cli {
1923
if let Some(path_arg) = &args.path {
2024
path.push(path_arg);
2125
}
26+
let watch = args.watch.unwrap_or(true);
2227

2328
// Perform the checkin.
24-
let artifact = tg::Artifact::check_in(client, &path.try_into()?).await?;
29+
let artifact = tg::Artifact::check_in(client, &path.try_into()?, watch).await?;
2530

2631
// Print the ID.
2732
let id = artifact.id(client).await?;

packages/cli/src/commands/server.rs

+12
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,17 @@ impl Cli {
140140
},
141141
);
142142

143+
// Create the file system monitor options
144+
let file_system_monitor = config
145+
.as_ref()
146+
.and_then(|config| config.file_system_monitor.as_ref())
147+
.map_or(
148+
tangram_server::options::FileSystemMonitor { enable: true },
149+
|file_system_monitor| tangram_server::options::FileSystemMonitor {
150+
enable: file_system_monitor.enable,
151+
},
152+
);
153+
143154
// Create the messenger options.
144155
let messenger = config
145156
.and_then(|config| config.messenger.as_ref())
@@ -248,6 +259,7 @@ impl Cli {
248259
advanced,
249260
build,
250261
database,
262+
file_system_monitor,
251263
messenger,
252264
oauth,
253265
path,

packages/cli/src/commands/watch.rs

+79
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
use std::path::PathBuf;
2+
3+
use tangram_client as tg;
4+
5+
use crate::Cli;
6+
use tg::Handle;
7+
8+
/// Manage watches.
9+
#[derive(Debug, clap::Args)]
10+
pub struct Args {
11+
#[clap(subcommand)]
12+
pub command: Command,
13+
}
14+
15+
#[derive(Debug, clap::Subcommand)]
16+
pub enum Command {
17+
Add(AddArgs),
18+
Remove(RemoveArgs),
19+
List(ListArgs),
20+
}
21+
22+
#[derive(Debug, clap::Args)]
23+
pub struct AddArgs {
24+
pub path: PathBuf,
25+
}
26+
27+
#[derive(Debug, clap::Args)]
28+
pub struct RemoveArgs {
29+
pub path: PathBuf,
30+
}
31+
32+
#[derive(Debug, clap::Args)]
33+
34+
pub struct ListArgs;
35+
36+
impl Cli {
37+
pub async fn command_watch(&self, args: Args) -> tg::Result<()> {
38+
match args.command {
39+
Command::Add(args) => self.command_watch_add(args).await,
40+
Command::List(args) => self.command_watch_list(args).await,
41+
Command::Remove(args) => self.command_watch_remove(args).await,
42+
}
43+
}
44+
45+
async fn command_watch_add(&self, args: AddArgs) -> tg::Result<()> {
46+
let client = &self.client().await?;
47+
let path = args
48+
.path
49+
.canonicalize()
50+
.map_err(|source| tg::error!(!source, "failed to canonicalize the path"))?;
51+
let path = path.try_into()?;
52+
tg::Artifact::check_in(client, &path, true)
53+
.await
54+
.map_err(|source| tg::error!(!source, %path, "failed to add watch"))?;
55+
Ok(())
56+
}
57+
58+
async fn command_watch_list(&self, _args: ListArgs) -> tg::Result<()> {
59+
let client = self.client().await?;
60+
let paths = client
61+
.get_watches()
62+
.await
63+
.map_err(|source| tg::error!(!source, "failed to get watches"))?;
64+
for path in paths {
65+
eprintln!("{path}");
66+
}
67+
Ok(())
68+
}
69+
70+
async fn command_watch_remove(&self, args: RemoveArgs) -> tg::Result<()> {
71+
let client = self.client().await?;
72+
let path = args.path.try_into()?;
73+
client
74+
.remove_watch(&path)
75+
.await
76+
.map_err(|source| tg::error!(!source, %path, "failed to remove watch"))?;
77+
Ok(())
78+
}
79+
}

packages/cli/src/config.rs

+37-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,11 @@
1+
//! # Configuring the tangram CLI and server
2+
//!
3+
//! Tangram can be configured by a global config file located at $HOME/.config/config.json or by passing the `--config <path>` option to the `tg` command line before any subcommand, for example
4+
//!
5+
//! ```sh
6+
//! # Run the server using a config file.
7+
//! tg --config config.json server run
8+
//! ```
19
use serde_with::serde_as;
210
use std::path::PathBuf;
311
use tangram_client as tg;
@@ -6,57 +14,75 @@ use url::Url;
614
#[serde_as]
715
#[derive(Clone, Debug, Default, serde::Serialize, serde::Deserialize)]
816
pub struct Config {
17+
/// Advanced configuration options.
918
#[serde(default, skip_serializing_if = "Option::is_none")]
1019
pub advanced: Option<Advanced>,
1120

1221
#[serde(default, skip_serializing_if = "Option::is_none")]
1322
pub autoenv: Option<Autoenv>,
1423

24+
/// Configure the server's build options.
1525
#[serde(default, skip_serializing_if = "Option::is_none")]
1626
pub build: Option<Build>,
1727

28+
/// Configure the server's database options.
1829
#[serde(default, skip_serializing_if = "Option::is_none")]
1930
pub database: Option<Database>,
2031

32+
/// Configure the server's file system monitoring options.
33+
#[serde(default, skip_serializing_if = "Option::is_none")]
34+
pub file_system_monitor: Option<FileSystemMonitor>,
35+
2136
#[serde(default, skip_serializing_if = "Option::is_none")]
2237
pub messenger: Option<Messenger>,
2338

2439
#[serde(default, skip_serializing_if = "Option::is_none")]
2540
pub oauth: Option<Oauth>,
2641

42+
/// Configure the server's path. Default = `$HOME/.tangram`.
2743
#[serde(default, skip_serializing_if = "Option::is_none")]
2844
pub path: Option<PathBuf>,
2945

46+
/// A list of remote servers that this server can push and pull objects/builds from.
3047
#[serde(default, skip_serializing_if = "Option::is_none")]
3148
pub remotes: Option<Vec<Remote>>,
3249

50+
/// Server and CLI tracing options.
3351
#[serde(default, skip_serializing_if = "Option::is_none")]
3452
pub tracing: Option<Tracing>,
3553

54+
/// The URL of the server, if serving over tcp.
3655
#[serde(default, skip_serializing_if = "Option::is_none")]
3756
pub url: Option<Url>,
3857

58+
/// Configurate the virtual file system.
3959
#[serde(default, skip_serializing_if = "Option::is_none")]
4060
pub vfs: Option<Vfs>,
4161
}
4262

4363
#[derive(Clone, Debug, Default, serde::Serialize, serde::Deserialize)]
4464
pub struct Advanced {
65+
/// Configure how errors are displayed in the CLI.
4566
#[serde(default, skip_serializing_if = "Option::is_none")]
4667
pub error_trace_options: Option<tg::error::TraceOptions>,
4768

69+
/// Configure the number of file descriptors available in the client.
4870
#[serde(default, skip_serializing_if = "Option::is_none")]
4971
pub file_descriptor_limit: Option<u64>,
5072

73+
/// Configure the number of file descriptors available in the server.
5174
#[serde(default, skip_serializing_if = "Option::is_none")]
5275
pub file_descriptor_semaphore_size: Option<usize>,
5376

77+
/// Toggle whether temp directories are preserved or deleted after builds. Default = false.
5478
#[serde(default, skip_serializing_if = "Option::is_none")]
5579
pub preserve_temp_directories: Option<bool>,
5680

81+
/// Toggle whether tokio-console support is enabled. Default = false.
5782
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
5883
pub tokio_console: bool,
5984

85+
/// Toggle whether log messages are printed to the server's stderr. Default = false.
6086
#[serde(default, skip_serializing_if = "Option::is_none")]
6187
pub write_build_logs_to_stderr: Option<bool>,
6288
}
@@ -69,7 +95,7 @@ pub struct Autoenv {
6995

7096
#[derive(Clone, Debug, Default, serde::Serialize, serde::Deserialize)]
7197
pub struct Build {
72-
/// Enable builds.
98+
/// Toggle whether builds are enabled on this server.
7399
#[serde(default, skip_serializing_if = "Option::is_none")]
74100
pub enable: Option<bool>,
75101

@@ -85,6 +111,12 @@ pub enum Database {
85111
Postgres(PostgresDatabase),
86112
}
87113

114+
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
115+
pub struct FileSystemMonitor {
116+
/// Toggle whether the file system monitor is enabled. Default = true.
117+
pub enable: bool,
118+
}
119+
88120
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
89121
pub struct SqliteDatabase {
90122
/// The maximum number of connections.
@@ -149,8 +181,11 @@ pub struct RemoteBuild {
149181

150182
#[derive(Clone, Debug, Default, serde::Serialize, serde::Deserialize)]
151183
pub struct Tracing {
184+
/// The filter applied to tracing messages.
152185
#[serde(default, skip_serializing_if = "String::is_empty")]
153186
pub filter: String,
187+
188+
/// The display format of tracing messages.
154189
#[serde(default, skip_serializing_if = "Option::is_none")]
155190
pub format: Option<TracingFormat>,
156191
}
@@ -190,6 +225,7 @@ impl std::str::FromStr for TracingFormat {
190225

191226
#[derive(Clone, Debug, Default, serde::Serialize, serde::Deserialize)]
192227
pub struct Vfs {
228+
/// Toggle whether the VFS is enabled. When the VFS is disabled, checkouts will be made onto local disk. Default = true.
193229
#[serde(default, skip_serializing_if = "Option::is_none")]
194230
pub enable: Option<bool>,
195231
}

packages/cli/src/main.rs

+2
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ pub enum Command {
7676
Tree(self::commands::tree::Args),
7777
Update(self::commands::package::UpdateArgs),
7878
Upgrade(self::commands::upgrade::Args),
79+
Watch(self::commands::watch::Args),
7980
}
8081

8182
fn default_path() -> PathBuf {
@@ -212,6 +213,7 @@ impl Cli {
212213
Command::Tree(args) => self.command_tree(args).boxed(),
213214
Command::Update(args) => self.command_package_update(args).boxed(),
214215
Command::Upgrade(args) => self.command_upgrade(args).boxed(),
216+
Command::Watch(args) => self.command_watch(args).boxed(),
215217
}
216218
.await
217219
}

packages/client/src/artifact.rs

+6-2
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ pub enum Data {
7575
#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
7676
pub struct CheckInArg {
7777
pub path: crate::Path,
78+
pub watch: bool,
7879
}
7980

8081
#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
@@ -123,8 +124,11 @@ impl Artifact {
123124
}
124125

125126
impl Artifact {
126-
pub async fn check_in(tg: &impl Handle, path: &crate::Path) -> tg::Result<Self> {
127-
let arg = CheckInArg { path: path.clone() };
127+
pub async fn check_in(tg: &impl Handle, path: &crate::Path, watch: bool) -> tg::Result<Self> {
128+
let arg = CheckInArg {
129+
path: path.clone(),
130+
watch,
131+
};
128132
let output = tg.check_in_artifact(arg).await?;
129133
let artifact = Self::with_id(output.id);
130134
Ok(artifact)

packages/client/src/blob.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -261,7 +261,7 @@ impl Blob {
261261

262262
// Check in the extracted artifact.
263263
let path = path.try_into()?;
264-
let artifact = Artifact::check_in(tg, &path)
264+
let artifact = Artifact::check_in(tg, &path, false)
265265
.await
266266
.map_err(|source| error!(!source, "failed to check in the extracted archive"))?;
267267

packages/client/src/handle.rs

+4
Original file line numberDiff line numberDiff line change
@@ -308,4 +308,8 @@ pub trait Handle: Clone + Unpin + Send + Sync + 'static {
308308
&self,
309309
token: &str,
310310
) -> impl Future<Output = tg::Result<Option<tg::User>>> + Send;
311+
312+
fn get_watches(&self) -> impl Future<Output = tg::Result<Vec<tg::Path>>> + Send;
313+
314+
fn remove_watch(&self, path: &tg::Path) -> impl Future<Output = tg::Result<()>> + Send;
311315
}

packages/client/src/lib.rs

+8
Original file line numberDiff line numberDiff line change
@@ -781,4 +781,12 @@ impl Handle for Client {
781781
async fn get_user_for_token(&self, token: &str) -> tg::Result<Option<tg::User>> {
782782
self.get_user_for_token(token).await
783783
}
784+
785+
async fn get_watches(&self) -> tg::Result<Vec<tg::Path>> {
786+
self.get_watches().await
787+
}
788+
789+
async fn remove_watch(&self, path: &tg::Path) -> tg::Result<()> {
790+
self.remove_watch(path).await
791+
}
784792
}

0 commit comments

Comments
 (0)