Skip to content

Commit

Permalink
Inotify
Browse files Browse the repository at this point in the history
  • Loading branch information
lhchavez committed Aug 18, 2024
1 parent 9b65d88 commit dae9846
Show file tree
Hide file tree
Showing 6 changed files with 640 additions and 11 deletions.
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ libc = "0.2.152"
# Default enable napi4 feature, see https://nodejs.org/api/n-api.html#node-api-version-matrix
napi = { version = "2.12.2", default-features = false, features = ["napi4"] }
napi-derive = "2.12.2"
nix = { version = "0.29.0", features = ["fs", "term", "poll"] }
nix = { version = "0.29.0", features = ["fs", "term", "poll", "inotify"] }

[build-dependencies]
napi-build = "2.0.1"
Expand Down
40 changes: 37 additions & 3 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,17 +19,25 @@ export interface Size {
rows: number
}
/** Resize the terminal. */
export function ptyResize(fd: number, size: Size): void
export declare function ptyResize(fd: number, size: Size): void
/**
* Set the close-on-exec flag on a file descriptor. This is `fcntl(fd, F_SETFD, FD_CLOEXEC)` under
* the covers.
*/
export function setCloseOnExec(fd: number, closeOnExec: boolean): void
export declare function setCloseOnExec(fd: number, closeOnExec: boolean): void
/**
* Get the close-on-exec flag on a file descriptor. This is `fcntl(fd, F_GETFD) & FD_CLOEXEC ==
*_CLOEXEC` under the covers.
*/
export function getCloseOnExec(fd: number): boolean
export declare function getCloseOnExec(fd: number): boolean
export const IN_CLOSE_WRITE: number
export const IN_MOVED_FROM: number
export const IN_MOVED_TO: number
export const IN_CREATE: number
export const IN_DELETE: number
export const IN_IGNORED: number
export const IN_Q_OVERFLOW: number
export const IN_UNMOUNT: number
export class Pty {
/** The pid of the forked process. */
pid: number
Expand All @@ -41,3 +49,29 @@ export class Pty {
*/
takeFd(): c_int
}
/**
* A way to access Linux' `inotify(7)` subsystem. For simplicity, this only allows subscribing for
* events on directories (instead of files) and only for modify-close and rename events.
*/
export class Inotify {
constructor()
/**
* Close the inotify file descriptor. Must be called at most once to avoid file descriptor
* leaks.
*/
close(): void
/**
* Borrow the file descriptor. It is expected that Nod does not close the file descriptor and
* instead the .close() method should be called to clean the file descriptor up. Read the file
* descriptor on node according to `inotify(7)` to get events.
*/
fd(): c_int
/**
* Register one directory to be watched. Events for close-after-write, renames, and deletions
* will be registered. Events for creation and modification will be ignored. Returns a watch
* descriptor, which can be used in `remove_watch`.
*/
addCloseWrite(dir: string): number
/** Stop watching the watch descriptor provided. */
removeWatch(wd: number): void
}
11 changes: 10 additions & 1 deletion index.js
Original file line number Diff line number Diff line change
Expand Up @@ -310,9 +310,18 @@ if (!nativeBinding) {
throw new Error(`Failed to load native binding`)
}

const { Pty, ptyResize, setCloseOnExec, getCloseOnExec } = nativeBinding
const { Pty, ptyResize, setCloseOnExec, getCloseOnExec, Inotify, IN_CLOSE_WRITE, IN_MOVED_FROM, IN_MOVED_TO, IN_CREATE, IN_DELETE, IN_IGNORED, IN_Q_OVERFLOW, IN_UNMOUNT } = nativeBinding

module.exports.Pty = Pty
module.exports.ptyResize = ptyResize
module.exports.setCloseOnExec = setCloseOnExec
module.exports.getCloseOnExec = getCloseOnExec
module.exports.Inotify = Inotify
module.exports.IN_CLOSE_WRITE = IN_CLOSE_WRITE
module.exports.IN_MOVED_FROM = IN_MOVED_FROM
module.exports.IN_MOVED_TO = IN_MOVED_TO
module.exports.IN_CREATE = IN_CREATE
module.exports.IN_DELETE = IN_DELETE
module.exports.IN_IGNORED = IN_IGNORED
module.exports.IN_Q_OVERFLOW = IN_Q_OVERFLOW
module.exports.IN_UNMOUNT = IN_UNMOUNT
254 changes: 254 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -371,3 +371,257 @@ fn set_nonblocking(fd: i32) -> Result<(), napi::Error> {
}
Ok(())
}

