/**
|
* dxStd Standard Library Module.
|
*
|
* This module provides a comprehensive standard library for DejaOS, wrapping and extending
|
* the built-in 'os' and 'std' modules. It offers a unified interface for interacting
|
* with the operating system, including file I/O, timers, environment variables, and more.
|
*
|
* Features:
|
* - High-level wrappers for file system operations (read, write, stat, etc.).
|
* - Robust, non-blocking timer functions (`setInterval`, `setTimeout`).
|
* - Process and environment variable management.
|
* - Worker (threading) support.
|
* - Utility functions for common tasks like random string generation.
|
*
|
* Usage:
|
* - Import the module: `import dxstd from "./dxmodules/dxStd.js";`
|
* - Use the functions: `dxstd.saveFile("/app/data/test.txt", "Hello, DejaOS!");`, `dxstd.sleep(1000);`
|
*/
|
import * as os from "os"
|
import * as std from "std"
|
|
const dxstd = {}
|
|
// --- Constants ---
|
// File open flags from 'os' module
|
dxstd.O_RDONLY = os.O_RDONLY
|
dxstd.O_WRONLY = os.O_WRONLY
|
dxstd.O_RDWR = os.O_RDWR
|
dxstd.O_APPEND = os.O_APPEND
|
dxstd.O_CREAT = os.O_CREAT
|
dxstd.O_EXCL = os.O_EXCL
|
dxstd.O_TRUNC = os.O_TRUNC
|
|
// File seek flags from 'std' module
|
dxstd.SEEK_SET = std.SEEK_SET
|
dxstd.SEEK_CUR = std.SEEK_CUR
|
dxstd.SEEK_END = std.SEEK_END
|
|
// File mode flags from 'os' module
|
dxstd.S_IFMT = os.S_IFMT
|
dxstd.S_IFIFO = os.S_IFIFO
|
dxstd.S_IFCHR = os.S_IFCHR
|
dxstd.S_IFDIR = os.S_IFDIR
|
dxstd.S_IFBLK = os.S_IFBLK
|
dxstd.S_IFREG = os.S_IFREG
|
dxstd.S_IFSOCK = os.S_IFSOCK
|
dxstd.S_IFLNK = os.S_IFLNK
|
dxstd.S_ISGID = os.S_ISGID
|
dxstd.S_ISUID = os.S_ISUID
|
|
// --- Standard I/O Streams ---
|
// Expose the raw standard I/O streams for direct access when needed.
|
dxstd.in = std.in;
|
dxstd.out = std.out;
|
dxstd.err = std.err;
|
|
|
/**
|
* Exits the application.
|
* @param {number} n - The exit code.
|
*/
|
dxstd.exit = function (n) {
|
std.exit(n);
|
}
|
/**
|
* Starts a timer to execute a function asynchronously after a delay.
|
* @param {function} func - The function to execute.
|
* @param {number} delay - The delay in milliseconds.
|
* @returns {*} A timer handle that can be used with clearTimeout.
|
*/
|
dxstd.setTimeout = function (func, delay) {
|
return os.setTimeout(func, delay)
|
}
|
/**
|
* Clears a specified timer.
|
* @param {*} handle - The timer handle returned by setTimeout.
|
*/
|
dxstd.clearTimeout = function (handle) {
|
os.clearTimeout(handle)
|
}
|
// Map to store timer IDs for clearing. Only timers created in the current thread can be cleared.
|
let allTimerIdsMap = {}
|
|
/**
|
* Sets up a recurring timer.
|
* @param {function} callback - The function to be called repeatedly. Required.
|
* @param {number} interval - The interval time in milliseconds. Required.
|
* @param {boolean} [once] - If true, executes the callback once immediately after creation. Optional.
|
* @note Any exception thrown inside the callback will be caught and logged to the standard error stream.
|
* The interval will continue to run. This implementation uses a recursive setTimeout, which means
|
* the interval will be delayed by the execution time of the callback. This can lead to "timer drift"
|
* over long periods.
|
* @returns {string} The unique timer ID for this interval, which can be used with clearInterval.
|
*/
|
dxstd.setInterval = function (callback, interval, once) {
|
const timerId = this.genRandomStr(20);
|
|
// The state for each timer is stored in a map.
|
// The `handle` stores the native setTimeout identifier.
|
allTimerIdsMap[timerId] = { handle: null };
|
|
const tick = () => {
|
// If the timerId was cleared from the map, it means clearInterval was called.
|
// We stop the loop.
|
if (!allTimerIdsMap[timerId]) {
|
return;
|
}
|
|
try {
|
callback();
|
} catch (e) {
|
std.err.puts(`Error in setInterval callback for timerId ${timerId}: ${e.message || e}\n${e.stack || ''}\n`);
|
std.err.flush();
|
}
|
|
// After the callback executes, we check again.
|
// This prevents rescheduling if clearInterval was called inside the callback.
|
if (allTimerIdsMap[timerId]) {
|
allTimerIdsMap[timerId].handle = os.setTimeout(tick, interval);
|
}
|
};
|
|
// If 'once' is true, execute the callback immediately, with exception handling.
|
if (once) {
|
try {
|
callback();
|
} catch (e) {
|
std.err.puts(`Error in setInterval 'once' callback for timerId ${timerId}: ${e.message || e}\n${e.stack || ''}\n`);
|
std.err.flush();
|
}
|
}
|
|
// Schedule the first tick.
|
allTimerIdsMap[timerId].handle = os.setTimeout(tick, interval);
|
|
return timerId;
|
}
|
/**
|
* Clears an interval timer.
|
* @param {string} timerId - The ID of the timer to clear. Required.
|
*/
|
dxstd.clearInterval = function (timerId) {
|
const timerState = allTimerIdsMap[timerId];
|
if (timerState) {
|
// Clear the scheduled native timer.
|
if (timerState.handle) {
|
os.clearTimeout(timerState.handle);
|
}
|
// By removing the entry from the map, we signal to any pending 'tick' function
|
// that it should not reschedule itself. This prevents race conditions.
|
delete allTimerIdsMap[timerId];
|
}
|
}
|
/**
|
* Clears all interval timers created in the current thread.
|
* Note: This only clears timers created by the calling thread. If multiple threads exist,
|
* each must call this function to clear its own timers.
|
*/
|
dxstd.clearIntervalAll = function () {
|
const allIds = Object.keys(allTimerIdsMap);
|
for (const timerId of allIds) {
|
this.clearInterval(timerId);
|
}
|
};
|
/**
|
* Generates a random string of a specified length consisting of letters and numbers.
|
* @param {number} [length=6] - The length of the string. Optional.
|
* @returns {string} The generated random string.
|
* @note This function is based on Math.random() and is not cryptographically secure. For security-sensitive applications, it is recommended to use the `dxCommonUtils.random` module.
|
*/
|
dxstd.genRandomStr = function (length = 6) {
|
if (typeof length !== 'number' || length < 0 || length > 100) {
|
throw new Error('Length must be a non-negative number <= 100');
|
}
|
const charset = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
|
let result = ''
|
for (let i = 0; i < length; i++) {
|
const randomIndex = Math.floor(Math.random() * charset.length)
|
result += charset.charAt(randomIndex)
|
}
|
return result
|
}
|
/**
|
* Executes a string as a JavaScript script.
|
* @param {string} str - The JavaScript script string.
|
* @param {boolean} [async=false] - If true, the script can use 'await' and the function returns a Promise.
|
* @returns {*} The result of the script evaluation.
|
*/
|
dxstd.eval = function (str, async) {
|
return std.evalScript(str, { async: async });
|
}
|
/**
|
* Loads and executes the content of a file as a JavaScript script.
|
* @param {string} filename - The absolute path of the script file.
|
* @returns {*} The result of the script evaluation.
|
*/
|
dxstd.loadScript = function (filename) {
|
return std.loadScript(filename);
|
}
|
/**
|
* Loads the content of a file as a UTF-8 string.
|
* @param {string} filename - The name of the file.
|
* @returns {string} The content of the file.
|
*/
|
dxstd.loadFile = function (filename) {
|
return std.loadFile(filename)
|
}
|
/**
|
* Saves a string to a file. Creates the directory path if it doesn't exist.
|
* @param {string} filename - The absolute path of the file.
|
* @param {string} content - The string content to save. An empty string is allowed.
|
* @param {boolean} [sync=true] - If true, performs a system-wide sync to flush all file system buffers to disk, ensuring data reliability at the cost of performance.
|
* @returns {boolean} Returns true on success.
|
* @throws {Error} If content is not a string, filename is empty, or the file cannot be saved.
|
*/
|
dxstd.saveFile = function (filename, content, sync = true) {
|
if (typeof content !== 'string') {
|
throw new Error("The 'content' value must be a string")
|
}
|
if (!filename) {
|
throw new Error("The 'filename' should not be empty")
|
}
|
// This is still needed to create parent directories if they don't exist.
|
this.ensurePathExists(filename)
|
let fd = null
|
try {
|
// The "w" mode means: open for writing, create if it does not exist, and truncate it to zero length if it does.
|
fd = std.open(filename, "w");
|
fd.puts(content)
|
fd.flush();
|
} catch (e) {
|
throw new Error(`Failed to save file '${filename}': ${e}`);
|
} finally {
|
if (fd) {
|
fd.close();
|
}
|
}
|
// 'sync' is a heavy operation that forces all buffered data to disk across the system.
|
// This improves reliability at the cost of performance.
|
if (sync) {
|
os.exec(['sync']);
|
}
|
return true
|
}
|
|
/**
|
* Saves binary data to a file. Creates the directory path if it doesn't exist.
|
* Supports ArrayBuffer and Uint8Array, with optional offset and length.
|
* @param {string} filename - The absolute path of the file.
|
* @param {ArrayBuffer|Uint8Array} data - The binary data to save.
|
* @param {number} [offset=0] - Byte offset in data to start writing from.
|
* @param {number} [length=data.byteLength] - Number of bytes to write.
|
* @param {boolean} [sync=true] - If true, performs a system-wide sync to flush buffers to disk.
|
* @returns {boolean} Returns true on success.
|
* @throws {Error} If parameters are invalid or file operation fails.
|
*/
|
dxstd.saveBinaryFile = function (filename, data, offset = 0, length = undefined, sync = true) {
|
if (!(data instanceof ArrayBuffer) && !(data instanceof Uint8Array)) {
|
throw new Error("The 'data' must be an ArrayBuffer or a Uint8Array");
|
}
|
if (!filename) throw new Error("The 'filename' should not be empty");
|
|
// Work with Uint8Array as it's a view and has consistent properties.
|
const buf = data instanceof Uint8Array ? data : new Uint8Array(data);
|
const writeLength = length !== undefined ? length : (buf.byteLength - offset);
|
|
if (offset < 0 || writeLength < 0 || offset + writeLength > buf.byteLength) {
|
throw new Error("Invalid offset or length for data buffer");
|
}
|
|
this.ensurePathExists(filename);
|
let fd = -1;
|
try {
|
fd = this.open(filename, this.O_WRONLY | this.O_CREAT | this.O_TRUNC);
|
if (fd < 0) throw new Error(`Failed to open file. Code: ${fd}`);
|
|
if (writeLength > 0) {
|
// The native os.write function strictly expects an ArrayBuffer, not a view.
|
// We must provide the underlying buffer and the absolute byte offset within that buffer.
|
const absoluteOffset = buf.byteOffset + offset;
|
const bytesWritten = this.write(fd, buf.buffer, absoluteOffset, writeLength);
|
if (bytesWritten < 0) throw new Error(`Write operation failed. Code: ${bytesWritten}`);
|
if (bytesWritten !== writeLength) {
|
throw new Error(`Partial write: expected ${writeLength} bytes, wrote ${bytesWritten}`);
|
}
|
}
|
} catch (e) {
|
throw new Error(`Failed to save binary file '${filename}': ${e.message || e}`);
|
} finally {
|
if (fd >= 0) this.close(fd);
|
}
|
|
if (sync) os.exec(['sync']);
|
return true;
|
};
|
|
/**
|
* Loads a portion or the entire content of a file as binary data into a Uint8Array.
|
* @param {string} filename - The absolute path of the file.
|
* @param {number} [offset=0] - Byte offset in the file to start reading from.
|
* @param {number} [length] - Number of bytes to read. Defaults to reading from the offset to the end of the file.
|
* @returns {Uint8Array} A Uint8Array containing the specified portion of the file's binary data.
|
* @throws {Error} If the file does not exist, the path is a directory, or parameters are invalid.
|
*/
|
dxstd.loadBinaryFile = function (filename, offset = 0, length = undefined) {
|
if (!filename) throw new Error("The 'filename' should not be empty");
|
|
const [stat, err] = this.stat(filename);
|
if (err) throw new Error(`Cannot access file '${filename}'. Code: ${err}`);
|
if ((stat.mode & this.S_IFMT) === this.S_IFDIR) throw new Error(`Path '${filename}' is a directory`);
|
|
const fileSize = stat.size;
|
|
const readOffset = offset;
|
const readLength = length !== undefined ? length : (fileSize - readOffset);
|
|
if (readOffset < 0 || readLength < 0 || readOffset + readLength > fileSize) {
|
throw new Error("Invalid offset or length for reading file");
|
}
|
|
if (readLength === 0) {
|
return new Uint8Array(0);
|
}
|
|
let fd = -1;
|
try {
|
fd = this.open(filename, this.O_RDONLY);
|
if (fd < 0) throw new Error(`Failed to open file. Code: ${fd}`);
|
|
// Move the file pointer to the desired offset before reading.
|
this.seek(fd, readOffset, std.SEEK_SET);
|
|
// Read into a new buffer. The offset for the buffer itself is 0.
|
const buffer = new ArrayBuffer(readLength);
|
const bytesRead = this.read(fd, buffer, 0, readLength);
|
|
if (bytesRead < 0) throw new Error(`Read operation failed. Code: ${bytesRead}`);
|
if (bytesRead !== readLength) throw new Error(`Partial read: expected ${readLength} bytes, got ${bytesRead}`);
|
|
return new Uint8Array(buffer);
|
} catch (e) {
|
throw new Error(`Failed to load binary file '${filename}': ${e.message || e}`);
|
} finally {
|
if (fd >= 0) this.close(fd);
|
}
|
};
|
|
/**
|
* Ensures that the directory path for a given filename exists. Creates it if necessary.
|
* @param {string} filename - The full path of the file.
|
*/
|
dxstd.ensurePathExists = function (filename) {
|
const pathSegments = filename.split('/');
|
let currentPath = '';
|
// We iterate to pathSegments.length - 1 because the last segment is the filename.
|
for (let i = 0; i < pathSegments.length - 1; i++) {
|
// Handle root directory case
|
if (i === 0 && pathSegments[i] === '') {
|
currentPath = '/';
|
continue;
|
}
|
currentPath += pathSegments[i] + '/';
|
|
const [st, err] = os.stat(currentPath);
|
|
if (!err) {
|
// Path exists, check if it is a directory.
|
if ((st.mode & this.S_IFMT) !== this.S_IFDIR) {
|
throw new Error(`Path component '${currentPath}' exists but is not a directory`);
|
}
|
} else {
|
// Path does not exist, create it.
|
// We assume any error from stat means the path needs to be created.
|
// A subsequent mkdir failure (e.g., permission denied) will throw an error.
|
this.mkdir(currentPath);
|
}
|
}
|
}
|
/**
|
* Checks if a file or directory exists.
|
* @param {string} filename - The name of the file or directory.
|
* @returns {boolean} True if the file exists, false otherwise.
|
*/
|
dxstd.exist = function (filename) {
|
const [, err] = os.stat(filename);
|
return err === 0;
|
}
|
/**
|
* Returns an object containing key-value pairs of the environment variables.
|
* @returns {object} The environment variables.
|
*/
|
dxstd.getenviron = function () {
|
return std.getenviron();
|
}
|
/**
|
* Returns the value of an environment variable.
|
* @param {string} name - The name of the variable.
|
* @returns {string|undefined} The value of the variable, or undefined if not defined.
|
*/
|
dxstd.getenv = function (name) {
|
return std.getenv(name);
|
}
|
/**
|
* Sets the value of an environment variable.
|
* @param {string} name - The name of the variable.
|
* @param {string} value - The value to set.
|
*/
|
dxstd.setenv = function (name, value) {
|
return std.setenv(name, value);
|
}
|
/**
|
* Deletes an environment variable.
|
* @param {string} name - The name of the variable to delete.
|
*/
|
dxstd.unsetenv = function (name) {
|
return std.unsetenv(name);
|
}
|
/**
|
* Parses a string using a superset of JSON.parse. It can parse non-standard JSON strings.
|
* It accepts the following extensions:
|
* - single line and multi-line comments
|
* - unquoted properties (JavaScript identifiers with only ASCII chars)
|
* - trailing comma in array and objects
|
* - single quoted strings
|
* - `\f` and `\v` are accepted as space characters
|
* - leading plus in numbers
|
* - octal (0o prefix) and hexadecimal (0x prefix) numbers
|
* @param {string} str - The JSON string to parse.
|
* @returns {*} The parsed object.
|
*/
|
dxstd.parseExtJSON = function (str) {
|
return std.parseExtJSON(str);
|
}
|
/**
|
* Sleeps for a specified number of milliseconds.
|
* @param {number} delay_ms - The delay in milliseconds.
|
*/
|
dxstd.sleep = function (delay_ms) {
|
return os.sleep(delay_ms);
|
}
|
/**
|
* Returns a string representing the platform: "linux", "darwin", "win32", or "js".
|
* @returns {string} The platform identifier.
|
*/
|
dxstd.platform = function () {
|
return os.platform;
|
}
|
/**
|
* Creates a new thread (worker). The API is close to the WebWorkers API.
|
* For dynamically imported modules, the path is relative to the current script or module.
|
* Threads do not share any data by default, but can share and pass data via dxMap, dxQueue, dxWpc.
|
* Nested workers are not supported.
|
* @param {string} module_filename - The module filename to be executed in the new thread. should be absolute path.
|
* @returns {os.Worker} A new Worker instance.
|
*/
|
dxstd.Worker = function (module_filename) {
|
return new os.Worker(module_filename)
|
}
|
|
/**
|
* Opens a file.
|
* @param {string} filename - The absolute path of the file.
|
* @param {number} flags - A bitwise OR of file open flags (e.g., `dxstd.O_RDWR | dxstd.O_CREAT`).
|
* @returns {number} A file descriptor handle, or a value < 0 on error.
|
* @note The 'flags' parameter must include one of `O_RDONLY`, `O_WRONLY`, or `O_RDWR`.
|
* Other common flags:
|
* - `O_APPEND`: Appends data to the end of the file on every write.
|
* - `O_CREAT`: Creates the file if it does not exist.
|
* - `O_EXCL`: Used with `O_CREAT`, ensures that the caller creates the file. Fails if the file already exists.
|
* - `O_TRUNC`: Truncates the file to zero length if it exists.
|
*/
|
dxstd.open = function (filename, flags) {
|
return os.open(filename, flags);
|
}
|
/**
|
* Checks if a given path is a directory.
|
* @param {string} filename - The path to check.
|
* @returns {boolean} True if the path is a directory, false otherwise.
|
* @throws {Error} If the path does not exist.
|
*/
|
dxstd.isDir = function (filename) {
|
const [stat, err] = os.stat(filename)
|
if (err) {
|
throw new Error(`Cannot stat path '${filename}': error code ${err}`)
|
}
|
return ((stat.mode & this.S_IFMT) === this.S_IFDIR);
|
}
|
/**
|
* Closes a file descriptor.
|
* @param {number} fd - The file descriptor handle.
|
*/
|
dxstd.close = function (fd) {
|
return os.close(fd)
|
}
|
|
/**
|
* Seeks to a position in a file.
|
* @param {number} fd - The file descriptor handle.
|
* @param {number | bigint} offset - The offset. Can be positive or negative.
|
* @param {number} whence - The starting point for the offset: `dxstd.SEEK_SET` (start), `dxstd.SEEK_CUR` (current), `dxstd.SEEK_END` (end).
|
* @returns {number | bigint} The new offset from the beginning of the file.
|
*/
|
dxstd.seek = function (fd, offset, whence) {
|
return os.seek(fd, offset, whence)
|
}
|
/**
|
* Reads `length` bytes from file descriptor `fd` into an ArrayBuffer `buffer` at byte position `offset`.
|
* @param {number} fd - The file descriptor handle.
|
* @param {ArrayBuffer} buffer - The ArrayBuffer to read into.
|
* @param {number} offset - The offset in the buffer to start writing at.
|
* @param {number} length - The number of bytes to read.
|
* @returns {number} The number of bytes read, or a value < 0 on error.
|
*/
|
dxstd.read = function (fd, buffer, offset, length) {
|
return os.read(fd, buffer, offset, length);
|
}
|
/**
|
* Writes `length` bytes from an ArrayBuffer `buffer` at byte position `offset` to the file descriptor `fd`.
|
* @param {number} fd - The file descriptor handle.
|
* @param {ArrayBuffer} buffer - The ArrayBuffer to write from.
|
* @param {number} offset - The offset in the buffer to start reading from.
|
* @param {number} length - The number of bytes to write.
|
* @returns {number} The number of bytes written, or a value < 0 on error.
|
*/
|
dxstd.write = function (fd, buffer, offset, length) {
|
return os.write(fd, buffer, offset, length);
|
}
|
/**
|
* Deletes a file.
|
* @param {string} filename - The absolute path of the file.
|
* @returns {number} 0 on success, or -errno on failure.
|
*/
|
dxstd.remove = function (filename) {
|
return os.remove(filename)
|
}
|
|
/**
|
* Removes an empty directory by executing the system 'rmdir' command.
|
* @param {string} dirname The path to the directory.
|
* @returns {number} The exit code of the 'rmdir' command. Returns 0 on success.
|
* @note This relies on the 'rmdir' command being available in the system's PATH.
|
*/
|
dxstd.rmdir = function (dirname) {
|
return os.exec(['rmdir', dirname]);
|
}
|
|
/**
|
* Renames a file.
|
* @param {string} oldname - The old absolute path of the file.
|
* @param {string} newname - The new absolute path of the file.
|
* @returns {number} 0 on success, or -errno on failure.
|
*/
|
dxstd.rename = function (oldname, newname) {
|
return os.rename(oldname, newname)
|
}
|
/**
|
* Returns the current working directory.
|
* @returns {[string, number]} An array `[str, err]`, where `str` is the current working directory and `err` is an error code.
|
*/
|
dxstd.getcwd = function () {
|
return os.getcwd()
|
}
|
/**
|
* Changes the current working directory.
|
* @param {string} path - The directory path (absolute or relative).
|
* @returns {number} 0 on success, or -errno on failure.
|
*/
|
dxstd.chdir = function (path) {
|
return os.chdir(path)
|
}
|
/**
|
* Creates a directory.
|
* @param {string} path - The absolute path of the directory.
|
* @returns {number} 0 on success, or -errno on failure.
|
*/
|
dxstd.mkdir = function (path) {
|
return os.mkdir(path)
|
}
|
|
/**
|
* Returns status information for a file or directory.
|
* @param {string} path - The absolute path of the file or directory.
|
* @returns {[object, number]} An array `[obj, err]`, where `obj` is an object containing status information and `err` is an error code.
|
* The status object contains fields like `mode`, `size`, `mtime` (last modification time in ms since epoch), etc.
|
* The `mode` can be tested against the `S_*` constants, e.g., `(stat.mode & dxstd.S_IFMT) === dxstd.S_IFDIR`.
|
*/
|
dxstd.stat = function (path) {
|
return os.stat(path)
|
}
|
/**
|
* Same as `stat()`, but if the path is a symbolic link, it returns information about the link itself.
|
* @param {string} path - The absolute path of the file or directory.
|
* @returns {[object, number]} An array `[obj, err]`, containing status info or an error code.
|
*/
|
dxstd.lstat = function (path) {
|
return os.lstat(path)
|
}
|
/**
|
* Returns the names of the files in a directory.
|
* @param {string} path - The absolute path of the directory.
|
* @returns {[string[], number]} An array `[array, err]`, where `array` is an array of filenames and `err` is an error code.
|
*/
|
dxstd.readdir = function (path) {
|
return os.readdir(path)
|
}
|
export default dxstd
|