Skip to content

Commit 88e6308

Browse files
committed
Inotify
1 parent 9b65d88 commit 88e6308

File tree

6 files changed

+583
-11
lines changed

6 files changed

+583
-11
lines changed

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ libc = "0.2.152"
1212
# Default enable napi4 feature, see https://nodejs.org/api/n-api.html#node-api-version-matrix
1313
napi = { version = "2.12.2", default-features = false, features = ["napi4"] }
1414
napi-derive = "2.12.2"
15-
nix = { version = "0.29.0", features = ["fs", "term", "poll"] }
15+
nix = { version = "0.29.0", features = ["fs", "term", "poll", "inotify"] }
1616

1717
[build-dependencies]
1818
napi-build = "2.0.1"

index.d.ts

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,17 +19,22 @@ export interface Size {
1919
rows: number
2020
}
2121
/** Resize the terminal. */
22-
export function ptyResize(fd: number, size: Size): void
22+
export declare function ptyResize(fd: number, size: Size): void
2323
/**
2424
* Set the close-on-exec flag on a file descriptor. This is `fcntl(fd, F_SETFD, FD_CLOEXEC)` under
2525
* the covers.
2626
*/
27-
export function setCloseOnExec(fd: number, closeOnExec: boolean): void
27+
export declare function setCloseOnExec(fd: number, closeOnExec: boolean): void
2828
/**
2929
* Get the close-on-exec flag on a file descriptor. This is `fcntl(fd, F_GETFD) & FD_CLOEXEC ==
3030
*_CLOEXEC` under the covers.
3131
*/
32-
export function getCloseOnExec(fd: number): boolean
32+
export declare function getCloseOnExec(fd: number): boolean
33+
export const IN_CLOSE_WRITE: number
34+
export const IN_MOVED_FROM: number
35+
export const IN_MOVED_TO: number
36+
export const IN_CREATE: number
37+
export const IN_DELETE: number
3338
export class Pty {
3439
/** The pid of the forked process. */
3540
pid: number
@@ -41,3 +46,29 @@ export class Pty {
4146
*/
4247
takeFd(): c_int
4348
}
49+
/**
50+
* A way to access Linux' `inotify(7)` subsystem. For simplicity, this only allows subscribing for
51+
* events on directories (instead of files) and only for modify-close and rename events.
52+
*/
53+
export class Inotify {
54+
constructor()
55+
/**
56+
* Close the inotify file descriptor. Must be called at most once to avoid file descriptor
57+
* leaks.
58+
*/
59+
close(): void
60+
/**
61+
* Borrow the file descriptor. It is expected that Nod does not close the file descriptor and
62+
* instead the .close() method should be called to clean the file descriptor up. Read the file
63+
* descriptor on node according to `inotify(7)` to get events.
64+
*/
65+
fd(): c_int
66+
/**
67+
* Register one directory to be watched. Events for close-after-write, renames, and deletions
68+
* will be registered. Events for creation and modification will be ignored. Returns a watch
69+
* descriptor, which can be used in `remove_watch`.
70+
*/
71+
addCloseWrite(dir: string): number
72+
/** Stop watching the watch descriptor provided. */
73+
removeWatch(wd: number): void
74+
}

index.js

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -310,9 +310,15 @@ if (!nativeBinding) {
310310
throw new Error(`Failed to load native binding`)
311311
}
312312

313-
const { Pty, ptyResize, setCloseOnExec, getCloseOnExec } = nativeBinding
313+
const { Pty, ptyResize, setCloseOnExec, getCloseOnExec, Inotify, IN_CLOSE_WRITE, IN_MOVED_FROM, IN_MOVED_TO, IN_CREATE, IN_DELETE } = nativeBinding
314314

315315
module.exports.Pty = Pty
316316
module.exports.ptyResize = ptyResize
317317
module.exports.setCloseOnExec = setCloseOnExec
318318
module.exports.getCloseOnExec = getCloseOnExec
319+
module.exports.Inotify = Inotify
320+
module.exports.IN_CLOSE_WRITE = IN_CLOSE_WRITE
321+
module.exports.IN_MOVED_FROM = IN_MOVED_FROM
322+
module.exports.IN_MOVED_TO = IN_MOVED_TO
323+
module.exports.IN_CREATE = IN_CREATE
324+
module.exports.IN_DELETE = IN_DELETE

src/lib.rs

Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -371,3 +371,227 @@ fn set_nonblocking(fd: i32) -> Result<(), napi::Error> {
371371
}
372372
Ok(())
373373
}
374+
375+
#[cfg(target_os = "linux")]
376+
use nix::sys::inotify::{AddWatchFlags, InitFlags};
377+
#[cfg(target_os = "linux")]
378+
use std::ffi::CString;
379+
380+
/// A way to access Linux' `inotify(7)` subsystem. For simplicity, this only allows subscribing for
381+
/// events on directories (instead of files) and only for modify-close and rename events.
382+
#[napi]
383+
#[allow(dead_code)]
384+
struct Inotify {
385+
fd: Option<OwnedFd>,
386+
}
387+
388+
#[cfg(target_os = "linux")]
389+
#[napi]
390+
#[allow(dead_code)]
391+
impl Inotify {
392+
#[napi(constructor)]
393+
pub fn new() -> Result<Self, napi::Error> {
394+
let fd = match Errno::result(unsafe {
395+
libc::inotify_init1((InitFlags::IN_CLOEXEC | InitFlags::IN_NONBLOCK).bits())
396+
}) {
397+
Ok(fd) => unsafe { OwnedFd::from_raw_fd(fd) },
398+
Err(err) => {
399+
return Err(napi::Error::new(
400+
GenericFailure,
401+
format!("inotify_init: {}", err),
402+
));
403+
}
404+
};
405+
Ok(Inotify { fd: Some(fd) })
406+
}
407+
408+
/// Close the inotify file descriptor. Must be called at most once to avoid file descriptor
409+
/// leaks.
410+
#[napi]
411+
#[allow(dead_code)]
412+
pub fn close(&mut self) -> Result<(), napi::Error> {
413+
let inotify = self.fd.take();
414+
if inotify.is_none() {
415+
return Err(napi::Error::new(
416+
GenericFailure,
417+
"inotify fd has already been closed",
418+
));
419+
}
420+
421+
Ok(())
422+
}
423+
424+
/// Borrow the file descriptor. It is expected that Nod does not close the file descriptor and
425+
/// instead the .close() method should be called to clean the file descriptor up. Read the file
426+
/// descriptor on node according to `inotify(7)` to get events.
427+
#[napi]
428+
#[allow(dead_code)]
429+
pub fn fd(&self) -> Result<c_int, napi::Error> {
430+
if let Some(fd) = &self.fd {
431+
Ok(fd.as_raw_fd())
432+
} else {
433+
Err(napi::Error::new(
434+
GenericFailure,
435+
"inotify fd has already been closed",
436+
))
437+
}
438+
}
439+
440+
/// Register one directory to be watched. Events for close-after-write, renames, and deletions
441+
/// will be registered. Events for creation and modification will be ignored. Returns a watch
442+
/// descriptor, which can be used in `remove_watch`.
443+
#[napi]
444+
#[allow(dead_code)]
445+
pub fn add_close_write(&self, dir: String) -> Result<i32, napi::Error> {
446+
let cstring_dir = match CString::new(dir.as_str()) {
447+
Ok(cstring_dir) => cstring_dir,
448+
Err(err) => {
449+
return Err(napi::Error::new(
450+
GenericFailure,
451+
format!("CString::new: {}", err),
452+
));
453+
}
454+
};
455+
if let Some(fd) = &self.fd {
456+
match Errno::result(unsafe {
457+
libc::inotify_add_watch(
458+
fd.as_raw_fd(),
459+
cstring_dir.as_c_str().as_ptr(),
460+
(AddWatchFlags::IN_CLOSE_WRITE | AddWatchFlags::IN_MOVED_TO | AddWatchFlags::IN_DELETE)
461+
.bits(),
462+
)
463+
}) {
464+
Ok(wd) => Ok(wd),
465+
Err(err) => Err(napi::Error::new(
466+
GenericFailure,
467+
format!("inotify_add_watch: {}", err),
468+
)),
469+
}
470+
} else {
471+
Err(napi::Error::new(
472+
GenericFailure,
473+
"inotify fd has already been closed",
474+
))
475+
}
476+
}
477+
478+
/// Stop watching the watch descriptor provided.
479+
#[napi]
480+
#[allow(dead_code)]
481+
pub fn remove_watch(&self, wd: i32) -> Result<(), napi::Error> {
482+
if let Some(fd) = &self.fd {
483+
if let Err(err) = Errno::result(unsafe { libc::inotify_rm_watch(fd.as_raw_fd(), wd) }) {
484+
Err(napi::Error::new(
485+
GenericFailure,
486+
format!("inotify_remove_watch: {}", err),
487+
))
488+
} else {
489+
Ok(())
490+
}
491+
} else {
492+
Err(napi::Error::new(
493+
GenericFailure,
494+
"inotify fd has already been closed",
495+
))
496+
}
497+
}
498+
}
499+
500+
#[cfg(target_os = "linux")]
501+
#[napi]
502+
#[allow(dead_code)]
503+
pub const IN_CLOSE_WRITE: u32 = AddWatchFlags::IN_CLOSE_WRITE.bits();
504+
505+
#[cfg(target_os = "linux")]
506+
#[napi]
507+
#[allow(dead_code)]
508+
pub const IN_MOVED_FROM: u32 = AddWatchFlags::IN_MOVED_FROM.bits();
509+
510+
#[cfg(target_os = "linux")]
511+
#[napi]
512+
#[allow(dead_code)]
513+
pub const IN_MOVED_TO: u32 = AddWatchFlags::IN_MOVED_TO.bits();
514+
515+
#[cfg(target_os = "linux")]
516+
#[napi]
517+
#[allow(dead_code)]
518+
pub const IN_CREATE: u32 = AddWatchFlags::IN_CREATE.bits();
519+
520+
#[cfg(target_os = "linux")]
521+
#[napi]
522+
#[allow(dead_code)]
523+
pub const IN_DELETE: u32 = AddWatchFlags::IN_DELETE.bits();
524+
525+
#[cfg(not(target_os = "linux"))]
526+
#[napi]
527+
impl Inotify {
528+
#[napi(constructor)]
529+
#[allow(dead_code)]
530+
pub fn new() -> Result<Self, napi::Error> {
531+
Err(napi::Error::new(
532+
GenericFailure,
533+
format!("inotify not supported in non-Linux"),
534+
))
535+
}
536+
537+
#[napi]
538+
#[allow(dead_code)]
539+
pub fn close(&mut self) -> Result<(), napi::Error> {
540+
Err(napi::Error::new(
541+
GenericFailure,
542+
format!("inotify not supported in non-Linux"),
543+
))
544+
}
545+
546+
#[napi]
547+
#[allow(dead_code)]
548+
pub fn fd(&self) -> Result<c_int, napi::Error> {
549+
Err(napi::Error::new(
550+
GenericFailure,
551+
format!("inotify not supported in non-Linux"),
552+
))
553+
}
554+
555+
#[napi]
556+
#[allow(dead_code)]
557+
pub fn add_close_write(&self, dir: String) -> Result<i32, napi::Error> {
558+
Err(napi::Error::new(
559+
GenericFailure,
560+
format!("inotify not supported in non-Linux"),
561+
))
562+
}
563+
564+
#[napi]
565+
#[allow(dead_code)]
566+
pub fn remove_watch(&self, wd: i32) -> Result<(), napi::Error> {
567+
Err(napi::Error::new(
568+
GenericFailure,
569+
format!("inotify not supported in non-Linux"),
570+
))
571+
}
572+
}
573+
574+
#[cfg(not(target_os = "linux"))]
575+
#[napi]
576+
#[allow(dead_code)]
577+
pub const IN_CLOSE_WRITE: u32 = 0;
578+
579+
#[cfg(not(target_os = "linux"))]
580+
#[napi]
581+
#[allow(dead_code)]
582+
pub const IN_MOVED_FROM: u32 = 0;
583+
584+
#[cfg(not(target_os = "linux"))]
585+
#[napi]
586+
#[allow(dead_code)]
587+
pub const IN_MOVED_TO: u32 = 0;
588+
589+
#[cfg(not(target_os = "linux"))]
590+
#[napi]
591+
#[allow(dead_code)]
592+
pub const IN_CREATE: u32 = 0;
593+
594+
#[cfg(not(target_os = "linux"))]
595+
#[napi]
596+
#[allow(dead_code)]
597+
pub const IN_DELETE: u32 = 0;

0 commit comments

Comments
 (0)