#[cfg(target_os = "linux")]
use nix::sys::inotify::{AddWatchFlags, InitFlags};
#[cfg(target_os = "linux")]
use std::ffi::CString;

/// A way to access Linux' `inotify(7)` subsystem. For simplicity, this only allows subscribing for
/// events on directories (instead of files) and only for modify-close and rename events.
#[napi]
#[allow(dead_code)]
struct Inotify {
fd: Option<OwnedFd>,
}

#[cfg(target_os = "linux")]
#[napi]
#[allow(dead_code)]
impl Inotify {
#[napi(constructor)]
pub fn new() -> Result<Self, napi::Error> {
let fd = match Errno::result(unsafe {
libc::inotify_init1((InitFlags::IN_CLOEXEC | InitFlags::IN_NONBLOCK).bits())
}) {
Ok(fd) => unsafe { OwnedFd::from_raw_fd(fd) },
Err(err) => {
return Err(napi::Error::new(
GenericFailure,
format!("inotify_init: {}", err),
));
}
};
Ok(Inotify { fd: Some(fd) })
}

/// Close the inotify file descriptor. Must be called at most once to avoid file descriptor
/// leaks.
#[napi]
#[allow(dead_code)]
pub fn close(&mut self) -> Result<(), napi::Error> {
let inotify = self.fd.take();
if inotify.is_none() {
return Err(napi::Error::new(
GenericFailure,
"inotify fd has already been closed",
));
}

Ok(())
}

/// Borrow the file descriptor. It is expected that Nod does not close the file descriptor and
/// instead the .close() method should be called to clean the file descriptor up. Read the file
/// descriptor on node according to `inotify(7)` to get events.
#[napi]
#[allow(dead_code)]
pub fn fd(&self) -> Result<c_int, napi::Error> {
if let Some(fd) = &self.fd {
Ok(fd.as_raw_fd())
} else {
Err(napi::Error::new(
GenericFailure,
"inotify fd has already been closed",
))
}
}

/// Register one directory to be watched. Events for close-after-write, renames, and deletions
/// will be registered. Events for creation and modification will be ignored. Returns a watch
/// descriptor, which can be used in `remove_watch`.
#[napi]
#[allow(dead_code)]
pub fn add_close_write(&self, dir: String) -> Result<i32, napi::Error> {
let cstring_dir = match CString::new(dir.as_str()) {
Ok(cstring_dir) => cstring_dir,
Err(err) => {
return Err(napi::Error::new(
GenericFailure,
format!("CString::new: {}", err),
));
}
};
if let Some(fd) = &self.fd {
match Errno::result(unsafe {
libc::inotify_add_watch(
fd.as_raw_fd(),
cstring_dir.as_c_str().as_ptr(),
(AddWatchFlags::IN_CLOSE_WRITE | AddWatchFlags::IN_MOVED_TO | AddWatchFlags::IN_DELETE)
.bits(),
)
}) {
Ok(wd) => Ok(wd),
Err(err) => Err(napi::Error::new(
GenericFailure,
format!("inotify_add_watch: {}", err),
)),
}
} else {
Err(napi::Error::new(
GenericFailure,
"inotify fd has already been closed",
))
}
}

/// Stop watching the watch descriptor provided.
#[napi]
#[allow(dead_code)]
pub fn remove_watch(&self, wd: i32) -> Result<(), napi::Error> {
if let Some(fd) = &self.fd {
if let Err(err) = Errno::result(unsafe { libc::inotify_rm_watch(fd.as_raw_fd(), wd) }) {
Err(napi::Error::new(
GenericFailure,
format!("inotify_remove_watch: {}", err),
))
} else {
Ok(())
}
} else {
Err(napi::Error::new(
GenericFailure,
"inotify fd has already been closed",
))
}
}
}

