Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: fs events #3452

Merged
merged 37 commits into from
Feb 21, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
3385a59
prototype file watching
bartlomieju Dec 6, 2019
775be5a
add test
bartlomieju Dec 6, 2019
b2762df
Merge branch 'master' into feat-fs_events
bartlomieju Jan 23, 2020
1ba4acb
fixes for new notify
bartlomieju Jan 23, 2020
551c122
fixes
bartlomieju Jan 23, 2020
05a31a3
remove debounce
bartlomieju Jan 23, 2020
7bb79e6
add EventType enum
bartlomieju Jan 23, 2020
630603f
debug tests
bartlomieju Jan 23, 2020
849ff0b
more debug logs
bartlomieju Jan 23, 2020
4dee826
fmt
bartlomieju Jan 23, 2020
98aa812
Merge branch 'master' into feat-fs_events
bartlomieju Jan 28, 2020
e6e264d
Merge branch 'master' into feat-fs_events
bartlomieju Feb 20, 2020
4d67c97
update to latest State
bartlomieju Feb 20, 2020
008a8be
Merge branch 'master' into feat-fs_events
ry Feb 20, 2020
ba283b6
WIP
ry Feb 20, 2020
e46d2e9
WIP
ry Feb 20, 2020
5bc320e
progress
ry Feb 20, 2020
afcc388
fmt
ry Feb 20, 2020
1bf1429
rename to Deno.fsEvents()
ry Feb 21, 2020
100b8f8
fix
ry Feb 21, 2020
5bb2cad
fix
ry Feb 21, 2020
2bdf5a8
fix
ry Feb 21, 2020
2964026
cleanup
ry Feb 21, 2020
c7314f2
lint
ry Feb 21, 2020
8d094f2
fmt
ry Feb 21, 2020
72a217b
fix
ry Feb 21, 2020
744f383
manual
ry Feb 21, 2020
62ada69
Merge branch 'master' into feat-fs_events
ry Feb 21, 2020
d4bfd61
x
ry Feb 21, 2020
ac8e48f
test
ry Feb 21, 2020
22add5e
use AsyncIterableIterator return method to close
ry Feb 21, 2020
648fbac
fix
ry Feb 21, 2020
63da3c7
remove delay
ry Feb 21, 2020
b477372
Merge branch 'master' into feat-fs_events
ry Feb 21, 2020
b22e093
fix
ry Feb 21, 2020
f6efa52
fmt
ry Feb 21, 2020
f670d5e
try removing Deno.runTests()
ry Feb 21, 2020
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2,040 changes: 1,096 additions & 944 deletions Cargo.lock

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ indexmap = "1.3.0"
lazy_static = "1.4.0"
libc = "0.2.66"
log = "0.4.8"
notify = { version = "5.0.0-pre.2" }
rand = "0.7.2"
regex = "1.3.1"
remove_dir_all = "0.5.2"
Expand All @@ -53,7 +54,7 @@ serde = { version = "1.0.104", features = ["derive"] }
serde_derive = "1.0.104"
serde_json = { version = "1.0.44", features = [ "preserve_order" ] }
source-map-mappings = "0.5.0"
sys-info = "0.5.8"
sys-info = "=0.5.8" # 0.5.9 seems to be broken on windows.
tempfile = "3.1.0"
termcolor = "1.0.5"
tokio = { version = "0.2", features = ["rt-core", "tcp", "udp", "process", "fs", "blocking", "sync", "io-std", "macros", "time"] }
Expand Down
1 change: 1 addition & 0 deletions cli/js/deno.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ export {
OpenOptions,
OpenMode
} from "./files.ts";
export { FsEvent, fsEvents } from "./fs_events.ts";
export {
EOF,
copy,
Expand Down
2 changes: 2 additions & 0 deletions cli/js/dispatch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,8 @@ export let OP_CWD: number;
export let OP_CONNECT_TLS: number;
export let OP_HOSTNAME: number;
export let OP_OPEN_PLUGIN: number;
export let OP_FS_EVENTS_OPEN: number;
export let OP_FS_EVENTS_POLL: number;
export let OP_COMPILE: number;
export let OP_TRANSPILE: number;
export let OP_SIGNAL_BIND: number;
Expand Down
40 changes: 40 additions & 0 deletions cli/js/fs_events.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// Copyright 2019 the Deno authors. All rights reserved. MIT license.
import { sendSync, sendAsync } from "./dispatch_json.ts";
import * as dispatch from "./dispatch.ts";
import { close } from "./files.ts";

export interface FsEvent {
kind: "any" | "access" | "create" | "modify" | "remove";
paths: string[];
}

class FsEvents implements AsyncIterableIterator<FsEvent> {
readonly rid: number;

constructor(paths: string[], options: { recursive: boolean }) {
const { recursive } = options;
this.rid = sendSync(dispatch.OP_FS_EVENTS_OPEN, { recursive, paths });
}

async next(): Promise<IteratorResult<FsEvent>> {
return await sendAsync(dispatch.OP_FS_EVENTS_POLL, {
rid: this.rid
});
}

async return(value?: FsEvent): Promise<IteratorResult<FsEvent>> {
close(this.rid);
return { value, done: true };
}

[Symbol.asyncIterator](): AsyncIterableIterator<FsEvent> {
return this;
}
}

export function fsEvents(
paths: string | string[],
options = { recursive: true }
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be written as

{
  recursive = true
}: FsEventsOptions = {}

for full robustness.

): AsyncIterableIterator<FsEvent> {
return new FsEvents(Array.isArray(paths) ? paths : [paths], options);
}
52 changes: 52 additions & 0 deletions cli/js/fs_events_test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
import { testPerm, assert } from "./test_util.ts";

// TODO(ry) Add more tests to specify format.

testPerm({ read: false }, function fsEventsPermissions() {
let thrown = false;
try {
Deno.fsEvents(".");
} catch (err) {
assert(err instanceof Deno.Err.PermissionDenied);
thrown = true;
}
assert(thrown);
});

async function getTwoEvents(
iter: AsyncIterableIterator<Deno.FsEvent>
): Promise<Deno.FsEvent[]> {
const events = [];
for await (const event of iter) {
console.log(">>>> event", event);
events.push(event);
if (events.length > 2) break;
}
return events;
}

testPerm({ read: true, write: true }, async function fsEventsBasic(): Promise<
void
> {
const testDir = await Deno.makeTempDir();
const iter = Deno.fsEvents(testDir);

// Asynchornously capture two fs events.
const eventsPromise = getTwoEvents(iter);

// Make some random file system activity.
const file1 = testDir + "/file1.txt";
const file2 = testDir + "/file2.txt";
Deno.writeFileSync(file1, new Uint8Array([0, 1, 2]));
Deno.writeFileSync(file2, new Uint8Array([0, 1, 2]));

// We should have gotten two fs events.
const events = await eventsPromise;
console.log("events", events);
assert(events.length >= 2);
assert(events[0].kind == "create");
assert(events[0].paths[0].includes(testDir));
assert(events[1].kind == "create" || events[1].kind == "modify");
assert(events[1].paths[0].includes(testDir));
});
15 changes: 15 additions & 0 deletions cli/js/lib.deno.ns.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1620,6 +1620,21 @@ declare namespace Deno {
*/
export function resources(): ResourceMap;

/** UNSTABLE: new API. Needs docs. */
export interface FsEvent {
kind: "any" | "access" | "create" | "modify" | "remove";
paths: string[];
}

/** UNSTABLE: new API. Needs docs.
*
* recursive option is true by default.
*/
export function fsEvents(
paths: string | string[],
options?: { recursive: boolean }
): AsyncIterableIterator<FsEvent>;

/** How to handle subprocess stdio.
*
* "inherit" The default if unspecified. The child inherits from the
Expand Down
1 change: 1 addition & 0 deletions cli/js/unit_tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import "./fetch_test.ts";
import "./file_test.ts";
import "./files_test.ts";
import "./form_data_test.ts";
import "./fs_events_test.ts";
import "./get_random_values_test.ts";
import "./globals_test.ts";
import "./headers_test.ts";
Expand Down
129 changes: 129 additions & 0 deletions cli/ops/fs_events.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license.
use super::dispatch_json::{Deserialize, JsonOp, Value};
use crate::deno_error::bad_resource;
use crate::ops::json_op;
use crate::state::State;
use deno_core::*;
use futures::future::poll_fn;
use futures::future::FutureExt;
use notify::event::Event as NotifyEvent;
use notify::Error as NotifyError;
use notify::EventKind;
use notify::RecommendedWatcher;
use notify::RecursiveMode;
use notify::Watcher;
use serde::Serialize;
use std::convert::From;
use std::path::PathBuf;
use tokio::sync::mpsc;

pub fn init(i: &mut Isolate, s: &State) {
i.register_op(
"fs_events_open",
s.core_op(json_op(s.stateful_op(op_fs_events_open))),
);
i.register_op(
"fs_events_poll",
s.core_op(json_op(s.stateful_op(op_fs_events_poll))),
);
}

struct FsEventsResource {
#[allow(unused)]
watcher: RecommendedWatcher,
receiver: mpsc::Receiver<Result<FsEvent, ErrBox>>,
}

/// Represents a file system event.
///
/// We do not use the event directly from the notify crate. We flatten
/// the structure into this simpler structure. We want to only make it more
/// complex as needed.
///
/// Feel free to expand this struct as long as you can add tests to demonstrate
/// the complexity.
#[derive(Serialize, Debug)]
struct FsEvent {
kind: String,
paths: Vec<PathBuf>,
}

impl From<NotifyEvent> for FsEvent {
fn from(e: NotifyEvent) -> Self {
let kind = match e.kind {
EventKind::Any => "any",
EventKind::Access(_) => "access",
EventKind::Create(_) => "create",
EventKind::Modify(_) => "modify",
EventKind::Remove(_) => "remove",
EventKind::Other => todo!(), // What's this for? Leaving it out for now.
}
.to_string();
FsEvent {
kind,
paths: e.paths,
}
}
}

pub fn op_fs_events_open(
state: &State,
args: Value,
_zero_copy: Option<ZeroCopyBuf>,
) -> Result<JsonOp, ErrBox> {
#[derive(Deserialize)]
struct OpenArgs {
recursive: bool,
paths: Vec<String>,
}
let args: OpenArgs = serde_json::from_value(args)?;
let (sender, receiver) = mpsc::channel::<Result<FsEvent, ErrBox>>(16);
let sender = std::sync::Mutex::new(sender);
let mut watcher: RecommendedWatcher =
Watcher::new_immediate(move |res: Result<NotifyEvent, NotifyError>| {
let res2 = res.map(FsEvent::from).map_err(ErrBox::from);
let mut sender = sender.lock().unwrap();
futures::executor::block_on(sender.send(res2)).expect("fs events error");
})?;
let recursive_mode = if args.recursive {
RecursiveMode::Recursive
} else {
RecursiveMode::NonRecursive
};
for path in &args.paths {
state.check_read(&PathBuf::from(path))?;
watcher.watch(path, recursive_mode)?;
}
let resource = FsEventsResource { watcher, receiver };
let table = &mut state.borrow_mut().resource_table;
let rid = table.add("fsEvents", Box::new(resource));
Ok(JsonOp::Sync(json!(rid)))
}

pub fn op_fs_events_poll(
state: &State,
args: Value,
_zero_copy: Option<ZeroCopyBuf>,
) -> Result<JsonOp, ErrBox> {
#[derive(Deserialize)]
struct PollArgs {
rid: u32,
}
let PollArgs { rid } = serde_json::from_value(args)?;
let state = state.clone();
let f = poll_fn(move |cx| {
let resource_table = &mut state.borrow_mut().resource_table;
let watcher = resource_table
.get_mut::<FsEventsResource>(rid)
.ok_or_else(bad_resource)?;
watcher
.receiver
.poll_recv(cx)
.map(|maybe_result| match maybe_result {
Some(Ok(value)) => Ok(json!({ "value": value, "done": false })),
ry marked this conversation as resolved.
Show resolved Hide resolved
Some(Err(err)) => Err(err),
None => Ok(json!({ "done": true })),
})
});
Ok(JsonOp::Async(f.boxed_local()))
}
1 change: 1 addition & 0 deletions cli/ops/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ pub mod errors;
pub mod fetch;
pub mod files;
pub mod fs;
pub mod fs_events;
pub mod io;
pub mod net;
pub mod os;
Expand Down
1 change: 1 addition & 0 deletions cli/worker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,7 @@ impl MainWorker {
ops::fetch::init(isolate, &state);
ops::files::init(isolate, &state);
ops::fs::init(isolate, &state);
ops::fs_events::init(isolate, &state);
ops::io::init(isolate, &state);
ops::plugins::init(isolate, &state, op_registry);
ops::net::init(isolate, &state);
Expand Down
17 changes: 17 additions & 0 deletions std/manual.md
Original file line number Diff line number Diff line change
Expand Up @@ -467,6 +467,23 @@ for await (const _ of sig) {

The above for-await loop exits after 5 seconds when sig.dispose() is called.

### File system events

To poll for file system events:

```ts
const iter = Deno.fsEvents("/");
for await (const event of iter) {
console.log(">>>> event", event);
// { kind: "create", paths: [ "/foo.txt" ] }
}
```

Note that the exact ordering of the events can vary between operating systems.
This feature uses different syscalls depending on the platform:

Linux: inotify macOS: FSEvents Windows: ReadDirectoryChangesW

### Linking to third party code

In the above examples, we saw that Deno could execute scripts from URLs. Like
Expand Down