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

Inotify #40

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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 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