diff --git a/packages/php-wasm/node/src/lib/file-lock-manager-for-node.ts b/packages/php-wasm/node/src/lib/file-lock-manager-for-node.ts index 0a740dba59..a242b0d51a 100644 --- a/packages/php-wasm/node/src/lib/file-lock-manager-for-node.ts +++ b/packages/php-wasm/node/src/lib/file-lock-manager-for-node.ts @@ -13,7 +13,7 @@ import type { WholeFileLockOp, Pid, Fd, -} from './file-lock-manager'; +} from '@php-wasm/universal'; type LockMode = 'exclusive' | 'shared' | 'unlock'; diff --git a/packages/php-wasm/node/src/lib/index.ts b/packages/php-wasm/node/src/lib/index.ts index e27461ff33..0cc3d1ed89 100644 --- a/packages/php-wasm/node/src/lib/index.ts +++ b/packages/php-wasm/node/src/lib/index.ts @@ -3,6 +3,5 @@ export * from './networking/with-networking'; export * from './load-runtime'; export * from './use-host-filesystem'; export * from './node-fs-mount'; -export * from './file-lock-manager'; export * from './file-lock-manager-for-node'; export * from './xdebug/with-xdebug'; diff --git a/packages/php-wasm/node/src/lib/load-runtime.ts b/packages/php-wasm/node/src/lib/load-runtime.ts index 0b49ffba38..80ed303026 100644 --- a/packages/php-wasm/node/src/lib/load-runtime.ts +++ b/packages/php-wasm/node/src/lib/load-runtime.ts @@ -3,12 +3,12 @@ import type { EmscriptenOptions, PHPRuntime, RemoteAPI, + FileLockManager, } from '@php-wasm/universal'; import { loadPHPRuntime, FSHelpers } from '@php-wasm/universal'; import fs from 'fs'; import { getPHPLoaderModule } from '.'; import { withNetworking } from './networking/with-networking'; -import type { FileLockManager } from './file-lock-manager'; import { withXdebug, type XdebugOptions } from './xdebug/with-xdebug'; import { withIntl } from './extensions/intl/with-intl'; import { joinPaths } from '@php-wasm/util'; diff --git a/packages/php-wasm/universal/src/lib/emscripten-types.ts b/packages/php-wasm/universal/src/lib/emscripten-types.ts index e149cdab8e..d833ee7c18 100644 --- a/packages/php-wasm/universal/src/lib/emscripten-types.ts +++ b/packages/php-wasm/universal/src/lib/emscripten-types.ts @@ -149,7 +149,7 @@ export namespace Emscripten { export interface Mount { type: Emscripten.FileSystemType; - opts: object; + opts: Record; mountpoint: string; mounts: Mount[]; root: FSNode; @@ -185,6 +185,10 @@ export namespace Emscripten { write: boolean; readonly isFolder: boolean; readonly isDevice: boolean; + + // NOTE: As of 2025-11-11, this property is added by a php-wasm patch + // for NODEFS.createNode(). It is not part of the Emscripten FSNode interface. + readonly isSharedFS?: boolean; } export interface ErrnoError extends Error { @@ -394,6 +398,7 @@ export namespace Emscripten { export declare const MEMFS: Emscripten.FileSystemType; export declare const NODEFS: Emscripten.FileSystemType; export declare const IDBFS: Emscripten.FileSystemType; + export declare const PROXYFS: Emscripten.FileSystemType; // https://emscripten.org/docs/porting/connecting_cpp_and_javascript/Interacting-with-code.html type StringToType = R extends Emscripten.JSType diff --git a/packages/php-wasm/node/src/lib/file-lock-manager.ts b/packages/php-wasm/universal/src/lib/file-lock-manager.ts similarity index 100% rename from packages/php-wasm/node/src/lib/file-lock-manager.ts rename to packages/php-wasm/universal/src/lib/file-lock-manager.ts diff --git a/packages/php-wasm/universal/src/lib/index.ts b/packages/php-wasm/universal/src/lib/index.ts index fe0b27ff8d..b09b45b964 100644 --- a/packages/php-wasm/universal/src/lib/index.ts +++ b/packages/php-wasm/universal/src/lib/index.ts @@ -88,3 +88,5 @@ export { sandboxedSpawnHandlerFactory } from './sandboxed-spawn-handler-factory' export * from './api'; export type { WithAPIState as WithIsReady } from './api'; + +export type * from './file-lock-manager'; diff --git a/packages/php-wasm/universal/src/lib/os-kernel-space.ts b/packages/php-wasm/universal/src/lib/os-kernel-space.ts new file mode 100644 index 0000000000..65c231ef06 --- /dev/null +++ b/packages/php-wasm/universal/src/lib/os-kernel-space.ts @@ -0,0 +1,11 @@ +// TODO: Consider merging FileLockManager file with os-kernel-space. +import type { FileLockManager } from './file-lock-manager'; + +// TODO: Consider merging FileLockManager into this type. +export class OSKernelSpace { + readonly fileLockManager: FileLockManager; + + constructor(fileLockManager: FileLockManager) { + this.fileLockManager = fileLockManager; + } +} diff --git a/packages/php-wasm/universal/src/lib/os-user-space.ts b/packages/php-wasm/universal/src/lib/os-user-space.ts new file mode 100644 index 0000000000..0bedaef592 --- /dev/null +++ b/packages/php-wasm/universal/src/lib/os-user-space.ts @@ -0,0 +1,865 @@ +// TODO: Move file manager into kernel space. +// TODO: Move FileLockManager into php-wasm/universal to resolve this +import type { + RequestedRangeLock, + WholeFileLock, + WholeFileLockOp, +} from './file-lock-manager'; +import type { Emscripten } from './emscripten-types'; +import type { OSKernelSpace } from './os-kernel-space'; + +type FSNode = Emscripten.FS.FSNode; + +type NonZeroNumber = Exclude; +type ResultTuple = + | [value: T, errorCode: 0] + | [value: never, errorCode: NonZeroNumber]; + +// TODO: Consider better name than OSUserSpace. Maybe WasmUserSpace, SystemUserSpace, etc? +export type OSUserSpaceContext = { + pid: number; + // TODO: When receiving this context, validate that all these fields exist. + constants: { + F_RDLCK: number; + F_WRLCK: number; + F_UNLCK: number; + F_GETFL: number; + O_ACCMODE: number; + O_RDONLY: number; + O_WRONLY: number; + O_APPEND: number; + O_NONBLOCK: number; + F_SETFL: number; + F_GETLK: number; + F_SETLK: number; + F_SETLKW: number; + SEEK_SET: number; + SEEK_CUR: number; + SEEK_END: number; + // TODO: Move these values to ES prefix or someplace like that. + // Emscripten does not expose these constants to JS, so we hardcode them here. + // Based on + // https://github.com/emscripten-core/emscripten/blob/76860cc47cef67f5712a7a03a247bc1baabf7ba4/system/lib/libc/musl/include/sys/file.h#L7-L10 + LOCK_SH: 1; + LOCK_EX: 2; + LOCK_NB: 4; + LOCK_UN: 8; + }; + errnoCodes: { + EBADF: NonZeroNumber; + EINVAL: NonZeroNumber; + EAGAIN: NonZeroNumber; + EDEADLK: NonZeroNumber; + EWOULDBLOCK: NonZeroNumber; + }; + memory: { + HEAP8: Int8Array; + HEAPU8: Uint8Array; + HEAP16: Int16Array; + HEAPU16: Uint16Array; + HEAP32: Int32Array; + HEAPU32: Uint32Array; + HEAPF32: Float32Array; + HEAP64: BigInt64Array; + HEAPU64: BigUint64Array; + HEAPF64: Float64Array; + }; + // This is a collection of functions present in built php-wasm JS. + // By receiving the entire collection here, we can avoid recompiling + // php-wasm JS whenever we add a new dependency from this collection. + wasmImports: { + builtin_fcntl64: (fd: number, cmd: number, varargs?: any) => number; + builtin_fd_close: (fd: number) => number; + js_wasm_trace: (...args: any[]) => void; + }; + // This is a collection of functions present in built php-wasm JS. + // By receiving the entire collection here, we can avoid recompiling + // php-wasm JS whenever we add a new dependency from this collection. + wasmExports: { + wasm_get_end_offset: (fd: number) => bigint; + }; + // This is a collection of functions present in built php-wasm JS. + // By receiving the entire collection here, we can avoid recompiling + // php-wasm JS whenever we add a new dependency from this collection. + syscalls: { + getStreamFromFD: (fd: number) => Emscripten.FS.FSStream; + }; + FS: typeof Emscripten.FS; + PROXYFS: typeof Emscripten.PROXYFS & { + // TODO: Add this method to our main Emscripten FS types + realPath(node: FSNode): string; + }; + NODEFS: typeof Emscripten.NODEFS & { + // TODO: Add this method to our main Emscripten FS types + realPath(node: FSNode): string; + }; +}; + +export function bindUserSpace( + { fileLockManager }: OSKernelSpace, + { + pid, + memory: { HEAP16, HEAP64, HEAP32 }, + constants: { + F_RDLCK, + F_WRLCK, + F_UNLCK, + F_GETFL, + O_ACCMODE, + O_RDONLY, + O_WRONLY, + O_APPEND, + O_NONBLOCK, + F_SETFL, + F_GETLK, + F_SETLK, + F_SETLKW, + SEEK_SET, + SEEK_CUR, + SEEK_END, + LOCK_SH, + LOCK_EX, + LOCK_NB, + LOCK_UN, + }, + errnoCodes: { EBADF, EINVAL, EAGAIN, EDEADLK, EWOULDBLOCK }, + wasmImports: { builtin_fcntl64, builtin_fd_close, js_wasm_trace }, + wasmExports: { wasm_get_end_offset }, + syscalls: { getStreamFromFD }, + FS, + PROXYFS, + NODEFS, + }: OSUserSpaceContext +) { + class VarArgsAccessor { + argsAddr: number; + + constructor(argsAddr: number) { + this.argsAddr = argsAddr; + } + + getNextAsPointer(): number { + return this.getNextAsInt(); + } + + getNextAsInt(): number { + const value = HEAP32[this.argsAddr]; + this.argsAddr += 4; + return value; + } + } + + type FcntlLockState = typeof F_RDLCK | typeof F_WRLCK | typeof F_UNLCK; + const locking = { + /* + * This is a set of possibly locked file descriptors. + * + * When a file descriptor is closed, we need to release any associated held by this process. + * Instead of trying remember and forget file descriptors as they are locked and unlocked, + * we just track file descriptors we have locked before and try an unlock when they are closed. + */ + maybeLockedFds: new Set(), + + lockStateToFcntl: { + shared: F_RDLCK, + exclusive: F_WRLCK, + unlocked: F_UNLCK, + } as const satisfies Record, + fcntlToLockState: { + [F_RDLCK as FcntlLockState]: 'shared', + [F_WRLCK as FcntlLockState]: 'exclusive', + [F_UNLCK as FcntlLockState]: 'unlocked', + } as const satisfies Record, + is_path_to_shared_fs(path: string) { + js_wasm_trace('is_path_to_shared_fs(%s)', path); + const { node } = FS.lookupPath(path, { noent_okay: true }); + if (node.mount.type !== PROXYFS) { + return !!node.isSharedFS; + } + + // TODO: Do we still need to support PROXYFS now that Playground CLI uses NODEFS everywhere? + // This looks like a PROXYFS node. Let's try a lookup. + const nodePath = PROXYFS.realPath(node); + const backingFs = node?.mount?.opts?.fs; + if (backingFs) { + // Tolerate ENOENT because looking up a MEMFS node by path always fails. + const { node: backingNode } = backingFs.lookupPath(nodePath, { + noent_okay: true, + }); + return !!backingNode?.isSharedFS; + } + + return false; + }, + get_fd_access_mode(fd: number) { + return builtin_fcntl64(fd, F_GETFL) & O_ACCMODE; + }, + get_vfs_path_from_fd(fd: number): ResultTuple { + try { + return [FS.readlink(`/proc/self/fd/${fd}`), 0]; + } catch { + return [null as never, EBADF] as const; + } + }, + + get_native_path_from_vfs_path(vfsPath: string) { + const { node } = FS.lookupPath(vfsPath, { + noent_okay: true, + }); + if (!node) { + throw new Error(`No node found for VFS path ${vfsPath}`); + } + if (node.mount.type === NODEFS) { + return NODEFS.realPath(node); + } else if (node.mount.type === PROXYFS) { + // TODO: Tolerate ENOENT here? + const { node: backingNode, path: backingPath } = + node.mount.opts.fs.lookupPath(vfsPath); + js_wasm_trace( + 'backingNode for %s: %s', + vfsPath, + backingPath, + backingNode + ); + return backingNode.mount.type.realPath(backingNode); + } else { + throw new Error( + `Unsupported filesystem type for path ${vfsPath}` + ); + } + }, + + check_lock_params(fd: number, l_type: number) { + const accessMode = locking.get_fd_access_mode(fd); + if ( + (l_type === F_WRLCK && accessMode === O_RDONLY) || + (l_type === F_RDLCK && accessMode === O_WRONLY) + ) { + return EBADF; + } + + return 0; + }, + }; + + type FlockStruct = { + l_type: number; + l_whence: number; + l_start: bigint; + l_len: bigint; + l_pid: number; + }; + + // NOTE: With the exception of l_type, these offsets are not exposed to + // JS by Emscripten, so we hardcode them here. + const emscripten_flock_l_type_offset = 0; + const emscripten_flock_l_whence_offset = 2; + const emscripten_flock_l_start_offset = 8; + const emscripten_flock_l_len_offset = 16; + const emscripten_flock_l_pid_offset = 24; + + /** + * Read the flock struct at the given address. + * + * @param {bigint} flockStructAddress - the address of the flock struct + * @returns the flock struct + */ + // TODO: Does this arg type need to be a bigint? + function read_flock_struct(flockStructAddress: number) { + /* + * NOTE: Since we are using HEAP vars like HEAP16 and HEAP64, + * we need to adjust offsets to address the word size of each HEAP. + * + * For example, an offset of 64 bytes is the following for each HEAP: + * - HEAP8: 64 (the 64th byte) + * - HEAP16: 32 (the 32nd 16-bit word) + * - HEAP32: 16 (the 16th 32-bit word) + * - HEAP64: 8 (the 8th 64-bit word) + * + * We get a word offset by dividing the byte offset by the word size. + */ + return { + l_type: HEAP16[ + // Shift right by 1 to divide by 2^1. + (flockStructAddress + emscripten_flock_l_type_offset) >> 1 + ], + l_whence: + HEAP16[ + // Shift right by 1 to divide by 2^1. + (flockStructAddress + emscripten_flock_l_whence_offset) >> 1 + ], + l_start: + HEAP64[ + // Shift right by 3 to divide by 2^3. + (flockStructAddress + emscripten_flock_l_start_offset) >> 3 + ], + l_len: HEAP64[ + // Shift right by 3 to divide by 2^3. + (flockStructAddress + emscripten_flock_l_len_offset) >> 3 + ], + l_pid: HEAP32[ + // Shift right by 2 to divide by 2^2. + (flockStructAddress + emscripten_flock_l_pid_offset) >> 2 + ], + }; + } + + /** + * Update the flock struct at the given address with the given fields. + * + * @param {bigint} flockStructAddress - the address of the flock struct + * @param {object} fields - the fields to update + */ + function update_flock_struct( + flockStructAddress: number, + fields: Partial + ) { + /* + * NOTE: Since we are using HEAP vars like HEAP16 and HEAP64, + * we need to adjust offsets to address the word size of each HEAP. + * + * For example, an offset of 64 bytes is the following for each HEAP: + * - HEAP8: 64 (the 64th byte) + * - HEAP16: 32 (the 32nd 16-bit word) + * - HEAP32: 16 (the 16th 32-bit word) + * - HEAP64: 8 (the 8th 64-bit word) + * + * We get a word offset by dividing the byte offset by the word size. + */ + if (fields.l_type !== undefined) { + HEAP16[ + // Shift right by 1 to divide by 2^1. + (flockStructAddress + emscripten_flock_l_type_offset) >> 1 + ] = fields.l_type; + } + if (fields.l_whence !== undefined) { + HEAP16[ + // Shift right by 1 to divide by 2^1. + (flockStructAddress + emscripten_flock_l_whence_offset) >> 1 + ] = fields.l_whence; + } + if (fields.l_start !== undefined) { + HEAP64[ + // Shift right by 3 to divide by 2^3. + (flockStructAddress + emscripten_flock_l_start_offset) >> 3 + ] = fields.l_start; + } + if (fields.l_len !== undefined) { + HEAP64[ + // Shift right by 3 to divide by 2^3. + (flockStructAddress + emscripten_flock_l_len_offset) >> 3 + ] = fields.l_len; + } + if (fields.l_pid !== undefined) { + HEAP32[ + // Shift right by 2 to divide by 2^2. + (flockStructAddress + emscripten_flock_l_pid_offset) >> 2 + ] = fields.l_pid; + } + } + + /** + * Resolve the base address of the range depending on the whence and start offset. + * + * @param {number} fd - the file descriptor + * @param {number} whence - what the start offset is relative to + * @param {bigint} startOffset - the offset from the whence + * @returns The resolved offset and the errno. If there is an error, + * the resolved offset is null, and the errno is non-zero. + */ + function get_base_address(fd: number, whence: number, startOffset: bigint) { + let baseAddress; + switch (whence) { + case SEEK_SET: + baseAddress = 0n; + break; + case SEEK_CUR: + try { + const stream = getStreamFromFD(fd); + baseAddress = FS.llseek(stream, 0, whence); + } catch (e) { + js_wasm_trace( + 'get_base_address(%d, %d, %d) getStreamFromFD error %s', + fd, + whence, + startOffset, + e + ); + return [null, EINVAL]; + } + break; + case SEEK_END: + baseAddress = wasm_get_end_offset(fd); + break; + default: + return [null, EINVAL]; + } + + if (baseAddress == -1) { + // We cannot resolve the offset within the file. + // Let's treat this as a problem with the file descriptor. + return [null, EBADF]; + } + + const resolvedOffset = baseAddress + startOffset; + if (resolvedOffset < 0) { + // This is not a valid offset. Report args as invalid. + return [null, EINVAL]; + } + + return [resolvedOffset, 0]; + } + + // TODO: Should command just be a string representation of const name? + async function fcntl64(fd: number, cmd: number, varargs?: number) { + switch (cmd) { + case F_GETLK: { + js_wasm_trace('fcntl(%d, F_GETLK)', fd); + const [vfsPath, vfsPathErrno] = + locking.get_vfs_path_from_fd(fd); + if (vfsPathErrno !== 0) { + js_wasm_trace( + 'fcntl(%d, F_GETLK) %s get_vfs_path_from_fd errno %d', + fd, + vfsPath, + vfsPathErrno + ); + return -EBADF; + } + + const varArgsAccessor = new VarArgsAccessor(varargs!); + const flockStructAddr = varArgsAccessor.getNextAsPointer(); + + if (!locking.is_path_to_shared_fs(vfsPath)) { + js_wasm_trace( + "fcntl(%d, F_GETLK) locking is not implemented for non-NodeFS path '%s'", + fd, + vfsPath + ); + + // If not a NodeFS path, we can't lock it. + // Default to succeeding as Emscripten does. + update_flock_struct(flockStructAddr, { + l_type: F_UNLCK, + }); + return 0; + } + + const flockStruct = read_flock_struct(flockStructAddr); + + if (!(flockStruct.l_type in locking.fcntlToLockState)) { + return -EINVAL; + } + + const paramsCheckErrno = locking.check_lock_params( + fd, + flockStruct.l_type + ); + if (paramsCheckErrno !== 0) { + js_wasm_trace( + 'fcntl(%d, F_GETLK) %s check_lock_params errno %d', + fd, + vfsPath, + paramsCheckErrno + ); + return -EINVAL; + } + + const requestedLockType = + locking.fcntlToLockState[flockStruct.l_type]; + const [absoluteStartOffset, baseAddressErrno] = + get_base_address( + fd, + flockStruct.l_whence, + flockStruct.l_start + ); + if (baseAddressErrno !== 0) { + js_wasm_trace( + 'fcntl(%d, F_GETLK) %s get_base_address errno %d', + fd, + vfsPath, + baseAddressErrno + ); + return -EINVAL; + } + + try { + const nativeFilePath = + locking.get_native_path_from_vfs_path(vfsPath); + const conflictingLock = + fileLockManager.findFirstConflictingByteRangeLock( + nativeFilePath, + { + type: requestedLockType, + start: absoluteStartOffset, + end: absoluteStartOffset + flockStruct.l_len, + pid, + } + ); + if (conflictingLock === undefined) { + js_wasm_trace( + 'fcntl(%d, F_GETLK) %s findFirstConflictingByteRangeLock type=unlocked start=0x%x end=0x%x', + fd, + vfsPath, + absoluteStartOffset, + absoluteStartOffset + flockStruct.l_len + ); + + update_flock_struct(flockStructAddr, { + l_type: F_UNLCK, + }); + return 0; + } + + js_wasm_trace( + 'fcntl(%d, F_GETLK) %s findFirstConflictingByteRangeLock type=%s start=0x%x end=0x%x conflictingLock %d', + fd, + vfsPath, + conflictingLock.type, + conflictingLock.start, + conflictingLock.end, + conflictingLock.pid + ); + + const fcntlLockState = + locking.lockStateToFcntl[conflictingLock.type]; + update_flock_struct(flockStructAddr, { + l_type: fcntlLockState, + l_whence: SEEK_SET, + l_start: conflictingLock.start, + l_len: BigInt( + conflictingLock.end - conflictingLock.start + ), + l_pid: conflictingLock.pid, + }); + return 0; + } catch (e) { + js_wasm_trace( + 'fcntl(%d, F_GETLK) %s findFirstConflictingByteRangeLock error %s', + fd, + vfsPath, + e + ); + return -EINVAL; + } + } + case F_SETLK: { + js_wasm_trace('fcntl(%d, F_SETLK)', fd); + const [vfsPath, vfsPathErrno] = + locking.get_vfs_path_from_fd(fd); + if (vfsPathErrno !== 0) { + js_wasm_trace( + 'fcntl(%d, F_SETLK) %s get_vfs_path_from_fd errno %d', + fd, + vfsPath, + vfsPathErrno + ); + return -vfsPathErrno; + } + + if (!locking.is_path_to_shared_fs(vfsPath)) { + js_wasm_trace( + 'fcntl(%d, F_SETLK) locking is not implemented for non-NodeFS path %s', + fd, + vfsPath + ); + + // If not a NodeFS path, we can't lock it. + // Default to succeeding as Emscripten does. + return 0; + } + + const varArgsAccessor = new VarArgsAccessor(varargs!); + const flockStructAddr = varArgsAccessor.getNextAsPointer(); + const flockStruct = read_flock_struct(flockStructAddr); + + const [absoluteStartOffset, baseAddressErrno] = + get_base_address( + fd, + flockStruct.l_whence, + flockStruct.l_start + ); + if (baseAddressErrno !== 0) { + js_wasm_trace( + 'fcntl(%d, F_SETLK) %s get_base_address errno %d', + fd, + vfsPath, + baseAddressErrno + ); + return -EINVAL; + } + + if (!(flockStruct.l_type in locking.fcntlToLockState)) { + js_wasm_trace( + 'fcntl(%d, F_SETLK) %s invalid lock type %d', + fd, + vfsPath, + flockStruct.l_type + ); + return -EINVAL; + } + + const paramsCheckErrno = locking.check_lock_params( + fd, + flockStruct.l_type + ); + if (paramsCheckErrno !== 0) { + js_wasm_trace( + 'fcntl(%d, F_SETLK) %s check_lock_params errno %d', + fd, + vfsPath, + paramsCheckErrno + ); + return -paramsCheckErrno; + } + + locking.maybeLockedFds.add(fd); + + const requestedLockType = + locking.fcntlToLockState[flockStruct.l_type]; + const rangeLock: RequestedRangeLock = { + type: requestedLockType, + start: absoluteStartOffset, + end: absoluteStartOffset + flockStruct.l_len, + pid, + }; + + try { + const nativeFilePath = + locking.get_native_path_from_vfs_path(vfsPath); + js_wasm_trace( + 'fcntl(%d, F_SETLK) %s calling lockFileByteRange for range lock %s', + fd, + vfsPath, + rangeLock + ); + + const succeeded = fileLockManager.lockFileByteRange( + nativeFilePath, + rangeLock + ); + + js_wasm_trace( + 'fcntl(%d, F_SETLK) %s lockFileByteRange returned %d for range lock %s', + fd, + vfsPath, + succeeded, + rangeLock + ); + return succeeded ? 0 : -EAGAIN; + } catch (e) { + js_wasm_trace( + 'fcntl(%d, F_SETLK) %s lockFileByteRange error %s for range lock %s', + fd, + vfsPath, + e, + rangeLock + ); + return -EINVAL; + } + } + // @TODO: Implement waiting for lock + case F_SETLKW: { + // We do not yet support the blocking form of flock(). + // We respond with EDEADLK to indicate failure + // because it is a known errno for a failed F_SETLKW command. + return -EDEADLK; + } + case F_SETFL: { + /** + * Overrides the core Emscripten implementation to reflect what + * fcntl does in linux kernel. This implementation is still missing + * a bunch of nuance, but, unlike the core Emscripten implementation, + * it overrides the stream flags while preserving non-stream flags. + * + * @see fcntl.c: + * https://github.com/torvalds/linux/blob/a79a588fc1761dc12a3064fc2f648ae66cea3c5a/fs/fcntl.c#L39 + */ + let arg = 0; + if (varargs !== undefined) { + const varArgsAccessor = new VarArgsAccessor(varargs); + arg = varArgsAccessor.getNextAsInt(); + } + + const stream = getStreamFromFD(fd); + + // Update the stream flags + const SETFL_MASK = O_APPEND | O_NONBLOCK; + stream.flags = + (arg & SETFL_MASK) | (stream.flags & ~SETFL_MASK); + + return 0; + } + default: + return builtin_fcntl64(fd, cmd, varargs); + } + } + + async function flock(fd: number, op: number) { + js_wasm_trace('js_flock(%d, %d)', fd, op); + + type FlockOp = typeof LOCK_SH | typeof LOCK_EX | typeof LOCK_UN; + const flockToLockOpType = { + [LOCK_SH]: 'shared', + [LOCK_EX]: 'exclusive', + [LOCK_UN]: 'unlock', + } as const satisfies Record; + + const [vfsPath, vfsPathErrno] = locking.get_vfs_path_from_fd(fd); + if (vfsPathErrno !== 0) { + js_wasm_trace( + 'js_flock(%d, %d) get_vfs_path_from_fd errno %d', + fd, + op, + vfsPath, + vfsPathErrno + ); + return -vfsPathErrno; + } + + if (!locking.is_path_to_shared_fs(vfsPath)) { + js_wasm_trace( + 'flock(%d, %d) locking is not implemented for non-NodeFS path %s', + fd, + op, + vfsPath + ); + // If not a NodeFS path, we can't lock it. + // Default to succeeding as Emscripten does. + return 0; + } + + const paramsCheckErrno = locking.check_lock_params(fd, op); + if (paramsCheckErrno !== 0) { + js_wasm_trace( + 'js_flock(%d, %d) check_lock_params errno %d', + fd, + op, + paramsCheckErrno + ); + return -paramsCheckErrno; + } + + // @TODO: Consider supporting blocking mode of flock() + if ((op & LOCK_NB) === 0) { + js_wasm_trace( + 'js_flock(%d, %d) blocking mode of flock() is not implemented', + fd, + op + ); + // We do not yet support the blocking form of flock(). + // We respond with EINVAL to indicate failure + // because it is a known errno for a failed blocking flock(). + return -EINVAL; + } + + const maskedOp = op & ((LOCK_SH | LOCK_EX | LOCK_UN) as FlockOp | 0); + + if (maskedOp === 0) { + js_wasm_trace('js_flock(%d, %d) invalid flock() operation', fd, op); + return -EINVAL; + } + + const lockOpType = flockToLockOpType[maskedOp as FlockOp]; + if (lockOpType === undefined) { + js_wasm_trace('js_flock(%d, %d) invalid flock() operation', fd, op); + return -EINVAL; + } + + try { + const nativeFilePath = + locking.get_native_path_from_vfs_path(vfsPath); + const obtainedLock = await fileLockManager.lockWholeFile( + nativeFilePath, + { + type: lockOpType, + pid: pid, + fd, + } + ); + js_wasm_trace( + 'js_flock(%d, %d) lockWholeFile %s returned %d', + fd, + op, + vfsPath, + obtainedLock + ); + return obtainedLock ? 0 : -EWOULDBLOCK; + } catch (e) { + js_wasm_trace('js_flock(%d, %d) lockWholeFile error %s', fd, op, e); + return -EINVAL; + } + } + + async function fd_close(fd: number) { + // We have to get the VFS path from the file descriptor + // before closing it. + const [vfsPath, vfsPathResolutionErrno] = + locking.get_vfs_path_from_fd(fd); + + const fdCloseResult = builtin_fd_close(fd); + if (fdCloseResult !== 0 || !locking.maybeLockedFds.has(fd)) { + js_wasm_trace('fd_close(%d) result %d', fd, fdCloseResult); + return fdCloseResult; + } + + if (vfsPathResolutionErrno !== 0) { + js_wasm_trace( + 'fd_close(%d) get_vfs_path_from_fd error %d', + fd, + vfsPathResolutionErrno + ); + /* + * It looks like the file may have had an associated lock, + * but since we cannot look up the path, + * there is nothing more for us to do. + * + * NOTE: This seems possible for files that are locked and + * then unlinked before close. It is an opportunity for a + * lock to be orphaned in the lock manager. + * @TODO: Explore how to ensure cleanup in this case. + */ + return fdCloseResult; + } + + try { + const nativeFilePath = + locking.get_native_path_from_vfs_path(vfsPath); + await fileLockManager.releaseLocksForProcessFd( + pid, + fd, + nativeFilePath + ); + js_wasm_trace('fd_close(%d) release locks success', fd); + } catch (e) { + js_wasm_trace("fd_close(%d) error '%s'", fd, e); + } finally { + locking.maybeLockedFds.delete(fd); + } + return fdCloseResult; + } + + // TODO: Implement based on current process + // TODO: Replace with process exit handler + async function js_release_file_locks() { + js_wasm_trace('js_release_file_locks()'); + if (!pid || !fileLockManager) { + js_wasm_trace('js_release_file_locks no pid or file lock manager'); + return; + } + + try { + await fileLockManager.releaseLocksForProcess(pid); + js_wasm_trace('js_release_file_locks succeeded'); + } catch (e) { + js_wasm_trace('js_release_file_locks error %s', e); + } + } + + return { + fcntl64, + flock, + fd_close, + js_release_file_locks, + }; +} diff --git a/packages/php-wasm/universal/tsconfig.json b/packages/php-wasm/universal/tsconfig.json index fe794238d4..18d7a4b681 100644 --- a/packages/php-wasm/universal/tsconfig.json +++ b/packages/php-wasm/universal/tsconfig.json @@ -4,7 +4,9 @@ "forceConsistentCasingInFileNames": true, "strict": true, "noImplicitOverride": true, - "noPropertyAccessFromIndexSignature": true, + // "noPropertyAccessFromIndexSignature": true, + // TODO: Why do we want to avoid property access from index signature? + "noPropertyAccessFromIndexSignature": false, "noImplicitReturns": true, "noFallthroughCasesInSwitch": true, "types": ["vitest", "vite/client"]