#[cfg(target_os = "linux")]
#[napi]
#[allow(dead_code)]
pub const IN_CLOSE_WRITE: u32 = AddWatchFlags::IN_CLOSE_WRITE.bits();

#[cfg(target_os = "linux")]
#[napi]
#[allow(dead_code)]
pub const IN_MOVED_FROM: u32 = AddWatchFlags::IN_MOVED_FROM.bits();

#[cfg(target_os = "linux")]
#[napi]
#[allow(dead_code)]
pub const IN_MOVED_TO: u32 = AddWatchFlags::IN_MOVED_TO.bits();

#[cfg(target_os = "linux")]
#[napi]
#[allow(dead_code)]
pub const IN_CREATE: u32 = AddWatchFlags::IN_CREATE.bits();

#[cfg(target_os = "linux")]
#[napi]
#[allow(dead_code)]
pub const IN_DELETE: u32 = AddWatchFlags::IN_DELETE.bits();

#[cfg(target_os = "linux")]
#[napi]
#[allow(dead_code)]
pub const IN_IGNORED: u32 = AddWatchFlags::IN_IGNORED.bits();

#[cfg(target_os = "linux")]
#[napi]
#[allow(dead_code)]
pub const IN_Q_OVERFLOW: u32 = AddWatchFlags::IN_Q_OVERFLOW.bits();

#[cfg(target_os = "linux")]
#[napi]
#[allow(dead_code)]
pub const IN_UNMOUNT: u32 = AddWatchFlags::IN_UNMOUNT.bits();

#[cfg(not(target_os = "linux"))]
#[napi]
impl Inotify {
#[napi(constructor)]
#[allow(dead_code)]
pub fn new() -> Result<Self, napi::Error> {
Err(napi::Error::new(
GenericFailure,
format!("inotify not supported in non-Linux"),
))
}

#[napi]
#[allow(dead_code)]
pub fn close(&mut self) -> Result<(), napi::Error> {
Err(napi::Error::new(
GenericFailure,
format!("inotify not supported in non-Linux"),
))
}

#[napi]
#[allow(dead_code)]
pub fn fd(&self) -> Result<c_int, napi::Error> {
Err(napi::Error::new(
GenericFailure,
format!("inotify not supported in non-Linux"),
))
}

#[napi]
#[allow(dead_code)]
pub fn add_close_write(&self, dir: String) -> Result<i32, napi::Error> {
Err(napi::Error::new(
GenericFailure,
format!("inotify not supported in non-Linux"),
))
}

#[napi]
#[allow(dead_code)]
pub fn remove_watch(&self, wd: i32) -> Result<(), napi::Error> {
Err(napi::Error::new(
GenericFailure,
format!("inotify not supported in non-Linux"),
))
}
}

#[cfg(not(target_os = "linux"))]
#[napi]
#[allow(dead_code)]
pub const IN_CLOSE_WRITE: u32 = 0;

#[cfg(not(target_os = "linux"))]
#[napi]
#[allow(dead_code)]
pub const IN_MOVED_FROM: u32 = 0;

#[cfg(not(target_os = "linux"))]
#[napi]
#[allow(dead_code)]
pub const IN_MOVED_TO: u32 = 0;

#[cfg(not(target_os = "linux"))]
#[napi]
#[allow(dead_code)]
pub const IN_CREATE: u32 = 0;

#[cfg(not(target_os = "linux"))]
#[napi]
#[allow(dead_code)]
pub const IN_DELETE: u32 = 0;

#[cfg(not(target_os = "linux"))]
#[napi]
#[allow(dead_code)]
pub const IN_IGNORED: u32 = 0;

#[cfg(not(target_os = "linux"))]
#[napi]
#[allow(dead_code)]
pub const IN_Q_OVERFLOW: u32 = 0;

#[cfg(not(target_os = "linux"))]
#[napi]
#[allow(dead_code)]
pub const IN_UNMOUNT: u32 = 0;
Loading

0 comments on commit dae9846

Please sign in to comment.