@php-wasm/node 3.0.54 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.js CHANGED
@@ -390,9 +390,1156 @@ async function withNetworking(phpModuleArgs = {}) {
390
390
  }
391
391
 
392
392
  // packages/php-wasm/node/src/lib/load-runtime.ts
393
- import { loadPHPRuntime, FSHelpers as FSHelpers5 } from "@php-wasm/universal";
393
+ import {
394
+ loadPHPRuntime,
395
+ FSHelpers as FSHelpers5,
396
+ FileLockManagerComposite
397
+ } from "@php-wasm/universal";
398
+
399
+ // packages/php-wasm/node/src/lib/wasm-user-space.ts
400
+ import { lookup as lookup2 } from "dns/promises";
401
+ function bindUserSpace({ fileLockManager }, {
402
+ pid,
403
+ memory: { HEAP16, HEAP64, HEAP32 },
404
+ constants: {
405
+ F_RDLCK,
406
+ F_WRLCK,
407
+ F_UNLCK,
408
+ F_GETFL,
409
+ O_ACCMODE,
410
+ O_RDONLY,
411
+ O_WRONLY,
412
+ O_APPEND,
413
+ O_NONBLOCK,
414
+ F_SETFL,
415
+ F_GETLK,
416
+ F_SETLK,
417
+ F_SETLKW,
418
+ SEEK_SET,
419
+ SEEK_CUR,
420
+ SEEK_END,
421
+ LOCK_SH,
422
+ LOCK_EX,
423
+ LOCK_NB,
424
+ LOCK_UN
425
+ },
426
+ errnoCodes: { EBADF, EINVAL, EAGAIN, EWOULDBLOCK },
427
+ wasmImports: { builtin_fcntl64, builtin_fd_close, js_wasm_trace },
428
+ wasmExports: { wasm_get_end_offset },
429
+ syscalls: { getStreamFromFD },
430
+ FS,
431
+ PROXYFS,
432
+ NODEFS
433
+ }) {
434
+ class VarArgsAccessor {
435
+ constructor(argsAddr) {
436
+ this.argsAddr = argsAddr;
437
+ }
438
+ getNextAsPointer() {
439
+ return this.getNextAsInt();
440
+ }
441
+ getNextAsInt() {
442
+ const fourByteOffset = this.argsAddr >> 2;
443
+ const value = HEAP32[fourByteOffset];
444
+ this.argsAddr += 4;
445
+ return value;
446
+ }
447
+ }
448
+ const locking = {
449
+ /*
450
+ * This is a set of possibly locked file descriptors.
451
+ *
452
+ * When a file descriptor is closed, we need to release any associated held by this process.
453
+ * Instead of trying remember and forget file descriptors as they are locked and unlocked,
454
+ * we just track file descriptors we have locked before and try an unlock when they are closed.
455
+ */
456
+ maybeLockedFds: /* @__PURE__ */ new Set(),
457
+ lockStateToFcntl: {
458
+ shared: F_RDLCK,
459
+ exclusive: F_WRLCK,
460
+ unlocked: F_UNLCK
461
+ },
462
+ fcntlToLockState: {
463
+ [F_RDLCK]: "shared",
464
+ [F_WRLCK]: "exclusive",
465
+ [F_UNLCK]: "unlocked"
466
+ },
467
+ is_path_to_shared_fs(path2) {
468
+ const { node } = FS.lookupPath(path2, { noent_okay: true });
469
+ if (!node) {
470
+ return false;
471
+ }
472
+ if (node.mount.type !== PROXYFS) {
473
+ return !!node.isSharedFS;
474
+ }
475
+ const nodePath = PROXYFS.realPath(node);
476
+ const backingFs = node?.mount?.opts?.["fs"];
477
+ if (backingFs) {
478
+ const { node: backingNode } = backingFs.lookupPath(nodePath, {
479
+ noent_okay: true
480
+ });
481
+ return !!backingNode?.isSharedFS;
482
+ }
483
+ return false;
484
+ },
485
+ get_fd_access_mode(fd) {
486
+ return builtin_fcntl64(fd, F_GETFL) & O_ACCMODE;
487
+ },
488
+ get_vfs_path_from_fd(fd) {
489
+ try {
490
+ return [FS.readlink(`/proc/self/fd/${fd}`), 0];
491
+ } catch {
492
+ return [null, EBADF];
493
+ }
494
+ },
495
+ get_native_path_from_vfs_path(vfsPath) {
496
+ const { node } = FS.lookupPath(vfsPath, {
497
+ noent_okay: true
498
+ });
499
+ if (!node) {
500
+ throw new Error(`No node found for VFS path ${vfsPath}`);
501
+ }
502
+ if (node.mount.type === NODEFS) {
503
+ return NODEFS.realPath(node);
504
+ } else if (node.mount.type === PROXYFS) {
505
+ const { node: backingNode, path: backingPath } = node.mount.opts["fs"].lookupPath(vfsPath);
506
+ js_wasm_trace(
507
+ "backingNode for %s: %s",
508
+ vfsPath,
509
+ backingPath,
510
+ backingNode
511
+ );
512
+ return backingNode.mount.type.realPath(backingNode);
513
+ } else {
514
+ throw new Error(
515
+ `Unsupported filesystem type for path ${vfsPath}`
516
+ );
517
+ }
518
+ },
519
+ get_native_fd_from_emscripten_fd(fd) {
520
+ try {
521
+ const stream = getStreamFromFD(fd);
522
+ if (stream.nfd === void 0) {
523
+ return [null, EBADF];
524
+ }
525
+ return [stream.nfd, 0];
526
+ } catch {
527
+ return [null, EBADF];
528
+ }
529
+ },
530
+ check_lock_params(fd, l_type) {
531
+ const accessMode = locking.get_fd_access_mode(fd);
532
+ if (l_type === F_WRLCK && accessMode === O_RDONLY || l_type === F_RDLCK && accessMode === O_WRONLY) {
533
+ js_wasm_trace(
534
+ "check_lock_params(%d, %d, %d) EBADF",
535
+ fd,
536
+ l_type,
537
+ accessMode
538
+ );
539
+ return EBADF;
540
+ }
541
+ return 0;
542
+ }
543
+ };
544
+ const emscripten_flock_l_type_offset = 0;
545
+ const emscripten_flock_l_whence_offset = 2;
546
+ const emscripten_flock_l_start_offset = 8;
547
+ const emscripten_flock_l_len_offset = 16;
548
+ const emscripten_flock_l_pid_offset = 24;
549
+ function readFlockStruct(flockStructAddress) {
550
+ return {
551
+ l_type: HEAP16[
552
+ // Shift right by 1 to divide by 2^1.
553
+ flockStructAddress + emscripten_flock_l_type_offset >> 1
554
+ ],
555
+ l_whence: HEAP16[
556
+ // Shift right by 1 to divide by 2^1.
557
+ flockStructAddress + emscripten_flock_l_whence_offset >> 1
558
+ ],
559
+ l_start: HEAP64[
560
+ // Shift right by 3 to divide by 2^3.
561
+ flockStructAddress + emscripten_flock_l_start_offset >> 3
562
+ ],
563
+ l_len: HEAP64[
564
+ // Shift right by 3 to divide by 2^3.
565
+ flockStructAddress + emscripten_flock_l_len_offset >> 3
566
+ ],
567
+ l_pid: HEAP32[
568
+ // Shift right by 2 to divide by 2^2.
569
+ flockStructAddress + emscripten_flock_l_pid_offset >> 2
570
+ ]
571
+ };
572
+ }
573
+ function updateFlockStruct(flockStructAddress, fields) {
574
+ if (fields.l_type !== void 0) {
575
+ HEAP16[
576
+ // Shift right by 1 to divide by 2^1.
577
+ flockStructAddress + emscripten_flock_l_type_offset >> 1
578
+ ] = fields.l_type;
579
+ }
580
+ if (fields.l_whence !== void 0) {
581
+ HEAP16[
582
+ // Shift right by 1 to divide by 2^1.
583
+ flockStructAddress + emscripten_flock_l_whence_offset >> 1
584
+ ] = fields.l_whence;
585
+ }
586
+ if (fields.l_start !== void 0) {
587
+ HEAP64[
588
+ // Shift right by 3 to divide by 2^3.
589
+ flockStructAddress + emscripten_flock_l_start_offset >> 3
590
+ ] = fields.l_start;
591
+ }
592
+ if (fields.l_len !== void 0) {
593
+ HEAP64[
594
+ // Shift right by 3 to divide by 2^3.
595
+ flockStructAddress + emscripten_flock_l_len_offset >> 3
596
+ ] = fields.l_len;
597
+ }
598
+ if (fields.l_pid !== void 0) {
599
+ HEAP32[
600
+ // Shift right by 2 to divide by 2^2.
601
+ flockStructAddress + emscripten_flock_l_pid_offset >> 2
602
+ ] = fields.l_pid;
603
+ }
604
+ }
605
+ function getBaseAddress(fd, whence, startOffset) {
606
+ let baseAddress;
607
+ switch (whence) {
608
+ case SEEK_SET:
609
+ baseAddress = 0n;
610
+ break;
611
+ case SEEK_CUR:
612
+ try {
613
+ const stream = getStreamFromFD(fd);
614
+ baseAddress = FS.llseek(stream, 0, whence);
615
+ } catch (e) {
616
+ js_wasm_trace(
617
+ "get_base_address(%d, %d, %d) getStreamFromFD error %s",
618
+ fd,
619
+ whence,
620
+ startOffset,
621
+ e
622
+ );
623
+ return [null, EINVAL];
624
+ }
625
+ break;
626
+ case SEEK_END:
627
+ baseAddress = wasm_get_end_offset(fd);
628
+ break;
629
+ default:
630
+ return [null, EINVAL];
631
+ }
632
+ if (baseAddress == -1) {
633
+ return [null, EBADF];
634
+ }
635
+ const resolvedOffset = baseAddress + startOffset;
636
+ if (resolvedOffset < 0) {
637
+ return [null, EINVAL];
638
+ }
639
+ return [resolvedOffset, 0];
640
+ }
641
+ function fcntl64(fd, cmd, varargs) {
642
+ js_wasm_trace("fcntl64(%d, %d)", fd, cmd);
643
+ if (!fileLockManager) {
644
+ js_wasm_trace(
645
+ "fcntl64(%d, %d) file lock manager is not available. delegate to Emscripten builtin fcntl64.",
646
+ fd,
647
+ cmd
648
+ );
649
+ return builtin_fcntl64(fd, cmd, varargs);
650
+ }
651
+ switch (cmd) {
652
+ case F_GETLK: {
653
+ const reportUnlockedFileByDefault = function reportUnlockedFileByDefault2() {
654
+ updateFlockStruct(flockStructAddr, {
655
+ l_type: F_UNLCK
656
+ });
657
+ return 0;
658
+ };
659
+ js_wasm_trace("fcntl(%d, F_GETLK)", fd);
660
+ const [vfsPath, vfsPathErrno] = locking.get_vfs_path_from_fd(fd);
661
+ if (vfsPathErrno !== 0) {
662
+ js_wasm_trace(
663
+ "fcntl(%d, F_GETLK) %s get_vfs_path_from_fd errno %d",
664
+ fd,
665
+ vfsPath,
666
+ vfsPathErrno
667
+ );
668
+ return -EBADF;
669
+ }
670
+ const varArgsAccessor = new VarArgsAccessor(varargs);
671
+ const flockStructAddr = varArgsAccessor.getNextAsPointer();
672
+ if (!locking.is_path_to_shared_fs(vfsPath) || fileLockManager === void 0) {
673
+ js_wasm_trace(
674
+ "fcntl(%d, F_GETLK) locking is not implemented for non-NodeFS path '%s'",
675
+ fd,
676
+ vfsPath
677
+ );
678
+ return reportUnlockedFileByDefault();
679
+ }
680
+ const flockStruct = readFlockStruct(flockStructAddr);
681
+ if (!(flockStruct.l_type in locking.fcntlToLockState)) {
682
+ return -EINVAL;
683
+ }
684
+ const paramsCheckErrno = locking.check_lock_params(
685
+ fd,
686
+ flockStruct.l_type
687
+ );
688
+ if (paramsCheckErrno !== 0) {
689
+ js_wasm_trace(
690
+ "fcntl(%d, F_GETLK) %s check_lock_params errno %d",
691
+ fd,
692
+ vfsPath,
693
+ paramsCheckErrno
694
+ );
695
+ return -EINVAL;
696
+ }
697
+ const requestedLockType = locking.fcntlToLockState[flockStruct.l_type];
698
+ const [absoluteStartOffset, baseAddressErrno] = getBaseAddress(
699
+ fd,
700
+ flockStruct.l_whence,
701
+ flockStruct.l_start
702
+ );
703
+ if (baseAddressErrno !== 0) {
704
+ js_wasm_trace(
705
+ "fcntl(%d, F_GETLK) %s get_base_address errno %d",
706
+ fd,
707
+ vfsPath,
708
+ baseAddressErrno
709
+ );
710
+ return -EINVAL;
711
+ }
712
+ const [nativeFd, nativeFdErrno] = locking.get_native_fd_from_emscripten_fd(fd);
713
+ if (nativeFdErrno !== 0) {
714
+ js_wasm_trace(
715
+ "fcntl(%d, F_GETLK) get_native_fd_from_emscripten_fd errno %d",
716
+ fd,
717
+ nativeFdErrno
718
+ );
719
+ return -nativeFdErrno;
720
+ }
721
+ try {
722
+ const nativeFilePath = locking.get_native_path_from_vfs_path(vfsPath);
723
+ const conflictingLock = fileLockManager.findFirstConflictingByteRangeLock(
724
+ nativeFilePath,
725
+ {
726
+ type: requestedLockType,
727
+ start: absoluteStartOffset,
728
+ end: absoluteStartOffset + flockStruct.l_len,
729
+ pid,
730
+ fd: nativeFd
731
+ }
732
+ );
733
+ if (conflictingLock === void 0) {
734
+ js_wasm_trace(
735
+ "fcntl(%d, F_GETLK) %s findFirstConflictingByteRangeLock type=unlocked start=0x%x end=0x%x",
736
+ fd,
737
+ vfsPath,
738
+ absoluteStartOffset,
739
+ absoluteStartOffset + flockStruct.l_len
740
+ );
741
+ updateFlockStruct(flockStructAddr, {
742
+ l_type: F_UNLCK
743
+ });
744
+ return 0;
745
+ }
746
+ js_wasm_trace(
747
+ "fcntl(%d, F_GETLK) %s findFirstConflictingByteRangeLock type=%s start=0x%x end=0x%x conflictingLock %d",
748
+ fd,
749
+ vfsPath,
750
+ conflictingLock.type,
751
+ conflictingLock.start,
752
+ conflictingLock.end,
753
+ conflictingLock.pid
754
+ );
755
+ const fcntlLockState = locking.lockStateToFcntl[conflictingLock.type];
756
+ updateFlockStruct(flockStructAddr, {
757
+ l_type: fcntlLockState,
758
+ l_whence: SEEK_SET,
759
+ l_start: conflictingLock.start,
760
+ l_len: BigInt(
761
+ conflictingLock.end - conflictingLock.start
762
+ ),
763
+ l_pid: conflictingLock.pid
764
+ });
765
+ return 0;
766
+ } catch (e) {
767
+ js_wasm_trace(
768
+ "fcntl(%d, F_GETLK) %s findFirstConflictingByteRangeLock error %s",
769
+ fd,
770
+ vfsPath,
771
+ e
772
+ );
773
+ return -EINVAL;
774
+ }
775
+ }
776
+ case F_SETLKW:
777
+ case F_SETLK: {
778
+ js_wasm_trace("fcntl(%d, F_SETLK)", fd);
779
+ const [vfsPath, vfsPathErrno] = locking.get_vfs_path_from_fd(fd);
780
+ if (vfsPathErrno !== 0) {
781
+ js_wasm_trace(
782
+ "fcntl(%d, F_SETLK) %s get_vfs_path_from_fd errno %d",
783
+ fd,
784
+ vfsPath,
785
+ vfsPathErrno
786
+ );
787
+ return -vfsPathErrno;
788
+ }
789
+ if (!locking.is_path_to_shared_fs(vfsPath)) {
790
+ js_wasm_trace(
791
+ "fcntl(%d, F_SETLK) locking is not implemented for non-NodeFS path %s",
792
+ fd,
793
+ vfsPath
794
+ );
795
+ return 0;
796
+ }
797
+ const varArgsAccessor = new VarArgsAccessor(varargs);
798
+ const flockStructAddr = varArgsAccessor.getNextAsPointer();
799
+ const flockStruct = readFlockStruct(flockStructAddr);
800
+ const [absoluteStartOffset, baseAddressErrno] = getBaseAddress(
801
+ fd,
802
+ flockStruct.l_whence,
803
+ flockStruct.l_start
804
+ );
805
+ if (baseAddressErrno !== 0) {
806
+ js_wasm_trace(
807
+ "fcntl(%d, F_SETLK) %s get_base_address errno %d",
808
+ fd,
809
+ vfsPath,
810
+ baseAddressErrno
811
+ );
812
+ return -EINVAL;
813
+ }
814
+ if (!(flockStruct.l_type in locking.fcntlToLockState)) {
815
+ js_wasm_trace(
816
+ "fcntl(%d, F_SETLK) %s invalid lock type %d",
817
+ fd,
818
+ vfsPath,
819
+ flockStruct.l_type
820
+ );
821
+ return -EINVAL;
822
+ }
823
+ const paramsCheckErrno = locking.check_lock_params(
824
+ fd,
825
+ flockStruct.l_type
826
+ );
827
+ if (paramsCheckErrno !== 0) {
828
+ js_wasm_trace(
829
+ "fcntl(%d, F_SETLK) %s check_lock_params errno %d",
830
+ fd,
831
+ vfsPath,
832
+ paramsCheckErrno
833
+ );
834
+ return -paramsCheckErrno;
835
+ }
836
+ const [nativeFd, nativeFdErrno] = locking.get_native_fd_from_emscripten_fd(fd);
837
+ if (nativeFdErrno !== 0) {
838
+ js_wasm_trace(
839
+ "fcntl(%d, F_SETLK) get_native_fd_from_emscripten_fd errno %d",
840
+ fd,
841
+ nativeFdErrno
842
+ );
843
+ return -nativeFdErrno;
844
+ }
845
+ const requestedLockType = locking.fcntlToLockState[flockStruct.l_type];
846
+ const rangeLock = {
847
+ type: requestedLockType,
848
+ start: absoluteStartOffset,
849
+ end: absoluteStartOffset + flockStruct.l_len,
850
+ pid,
851
+ fd: nativeFd
852
+ };
853
+ try {
854
+ const nativeFilePath = locking.get_native_path_from_vfs_path(vfsPath);
855
+ js_wasm_trace(
856
+ "fcntl(%d, F_SETLK) %s calling lockFileByteRange for range lock %s",
857
+ fd,
858
+ vfsPath,
859
+ rangeLock
860
+ );
861
+ const waitForLock = cmd === F_SETLKW;
862
+ const succeeded = fileLockManager.lockFileByteRange(
863
+ nativeFilePath,
864
+ rangeLock,
865
+ waitForLock
866
+ );
867
+ if (succeeded) {
868
+ locking.maybeLockedFds.add(nativeFd);
869
+ }
870
+ js_wasm_trace(
871
+ "fcntl(%d, F_SETLK) %s lockFileByteRange returned %d for range lock %s",
872
+ fd,
873
+ vfsPath,
874
+ succeeded,
875
+ rangeLock
876
+ );
877
+ return succeeded ? 0 : -EAGAIN;
878
+ } catch (e) {
879
+ js_wasm_trace(
880
+ "fcntl(%d, F_SETLK) %s lockFileByteRange error %s for range lock %s",
881
+ fd,
882
+ vfsPath,
883
+ e,
884
+ rangeLock
885
+ );
886
+ return -EINVAL;
887
+ }
888
+ }
889
+ case F_SETFL: {
890
+ let arg = 0;
891
+ if (varargs !== void 0) {
892
+ const varArgsAccessor = new VarArgsAccessor(varargs);
893
+ arg = varArgsAccessor.getNextAsInt();
894
+ }
895
+ const stream = getStreamFromFD(fd);
896
+ const SETFL_MASK = O_APPEND | O_NONBLOCK;
897
+ stream.flags = arg & SETFL_MASK | stream.flags & ~SETFL_MASK;
898
+ return 0;
899
+ }
900
+ default:
901
+ return builtin_fcntl64(fd, cmd, varargs);
902
+ }
903
+ }
904
+ function flock(fd, op) {
905
+ js_wasm_trace("flock(%d, %d)", fd, op);
906
+ if (!fileLockManager) {
907
+ js_wasm_trace(
908
+ "flock(%d, %d) file lock manager is not available. succeed by default as Emscripten does.",
909
+ fd,
910
+ op
911
+ );
912
+ return 0;
913
+ }
914
+ const flockToLockOpType = {
915
+ [LOCK_SH]: "shared",
916
+ [LOCK_EX]: "exclusive",
917
+ [LOCK_UN]: "unlock"
918
+ };
919
+ const [vfsPath, vfsPathErrno] = locking.get_vfs_path_from_fd(fd);
920
+ if (vfsPathErrno !== 0) {
921
+ js_wasm_trace(
922
+ "flock(%d, %d) get_vfs_path_from_fd errno %d",
923
+ fd,
924
+ op,
925
+ vfsPath,
926
+ vfsPathErrno
927
+ );
928
+ return -vfsPathErrno;
929
+ }
930
+ if (!locking.is_path_to_shared_fs(vfsPath)) {
931
+ js_wasm_trace(
932
+ "flock(%d, %d) locking is not implemented for non-NodeFS path %s",
933
+ fd,
934
+ op,
935
+ vfsPath
936
+ );
937
+ return 0;
938
+ }
939
+ const paramsCheckErrno = locking.check_lock_params(fd, op);
940
+ if (paramsCheckErrno !== 0) {
941
+ js_wasm_trace(
942
+ "flock(%d, %d) %s check_lock_params errno %d",
943
+ fd,
944
+ op,
945
+ vfsPath,
946
+ paramsCheckErrno
947
+ );
948
+ return -paramsCheckErrno;
949
+ }
950
+ const maskedOp = op & (LOCK_SH | LOCK_EX | LOCK_UN);
951
+ const waitForLock = (op & LOCK_NB) === 0;
952
+ if (maskedOp === 0) {
953
+ js_wasm_trace("flock(%d, %d) invalid flock() operation", fd, op);
954
+ return -EINVAL;
955
+ }
956
+ const lockOpType = flockToLockOpType[maskedOp];
957
+ if (lockOpType === void 0) {
958
+ js_wasm_trace("flock(%d, %d) invalid flock() operation", fd, op);
959
+ return -EINVAL;
960
+ }
961
+ const [nativeFd, nativeFdErrno] = locking.get_native_fd_from_emscripten_fd(fd);
962
+ if (nativeFdErrno !== 0) {
963
+ js_wasm_trace(
964
+ "js_flock(%d, %d) get_native_fd_from_emscripten_fd errno %d",
965
+ fd,
966
+ op,
967
+ nativeFdErrno
968
+ );
969
+ return -nativeFdErrno;
970
+ }
971
+ try {
972
+ const nativeFilePath = locking.get_native_path_from_vfs_path(vfsPath);
973
+ const succeeded = fileLockManager.lockWholeFile(nativeFilePath, {
974
+ type: lockOpType,
975
+ pid,
976
+ fd: nativeFd,
977
+ waitForLock
978
+ });
979
+ js_wasm_trace(
980
+ "flock(%d, %d) lockWholeFile %s returned %d",
981
+ fd,
982
+ op,
983
+ vfsPath,
984
+ succeeded
985
+ );
986
+ if (succeeded) {
987
+ locking.maybeLockedFds.add(nativeFd);
988
+ }
989
+ return succeeded ? 0 : -EWOULDBLOCK;
990
+ } catch (e) {
991
+ js_wasm_trace("flock(%d, %d) lockWholeFile error %s", fd, op, e);
992
+ return -EINVAL;
993
+ }
994
+ }
995
+ function fd_close(fd) {
996
+ if (!fileLockManager) {
997
+ js_wasm_trace(
998
+ "fd_close(%d) file lock manager is not available. delegate to Emscripten builtin fd_close.",
999
+ fd
1000
+ );
1001
+ return builtin_fd_close(fd);
1002
+ }
1003
+ const [vfsPath, vfsPathResolutionErrno] = locking.get_vfs_path_from_fd(fd);
1004
+ const [nativeFd, nativeFdErrno] = locking.get_native_fd_from_emscripten_fd(fd);
1005
+ const fdCloseResult = builtin_fd_close(fd);
1006
+ if (fdCloseResult !== 0) {
1007
+ js_wasm_trace(
1008
+ "fd_close(%d) %s result %d",
1009
+ fd,
1010
+ vfsPath,
1011
+ fdCloseResult
1012
+ );
1013
+ return fdCloseResult;
1014
+ }
1015
+ if (!locking.maybeLockedFds.has(nativeFd)) {
1016
+ js_wasm_trace(
1017
+ "fd_close(%d) not in maybe-locked-list %s result %d",
1018
+ fd,
1019
+ vfsPath,
1020
+ fdCloseResult
1021
+ );
1022
+ return fdCloseResult;
1023
+ }
1024
+ if (vfsPathResolutionErrno !== 0) {
1025
+ js_wasm_trace(
1026
+ "fd_close(%d) get_vfs_path_from_fd error %d",
1027
+ fd,
1028
+ vfsPathResolutionErrno
1029
+ );
1030
+ return fdCloseResult;
1031
+ }
1032
+ if (nativeFdErrno !== 0) {
1033
+ js_wasm_trace(
1034
+ "fd_close(%d) %s get_native_fd_from_emscripten_fd error %d",
1035
+ fd,
1036
+ vfsPath,
1037
+ nativeFdErrno
1038
+ );
1039
+ return fdCloseResult;
1040
+ }
1041
+ if (!locking.is_path_to_shared_fs(vfsPath)) {
1042
+ return fdCloseResult;
1043
+ }
1044
+ try {
1045
+ js_wasm_trace("fd_close(%d) %s release locks", fd, vfsPath);
1046
+ const nativeFilePath = locking.get_native_path_from_vfs_path(vfsPath);
1047
+ fileLockManager.releaseLocksOnFdClose(
1048
+ pid,
1049
+ nativeFd,
1050
+ nativeFilePath
1051
+ );
1052
+ js_wasm_trace("fd_close(%d) %s release locks success", fd, vfsPath);
1053
+ } catch (e) {
1054
+ js_wasm_trace("fd_close(%d) %s error '%s'", fd, vfsPath, e);
1055
+ }
1056
+ return fdCloseResult;
1057
+ }
1058
+ function js_release_file_locks() {
1059
+ js_wasm_trace("js_release_file_locks()");
1060
+ if (pid === void 0) {
1061
+ js_wasm_trace("js_release_file_locks pid is undefined");
1062
+ return;
1063
+ }
1064
+ if (fileLockManager === void 0) {
1065
+ js_wasm_trace(
1066
+ "js_release_file_locks file lock manager is undefined"
1067
+ );
1068
+ return;
1069
+ }
1070
+ try {
1071
+ fileLockManager.releaseLocksForProcess(pid);
1072
+ js_wasm_trace("js_release_file_locks succeeded");
1073
+ } catch (e) {
1074
+ js_wasm_trace("js_release_file_locks error %s", e);
1075
+ }
1076
+ }
1077
+ async function gethostbyname(hostname) {
1078
+ const { address } = await lookup2(hostname, {
1079
+ family: 4,
1080
+ verbatim: false
1081
+ });
1082
+ return address;
1083
+ }
1084
+ return {
1085
+ fcntl64,
1086
+ flock,
1087
+ fd_close,
1088
+ js_release_file_locks,
1089
+ gethostbyname
1090
+ };
1091
+ }
1092
+
1093
+ // packages/php-wasm/node/src/lib/load-runtime.ts
394
1094
  import fs5 from "fs";
395
1095
 
1096
+ // packages/php-wasm/node/src/lib/file-lock-manager-for-posix.ts
1097
+ import { MAX_ADDRESSABLE_FILE_OFFSET } from "@php-wasm/universal";
1098
+ import { constants, fcntlSync, flockSync } from "fs-ext-extra-prebuilt";
1099
+ import { logger as logger2 } from "@php-wasm/logger";
1100
+ function isLockDenialError(e) {
1101
+ if (e && typeof e === "object" && "code" in e) {
1102
+ const code = e.code;
1103
+ return code === "EWOULDBLOCK" || code === "EAGAIN" || code === "EACCES";
1104
+ }
1105
+ return false;
1106
+ }
1107
+ var FileLockManagerForPosix = class {
1108
+ constructor() {
1109
+ this.wholeFileLockMap = /* @__PURE__ */ new Map();
1110
+ this.rangeLockedFds = /* @__PURE__ */ new Map();
1111
+ }
1112
+ lockWholeFile(path2, op) {
1113
+ const opType = op.type === "unlock" ? "un" : op.waitForLock ? op.type === "exclusive" ? "ex" : "sh" : op.type === "exclusive" ? "exnb" : "shnb";
1114
+ try {
1115
+ flockSync(op.fd, opType);
1116
+ if (op.type === "unlock") {
1117
+ this.wholeFileLockMap.get(op.pid)?.delete(op.fd);
1118
+ } else {
1119
+ if (!this.wholeFileLockMap.has(op.pid)) {
1120
+ this.wholeFileLockMap.set(op.pid, /* @__PURE__ */ new Map());
1121
+ }
1122
+ this.wholeFileLockMap.get(op.pid).set(op.fd, {
1123
+ ...op,
1124
+ path: path2
1125
+ });
1126
+ }
1127
+ return true;
1128
+ } catch (e) {
1129
+ if (!isLockDenialError(e)) {
1130
+ logger2.warn("flock(): unexpected error", e);
1131
+ }
1132
+ return false;
1133
+ }
1134
+ }
1135
+ lockFileByteRange(path2, op, waitForLock) {
1136
+ if (op.start === op.end) {
1137
+ op = {
1138
+ ...op,
1139
+ end: MAX_ADDRESSABLE_FILE_OFFSET
1140
+ };
1141
+ }
1142
+ const fcntlCmd = waitForLock ? "setlkw" : "setlk";
1143
+ const fcntlOp = op.type === "unlocked" ? constants.F_UNLCK : op.type === "exclusive" ? constants.F_WRLCK : constants.F_RDLCK;
1144
+ try {
1145
+ fcntlSync(
1146
+ op.fd,
1147
+ fcntlCmd,
1148
+ fcntlOp,
1149
+ Number(op.start),
1150
+ Number(op.end - op.start)
1151
+ );
1152
+ if (!this.rangeLockedFds.has(op.pid)) {
1153
+ this.rangeLockedFds.set(op.pid, /* @__PURE__ */ new Map());
1154
+ }
1155
+ const pidMap = this.rangeLockedFds.get(op.pid);
1156
+ if (!pidMap.has(path2)) {
1157
+ pidMap.set(path2, /* @__PURE__ */ new Set());
1158
+ }
1159
+ pidMap.get(path2).add(op.fd);
1160
+ return true;
1161
+ } catch (e) {
1162
+ if (!isLockDenialError(e)) {
1163
+ logger2.warn("fcntl(): unexpected error", e);
1164
+ }
1165
+ return false;
1166
+ }
1167
+ }
1168
+ findFirstConflictingByteRangeLock(path2, op) {
1169
+ if (op.type === "unlocked") {
1170
+ return void 0;
1171
+ }
1172
+ const obtainedLock = this.lockFileByteRange(path2, op, false);
1173
+ if (obtainedLock) {
1174
+ this.lockFileByteRange(path2, { ...op, type: "unlocked" }, true);
1175
+ return void 0;
1176
+ }
1177
+ return {
1178
+ type: "exclusive",
1179
+ start: 0n,
1180
+ end: 0xffffffffffffffffn,
1181
+ pid: -1
1182
+ };
1183
+ }
1184
+ releaseLocksForProcess(targetPid) {
1185
+ const fdMap = this.wholeFileLockMap.get(targetPid);
1186
+ if (fdMap) {
1187
+ for (const storedLock of fdMap.values()) {
1188
+ try {
1189
+ this.lockWholeFile(storedLock.path, {
1190
+ ...storedLock,
1191
+ type: "unlock"
1192
+ });
1193
+ } catch (e) {
1194
+ logger2.error(
1195
+ `releaseLocksForProcess: failed to unlock whole-file lock for pid=${targetPid} fd=${storedLock.fd}`,
1196
+ e
1197
+ );
1198
+ }
1199
+ }
1200
+ this.wholeFileLockMap.delete(targetPid);
1201
+ }
1202
+ for (const [path2, fdSet] of this.rangeLockedFds.get(targetPid) ?? []) {
1203
+ for (const fd of fdSet) {
1204
+ try {
1205
+ this.lockFileByteRange(
1206
+ path2,
1207
+ {
1208
+ pid: targetPid,
1209
+ fd,
1210
+ type: "unlocked",
1211
+ start: 0n,
1212
+ end: MAX_ADDRESSABLE_FILE_OFFSET
1213
+ },
1214
+ false
1215
+ );
1216
+ } catch (e) {
1217
+ logger2.error(
1218
+ `releaseLocksForProcess: failed to unlock byte range for pid=${targetPid} fd=${fd} path=${path2}`,
1219
+ e
1220
+ );
1221
+ }
1222
+ }
1223
+ }
1224
+ this.rangeLockedFds.delete(targetPid);
1225
+ }
1226
+ releaseLocksOnFdClose(targetPid, targetFd, targetPath) {
1227
+ this.wholeFileLockMap.get(targetPid)?.delete(targetFd);
1228
+ this.rangeLockedFds.get(targetPid)?.delete(targetPath);
1229
+ }
1230
+ };
1231
+
1232
+ // packages/php-wasm/node/src/lib/file-lock-manager-for-windows.ts
1233
+ import {
1234
+ lockFileExSync,
1235
+ unlockFileExSync,
1236
+ constants as constants2
1237
+ } from "fs-ext-extra-prebuilt";
1238
+ import { logger as logger3 } from "@php-wasm/logger";
1239
+ import {
1240
+ MAX_ADDRESSABLE_FILE_OFFSET as MAX_ADDRESSABLE_FILE_OFFSET2,
1241
+ FileLockIntervalTree
1242
+ } from "@php-wasm/universal";
1243
+ function toLowAndHigh32BitNumbers(num) {
1244
+ const low = Number(num & 0xffffffffn);
1245
+ const high = Number(num >> 32n & 0xffffffffn);
1246
+ return [low, high];
1247
+ }
1248
+ function isErrnoError(e) {
1249
+ return e !== null && typeof e === "object" && ("errno" in e || "code" in e || "syscall" in e);
1250
+ }
1251
+ function tryLockFileExSync(fd, flags, start, end) {
1252
+ const [offsetLow, offsetHigh] = toLowAndHigh32BitNumbers(start);
1253
+ const [lengthLow, lengthHigh] = toLowAndHigh32BitNumbers(end - start);
1254
+ try {
1255
+ lockFileExSync(fd, flags, offsetLow, offsetHigh, lengthLow, lengthHigh);
1256
+ return true;
1257
+ } catch (e) {
1258
+ if (!isErrnoError(e)) {
1259
+ throw e;
1260
+ }
1261
+ return false;
1262
+ }
1263
+ }
1264
+ function tryUnlockFileExSync(fd, start, end) {
1265
+ const [offsetLow, offsetHigh] = toLowAndHigh32BitNumbers(start);
1266
+ const [lengthLow, lengthHigh] = toLowAndHigh32BitNumbers(end - start);
1267
+ try {
1268
+ unlockFileExSync(fd, offsetLow, offsetHigh, lengthLow, lengthHigh);
1269
+ return true;
1270
+ } catch (e) {
1271
+ if (!isErrnoError(e)) {
1272
+ throw e;
1273
+ }
1274
+ return false;
1275
+ }
1276
+ }
1277
+ var FileLockManagerForWindows = class {
1278
+ constructor() {
1279
+ this.wholeFileLockMap = /* @__PURE__ */ new Map();
1280
+ this.rangeLockedFds = /* @__PURE__ */ new Map();
1281
+ }
1282
+ lockWholeFile(path2, op) {
1283
+ const start = 0n;
1284
+ const end = 2n ** 64n - 1n;
1285
+ if (op.type === "unlock") {
1286
+ const success2 = tryUnlockFileExSync(op.fd, start, end);
1287
+ if (success2) {
1288
+ this.wholeFileLockMap.get(op.pid)?.delete(op.fd);
1289
+ if (this.wholeFileLockMap.get(op.pid)?.size === 0) {
1290
+ this.wholeFileLockMap.delete(op.pid);
1291
+ }
1292
+ } else {
1293
+ logger3.warn(
1294
+ `lockWholeFile: unlock failed for pid=${op.pid} fd=${op.fd} path=${path2}`
1295
+ );
1296
+ }
1297
+ return success2;
1298
+ }
1299
+ const preexistingLock = this.wholeFileLockMap.get(op.pid)?.get(op.fd);
1300
+ if (op.type === preexistingLock?.type) {
1301
+ return true;
1302
+ }
1303
+ let flags = 0;
1304
+ if (!op.waitForLock) {
1305
+ flags |= constants2.LOCKFILE_FAIL_IMMEDIATELY;
1306
+ }
1307
+ let success = false;
1308
+ if (op.type === "shared") {
1309
+ success = tryLockFileExSync(op.fd, flags, start, end);
1310
+ if (success && preexistingLock?.type === "exclusive") {
1311
+ const exclusiveUnlockSuccess = tryUnlockFileExSync(
1312
+ op.fd,
1313
+ start,
1314
+ end
1315
+ );
1316
+ if (!exclusiveUnlockSuccess) {
1317
+ const message = "Failed to unlock preexisting exclusive lock after failing to obtain shared lock";
1318
+ logger3.error(message);
1319
+ throw new Error(message);
1320
+ }
1321
+ }
1322
+ } else if (op.type === "exclusive") {
1323
+ flags |= constants2.LOCKFILE_EXCLUSIVE_LOCK;
1324
+ let sharedUnlockSuccess;
1325
+ if (preexistingLock?.type === "shared") {
1326
+ sharedUnlockSuccess = tryUnlockFileExSync(op.fd, start, end);
1327
+ if (!sharedUnlockSuccess) {
1328
+ logger3.warn(
1329
+ `lockWholeFile: failed to release shared lock before exclusive upgrade for pid=${op.pid} fd=${op.fd}`
1330
+ );
1331
+ }
1332
+ }
1333
+ success = tryLockFileExSync(op.fd, flags, start, end);
1334
+ if (!success) {
1335
+ logger3.debug(
1336
+ `lockWholeFile: failed to obtain exclusive lock for pid=${op.pid} fd=${op.fd}`
1337
+ );
1338
+ }
1339
+ if (!success && sharedUnlockSuccess) {
1340
+ const sharedReLockResult = tryLockFileExSync(
1341
+ op.fd,
1342
+ // Wait to restore the shared lock.
1343
+ 0,
1344
+ start,
1345
+ end
1346
+ );
1347
+ if (!sharedReLockResult) {
1348
+ const message = "Failed to re-lock preexisting shared lock after failing to obtain exclusive lock";
1349
+ logger3.error(message);
1350
+ throw new Error(message);
1351
+ }
1352
+ }
1353
+ } else {
1354
+ throw new Error(`Unexpected wholeFileLock() op: '${op.type}'`);
1355
+ }
1356
+ if (success) {
1357
+ if (!this.wholeFileLockMap.has(op.pid)) {
1358
+ this.wholeFileLockMap.set(op.pid, /* @__PURE__ */ new Map());
1359
+ }
1360
+ this.wholeFileLockMap.get(op.pid).set(op.fd, {
1361
+ ...op,
1362
+ path: path2
1363
+ });
1364
+ }
1365
+ return success;
1366
+ }
1367
+ lockFileByteRange(path2, op, waitForLock) {
1368
+ if (op.start === op.end) {
1369
+ op = {
1370
+ ...op,
1371
+ end: MAX_ADDRESSABLE_FILE_OFFSET2
1372
+ };
1373
+ }
1374
+ if (!this.rangeLockedFds.has(path2)) {
1375
+ this.rangeLockedFds.set(path2, new FileLockIntervalTree());
1376
+ }
1377
+ const lockedRangeTree = this.rangeLockedFds.get(path2);
1378
+ const overlappingLocks = lockedRangeTree.findOverlapping(op);
1379
+ let preexistingLock;
1380
+ if (overlappingLocks.length === 1 && overlappingLocks[0].pid === op.pid && // NOTE: FD shouldn't matter for fcntl() F_SETLK because it is a process-level lock,
1381
+ // but it matters for Windows where locks are fd-specific.
1382
+ overlappingLocks[0].fd === op.fd && overlappingLocks[0].start === op.start && overlappingLocks[0].end === op.end) {
1383
+ preexistingLock = overlappingLocks[0];
1384
+ }
1385
+ if (op.type === preexistingLock?.type) {
1386
+ return true;
1387
+ }
1388
+ let flags = 0;
1389
+ if (!waitForLock) {
1390
+ flags |= constants2.LOCKFILE_FAIL_IMMEDIATELY;
1391
+ }
1392
+ if (op.type === "shared") {
1393
+ const success = tryLockFileExSync(op.fd, flags, op.start, op.end);
1394
+ if (!success) {
1395
+ return false;
1396
+ }
1397
+ if (preexistingLock?.type === "exclusive") {
1398
+ const releasedPreexistingExclusiveLock = tryUnlockFileExSync(
1399
+ preexistingLock.fd,
1400
+ preexistingLock.start,
1401
+ preexistingLock.end
1402
+ );
1403
+ if (!releasedPreexistingExclusiveLock) {
1404
+ const message = "Failed to unlock preexisting exclusive lock after obtaining a shared lock";
1405
+ logger3.error(message);
1406
+ throw new Error(message);
1407
+ }
1408
+ lockedRangeTree.remove(preexistingLock);
1409
+ }
1410
+ lockedRangeTree.insert(op);
1411
+ return true;
1412
+ } else if (op.type === "exclusive") {
1413
+ let sharedUnlockSuccess;
1414
+ if (preexistingLock?.type === "shared") {
1415
+ sharedUnlockSuccess = tryUnlockFileExSync(
1416
+ op.fd,
1417
+ op.start,
1418
+ op.end
1419
+ );
1420
+ }
1421
+ if (op.type === "exclusive") {
1422
+ flags |= constants2.LOCKFILE_EXCLUSIVE_LOCK;
1423
+ }
1424
+ const success = tryLockFileExSync(op.fd, flags, op.start, op.end);
1425
+ if (!success) {
1426
+ if (preexistingLock && sharedUnlockSuccess) {
1427
+ const sharedRelockSuccess = tryLockFileExSync(
1428
+ op.fd,
1429
+ 0,
1430
+ op.start,
1431
+ op.end
1432
+ );
1433
+ if (!sharedRelockSuccess) {
1434
+ const message = "Failed to re-lock preexisting shared lock after failing to obtain exclusive lock";
1435
+ logger3.error(message);
1436
+ throw new Error(message);
1437
+ }
1438
+ }
1439
+ return false;
1440
+ }
1441
+ lockedRangeTree.insert(op);
1442
+ return true;
1443
+ } else {
1444
+ const intersectingLocksForThisProcess = overlappingLocks.filter((lock) => lock.pid === op.pid).filter((lock) => lock.start >= op.start && lock.end <= op.end);
1445
+ for (const lock of intersectingLocksForThisProcess) {
1446
+ const success = tryUnlockFileExSync(
1447
+ lock.fd,
1448
+ lock.start,
1449
+ lock.end
1450
+ );
1451
+ if (!success) {
1452
+ logger3.warn(
1453
+ `lockFileByteRange: unlock failed for pid=${op.pid} fd=${lock.fd} range=[${lock.start},${lock.end}]`
1454
+ );
1455
+ return false;
1456
+ }
1457
+ lockedRangeTree.remove(lock);
1458
+ }
1459
+ return true;
1460
+ }
1461
+ }
1462
+ findFirstConflictingByteRangeLock(path2, op) {
1463
+ if (op.type === "unlocked") {
1464
+ return void 0;
1465
+ }
1466
+ const obtainedLock = !!this.lockFileByteRange(path2, op, false);
1467
+ this.lockFileByteRange(path2, { ...op, type: "unlocked" }, false);
1468
+ if (obtainedLock) {
1469
+ return void 0;
1470
+ }
1471
+ return {
1472
+ type: "exclusive",
1473
+ start: 0n,
1474
+ end: 0xffffffffffffffffn,
1475
+ pid: -1
1476
+ };
1477
+ }
1478
+ releaseLocksForProcess(targetPid) {
1479
+ const fdMap = this.wholeFileLockMap.get(targetPid);
1480
+ if (fdMap) {
1481
+ for (const storedLock of fdMap.values()) {
1482
+ try {
1483
+ this.lockWholeFile(storedLock.path, {
1484
+ ...storedLock,
1485
+ type: "unlock"
1486
+ });
1487
+ } catch (e) {
1488
+ logger3.error(
1489
+ `releaseLocksForProcess: failed to unlock whole-file lock for pid=${targetPid} fd=${storedLock.fd}`,
1490
+ e
1491
+ );
1492
+ }
1493
+ }
1494
+ this.wholeFileLockMap.delete(targetPid);
1495
+ }
1496
+ for (const [path2, lockedRangeTree] of this.rangeLockedFds.entries()) {
1497
+ const rangesLockedByTargetPid = lockedRangeTree.findLocksForProcess(targetPid);
1498
+ for (const op of rangesLockedByTargetPid) {
1499
+ try {
1500
+ this.lockFileByteRange(
1501
+ path2,
1502
+ { ...op, type: "unlocked" },
1503
+ false
1504
+ );
1505
+ } catch (e) {
1506
+ logger3.error(
1507
+ `releaseLocksForProcess: failed to unlock byte range for pid=${targetPid} fd=${op.fd} path=${path2}`,
1508
+ e
1509
+ );
1510
+ }
1511
+ lockedRangeTree.remove(op);
1512
+ }
1513
+ }
1514
+ }
1515
+ releaseLocksOnFdClose(targetPid, targetFd, targetPath) {
1516
+ const storedLock = this.wholeFileLockMap.get(targetPid)?.get(targetFd);
1517
+ if (storedLock) {
1518
+ this.lockWholeFile(storedLock.path, {
1519
+ ...storedLock,
1520
+ type: "unlock"
1521
+ });
1522
+ }
1523
+ this.wholeFileLockMap.get(targetPid)?.delete(targetFd);
1524
+ const lockedRangeTree = this.rangeLockedFds.get(targetPath);
1525
+ for (const op of lockedRangeTree?.findLocksForProcess(targetPid) ?? []) {
1526
+ this.lockFileByteRange(
1527
+ targetPath,
1528
+ {
1529
+ ...op,
1530
+ type: "unlocked",
1531
+ // Use a dummy FD because we're releasing locks for
1532
+ // all FDs on this file (POSIX semantics), not just
1533
+ // the one being closed.
1534
+ fd: -1
1535
+ },
1536
+ false
1537
+ );
1538
+ lockedRangeTree.remove(op);
1539
+ }
1540
+ }
1541
+ };
1542
+
396
1543
  // packages/php-wasm/node/src/lib/extensions/xdebug/with-xdebug.ts
397
1544
  import { LatestSupportedPHPVersion as LatestSupportedPHPVersion3, FSHelpers } from "@php-wasm/universal";
398
1545
  import fs from "fs";
@@ -709,6 +1856,7 @@ async function withMemcached(version = LatestSupportedPHPVersion9, options) {
709
1856
 
710
1857
  // packages/php-wasm/node/src/lib/load-runtime.ts
711
1858
  import { dirname, joinPaths, toPosixPath } from "@php-wasm/util";
1859
+ import { platform } from "os";
712
1860
  async function loadNodeRuntime(phpVersion, options = {}) {
713
1861
  let emscriptenOptions = {
714
1862
  /**
@@ -719,6 +1867,14 @@ async function loadNodeRuntime(phpVersion, options = {}) {
719
1867
  quit: function(code, error) {
720
1868
  throw error;
721
1869
  },
1870
+ bindUserSpace: (userSpaceContext) => {
1871
+ const nativeFileLockManager = platform() === "win32" ? new FileLockManagerForWindows() : new FileLockManagerForPosix();
1872
+ const fileLockManager = options.fileLockManager ? new FileLockManagerComposite({
1873
+ nativeLockManager: nativeFileLockManager,
1874
+ wasmLockManager: options.fileLockManager
1875
+ }) : nativeFileLockManager;
1876
+ return bindUserSpace({ fileLockManager }, userSpaceContext);
1877
+ },
722
1878
  ...options.emscriptenOptions || {},
723
1879
  onRuntimeInitialized: (phpRuntime) => {
724
1880
  if (options?.followSymlinks === true) {
@@ -821,9 +1977,9 @@ function createNodeFsMountHandler(localPath) {
821
1977
  }
822
1978
  removeVfsNode = true;
823
1979
  }
824
- let lookup2;
1980
+ let lookup3;
825
1981
  try {
826
- lookup2 = FS.lookupPath(vfsMountPoint);
1982
+ lookup3 = FS.lookupPath(vfsMountPoint);
827
1983
  } catch (e) {
828
1984
  const error = e;
829
1985
  if (error.errno === 44) {
@@ -837,7 +1993,7 @@ function createNodeFsMountHandler(localPath) {
837
1993
  return () => {
838
1994
  FS.unmount(vfsMountPoint);
839
1995
  if (removeVfsNode) {
840
- if (FS.isDir(lookup2.node.mode)) {
1996
+ if (FS.isDir(lookup3.node.mode)) {
841
1997
  if (isParentOf(vfsMountPoint, FS.cwd())) {
842
1998
  throw new Error(
843
1999
  `Cannot remove the VFS directory "${vfsMountPoint}" on umount cleanup \u2013 it is a parent of the CWD "${FS.cwd()}". Change CWD before unmounting or explicitly disable post-unmount node cleanup with createNodeFsMountHandler(path, {cleanupNodesOnUnmount: false}).`
@@ -891,641 +2047,10 @@ function statPathFollowSymlinks(path2) {
891
2047
  }
892
2048
  return stat;
893
2049
  }
894
-
895
- // packages/php-wasm/node/src/lib/file-lock-manager-for-node.ts
896
- import { logger as logger2 } from "@php-wasm/logger";
897
- import { openSync, closeSync } from "fs";
898
- var MAX_64BIT_OFFSET = BigInt(2n ** 64n - 1n);
899
- var FileLockManagerForNode = class {
900
- /**
901
- * Create a new FileLockManagerForNode instance.
902
- *
903
- * @param nativeFlockSync A synchronous flock() function to lock files via the host OS.
904
- */
905
- constructor(nativeFlockSync = function flockSyncNoOp() {
906
- }) {
907
- this.nativeFlockSync = nativeFlockSync;
908
- this.locks = /* @__PURE__ */ new Map();
909
- }
910
- /**
911
- * Lock the whole file.
912
- *
913
- * @param path The path to the file to lock. This should be the path
914
- * of the file in the native filesystem.
915
- * @param op The whole file lock operation to perform.
916
- * @returns True if the lock was granted, false otherwise.
917
- */
918
- lockWholeFile(path2, op) {
919
- if (this.locks.get(path2) === void 0) {
920
- if (op.type === "unlock") {
921
- return true;
922
- }
923
- const maybeLock = FileLock.maybeCreate(
924
- path2,
925
- op.type,
926
- this.nativeFlockSync
927
- );
928
- if (maybeLock === void 0) {
929
- return false;
930
- }
931
- this.locks.set(path2, maybeLock);
932
- }
933
- const lock = this.locks.get(path2);
934
- const result = lock.lockWholeFile(op);
935
- this.forgetPathIfUnlocked(path2);
936
- return result;
937
- }
938
- /**
939
- * Lock a byte range.
940
- *
941
- * @param path The path to the file to lock. This should be the path
942
- * of the file in the native filesystem.
943
- * @param requestedLock The byte range lock to perform.
944
- * @returns True if the lock was granted, false otherwise.
945
- */
946
- lockFileByteRange(path2, requestedLock) {
947
- if (!this.locks.has(path2)) {
948
- if (requestedLock.type === "unlocked") {
949
- return true;
950
- }
951
- const maybeLock = FileLock.maybeCreate(
952
- path2,
953
- requestedLock.type,
954
- this.nativeFlockSync
955
- );
956
- if (maybeLock === void 0) {
957
- return false;
958
- }
959
- this.locks.set(path2, maybeLock);
960
- }
961
- const lock = this.locks.get(path2);
962
- return lock.lockFileByteRange(requestedLock);
963
- }
964
- /**
965
- * Find the first conflicting byte range lock.
966
- *
967
- * @param path The path to the file to find the conflicting lock for.
968
- * @param desiredLock The desired byte range lock.
969
- * @returns The first conflicting byte range lock, or undefined if no conflicting lock exists.
970
- */
971
- findFirstConflictingByteRangeLock(path2, desiredLock) {
972
- const lock = this.locks.get(path2);
973
- if (lock === void 0) {
974
- return void 0;
975
- }
976
- return lock.findFirstConflictingByteRangeLock(desiredLock);
977
- }
978
- /**
979
- * Release all locks for the given process.
980
- *
981
- * @param pid The process ID to release locks for.
982
- */
983
- releaseLocksForProcess(pid) {
984
- for (const [path2, lock] of this.locks.entries()) {
985
- lock.releaseLocksForProcess(pid);
986
- this.forgetPathIfUnlocked(path2);
987
- }
988
- }
989
- /**
990
- * Release all locks for the given process and file descriptor.
991
- *
992
- * @param pid The process ID to release locks for.
993
- * @param fd The file descriptor to release locks for.
994
- * @param path The path to the file to release locks for.
995
- */
996
- releaseLocksForProcessFd(pid, fd, nativePath) {
997
- const lock = this.locks.get(nativePath);
998
- if (!lock) {
999
- return;
1000
- }
1001
- lock.releaseLocksForProcessFd(pid, fd);
1002
- this.forgetPathIfUnlocked(nativePath);
1003
- }
1004
- /**
1005
- * Forget the path if it is unlocked.
1006
- *
1007
- * @param path The path to the file to forget.
1008
- */
1009
- forgetPathIfUnlocked(path2) {
1010
- const lock = this.locks.get(path2);
1011
- if (!lock) {
1012
- return;
1013
- }
1014
- if (lock.isUnlocked()) {
1015
- lock.dispose();
1016
- this.locks.delete(path2);
1017
- }
1018
- }
1019
- };
1020
- var FileLock = class _FileLock {
1021
- /**
1022
- * Create a new FileLock instance for the given file and mode.
1023
- * Fail if the underlying native file lock cannot be acquired.
1024
- *
1025
- * @param path The path to the file to lock
1026
- * @param mode The type of lock to acquire
1027
- * @returns A FileLock instance if the lock was acquired, undefined otherwise
1028
- */
1029
- static maybeCreate(path2, mode, nativeFlockSync) {
1030
- let fd;
1031
- try {
1032
- fd = openSync(path2, "a+");
1033
- const flockFlags = mode === "exclusive" ? "exnb" : "shnb";
1034
- nativeFlockSync(fd, flockFlags);
1035
- const nativeLock = { fd, mode, nativeFlockSync };
1036
- return new _FileLock(nativeLock);
1037
- } catch {
1038
- if (fd !== void 0) {
1039
- try {
1040
- closeSync(fd);
1041
- } catch (error) {
1042
- logger2.error(
1043
- "Error closing locking file descriptor",
1044
- error
1045
- );
1046
- }
1047
- }
1048
- return void 0;
1049
- }
1050
- }
1051
- constructor(nativeLock) {
1052
- this.nativeLock = nativeLock;
1053
- this.rangeLocks = new FileLockIntervalTree();
1054
- this.wholeFileLock = { type: "unlocked" };
1055
- }
1056
- /**
1057
- * Close the file descriptor and release the native lock.
1058
- *
1059
- * @TODO Replace this with a Symbol.dispose property once supported by all JS runtimes.
1060
- */
1061
- dispose() {
1062
- try {
1063
- closeSync(this.nativeLock.fd);
1064
- } catch (error) {
1065
- logger2.error("Error closing locking file descriptor", error);
1066
- }
1067
- }
1068
- /**
1069
- * Lock the whole file.
1070
- *
1071
- * This method corresponds to the flock() function.
1072
- *
1073
- * @param op The whole file lock operation to perform.
1074
- * @returns True if the lock was granted, false otherwise.
1075
- */
1076
- lockWholeFile(op) {
1077
- if (op.type === "unlock") {
1078
- const originalType = this.wholeFileLock.type;
1079
- if (originalType === "unlocked") {
1080
- } else if (this.wholeFileLock.type === "exclusive" && this.wholeFileLock.pid === op.pid && this.wholeFileLock.fd === op.fd) {
1081
- this.wholeFileLock = { type: "unlocked" };
1082
- } else if (this.wholeFileLock.type === "shared" && this.wholeFileLock.pidFds.has(op.pid) && this.wholeFileLock.pidFds.get(op.pid).has(op.fd)) {
1083
- this.wholeFileLock.pidFds.get(op.pid).delete(op.fd);
1084
- if (this.wholeFileLock.pidFds.get(op.pid).size === 0) {
1085
- this.wholeFileLock.pidFds.delete(op.pid);
1086
- }
1087
- if (this.wholeFileLock.pidFds.size === 0) {
1088
- this.wholeFileLock = { type: "unlocked" };
1089
- }
1090
- }
1091
- if (!this.ensureCompatibleNativeLock()) {
1092
- logger2.error(
1093
- "Unable to update native lock after removing a whole file lock."
1094
- );
1095
- }
1096
- return true;
1097
- }
1098
- if (this.isThereAConflictWithRequestedWholeFileLock(op)) {
1099
- return false;
1100
- }
1101
- if (!this.ensureCompatibleNativeLock({
1102
- overrideWholeFileLockType: op.type
1103
- })) {
1104
- return false;
1105
- }
1106
- if (op.type === "exclusive") {
1107
- this.wholeFileLock = {
1108
- type: "exclusive",
1109
- pid: op.pid,
1110
- fd: op.fd
1111
- };
1112
- return true;
1113
- }
1114
- if (op.type === "shared") {
1115
- if (this.wholeFileLock.type !== "shared") {
1116
- this.wholeFileLock = {
1117
- type: "shared",
1118
- pidFds: /* @__PURE__ */ new Map()
1119
- };
1120
- }
1121
- const sharedLock = this.wholeFileLock;
1122
- if (!sharedLock.pidFds.has(op.pid)) {
1123
- sharedLock.pidFds.set(op.pid, /* @__PURE__ */ new Set());
1124
- }
1125
- sharedLock.pidFds.get(op.pid).add(op.fd);
1126
- return true;
1127
- }
1128
- throw new Error(`Unexpected wholeFileLock() op: '${op.type}'`);
1129
- }
1130
- /**
1131
- * Lock a byte range.
1132
- *
1133
- * This method corresponds to the fcntl() F_SETLK command.
1134
- *
1135
- * @param requestedLock The byte range lock to perform.
1136
- * @returns True if the lock was granted, false otherwise.
1137
- */
1138
- lockFileByteRange(requestedLock) {
1139
- if (requestedLock.start === requestedLock.end) {
1140
- requestedLock = {
1141
- ...requestedLock,
1142
- end: MAX_64BIT_OFFSET
1143
- };
1144
- }
1145
- if (requestedLock.type === "unlocked") {
1146
- const overlappingLocksBySameProcess = this.rangeLocks.findOverlapping(requestedLock).filter((lock) => lock.pid === requestedLock.pid);
1147
- for (const overlappingLock of overlappingLocksBySameProcess) {
1148
- this.rangeLocks.remove(overlappingLock);
1149
- if (overlappingLock.start < requestedLock.start) {
1150
- this.rangeLocks.insert({
1151
- ...overlappingLock,
1152
- end: requestedLock.start
1153
- });
1154
- }
1155
- if (overlappingLock.end > requestedLock.end) {
1156
- this.rangeLocks.insert({
1157
- ...overlappingLock,
1158
- start: requestedLock.end
1159
- });
1160
- }
1161
- }
1162
- if (!this.ensureCompatibleNativeLock()) {
1163
- logger2.error(
1164
- "Unable to update native lock after removing a byte range lock."
1165
- );
1166
- }
1167
- return true;
1168
- }
1169
- if (this.isThereAConflictWithRequestedRangeLock(requestedLock)) {
1170
- return false;
1171
- }
1172
- if (!this.ensureCompatibleNativeLock({
1173
- overrideRangeLockType: requestedLock.type
1174
- })) {
1175
- return false;
1176
- }
1177
- const overlappingLocksFromSameProcess = this.rangeLocks.findOverlapping(requestedLock).filter((lock) => lock.pid === requestedLock.pid);
1178
- let minStart = requestedLock.start;
1179
- let maxEnd = requestedLock.end;
1180
- for (const overlappingLock of overlappingLocksFromSameProcess) {
1181
- this.rangeLocks.remove(overlappingLock);
1182
- if (overlappingLock.start < minStart) {
1183
- minStart = overlappingLock.start;
1184
- }
1185
- if (overlappingLock.end > maxEnd) {
1186
- maxEnd = overlappingLock.end;
1187
- }
1188
- }
1189
- const mergedLock = {
1190
- ...requestedLock,
1191
- start: minStart,
1192
- end: maxEnd
1193
- };
1194
- this.rangeLocks.insert(mergedLock);
1195
- return true;
1196
- }
1197
- /**
1198
- * Find the first conflicting byte range lock.
1199
- *
1200
- * This method corresponds to the fcntl() F_GETLK command.
1201
- *
1202
- * @param desiredLock The desired byte range lock.
1203
- * @returns The first conflicting byte range lock, or undefined if no conflicting lock exists.
1204
- */
1205
- findFirstConflictingByteRangeLock(desiredLock) {
1206
- const overlappingLocks = this.rangeLocks.findOverlapping(desiredLock);
1207
- const firstConflictingRangeLock = overlappingLocks.find(
1208
- (lock) => lock.pid !== desiredLock.pid && (desiredLock.type === "exclusive" || lock.type === "exclusive")
1209
- );
1210
- if (firstConflictingRangeLock) {
1211
- return firstConflictingRangeLock;
1212
- }
1213
- if (this.wholeFileLock.type === "unlocked") {
1214
- return void 0;
1215
- }
1216
- const wfl = this.wholeFileLock;
1217
- if (wfl.type === "exclusive" || desiredLock.type === "exclusive") {
1218
- return {
1219
- type: this.wholeFileLock.type,
1220
- start: 0n,
1221
- end: 0n,
1222
- pid: -1
1223
- };
1224
- }
1225
- return void 0;
1226
- }
1227
- /**
1228
- * Release all locks for the given process.
1229
- *
1230
- * @param pid The process ID to release locks for.
1231
- */
1232
- releaseLocksForProcess(pid) {
1233
- for (const rangeLock of this.rangeLocks.findLocksForProcess(pid)) {
1234
- this.lockFileByteRange({
1235
- ...rangeLock,
1236
- type: "unlocked"
1237
- });
1238
- }
1239
- if (this.wholeFileLock.type === "exclusive" && this.wholeFileLock.pid === pid) {
1240
- this.lockWholeFile({
1241
- pid,
1242
- fd: this.wholeFileLock.fd,
1243
- type: "unlock"
1244
- });
1245
- } else if (this.wholeFileLock.type === "shared" && this.wholeFileLock.pidFds.has(pid)) {
1246
- for (const fd of this.wholeFileLock.pidFds.get(pid)) {
1247
- this.lockWholeFile({
1248
- pid,
1249
- fd,
1250
- type: "unlock"
1251
- });
1252
- }
1253
- }
1254
- }
1255
- /**
1256
- * Release all locks for the given process and file descriptor.
1257
- *
1258
- * @param pid The process ID to release locks for.
1259
- * @param fd The file descriptor to release locks for.
1260
- */
1261
- releaseLocksForProcessFd(pid, fd) {
1262
- for (const rangeLock of this.rangeLocks.findLocksForProcess(pid)) {
1263
- this.lockFileByteRange({
1264
- ...rangeLock,
1265
- type: "unlocked"
1266
- });
1267
- }
1268
- this.lockWholeFile({
1269
- pid,
1270
- fd,
1271
- type: "unlock"
1272
- });
1273
- }
1274
- /**
1275
- * Check if the file lock is unlocked.
1276
- *
1277
- * @returns True if the file lock is unlocked, false otherwise.
1278
- */
1279
- isUnlocked() {
1280
- return this.wholeFileLock.type === "unlocked" && this.rangeLocks.isEmpty();
1281
- }
1282
- /**
1283
- * Ensure that the native lock is compatible with the php-wasm lock,
1284
- * upgrading or downgrading as needed.
1285
- *
1286
- * @param overrideWholeFileLockType If provided, use this type for the whole file lock.
1287
- * @param overrideRangeLockType If provided, use this type for the range lock.
1288
- * @returns True if the native lock was upgraded or downgraded, false otherwise.
1289
- */
1290
- ensureCompatibleNativeLock({
1291
- overrideWholeFileLockType,
1292
- overrideRangeLockType
1293
- } = {}) {
1294
- const wholeFileLockType = overrideWholeFileLockType ?? this.wholeFileLock.type;
1295
- const rangeLockType = overrideRangeLockType ?? this.rangeLocks.findStrictestExistingLockType();
1296
- let requiredNativeLockType;
1297
- if (wholeFileLockType === "exclusive" || rangeLockType === "exclusive") {
1298
- requiredNativeLockType = "exclusive";
1299
- } else if (wholeFileLockType === "shared" || rangeLockType === "shared") {
1300
- requiredNativeLockType = "shared";
1301
- } else {
1302
- requiredNativeLockType = "unlock";
1303
- }
1304
- if (this.nativeLock.mode === requiredNativeLockType) {
1305
- return true;
1306
- }
1307
- const flockFlags = requiredNativeLockType === "exclusive" && "exnb" || requiredNativeLockType === "shared" && "shnb" || "un";
1308
- try {
1309
- this.nativeLock.nativeFlockSync(this.nativeLock.fd, flockFlags);
1310
- this.nativeLock.mode = requiredNativeLockType;
1311
- return true;
1312
- } catch {
1313
- return false;
1314
- }
1315
- }
1316
- /**
1317
- * Check if a lock exists that conflicts with the requested range lock.
1318
- *
1319
- * @param requestedLock The desired byte range lock.
1320
- * @returns True if a conflicting lock exists, false otherwise.
1321
- */
1322
- isThereAConflictWithRequestedRangeLock(requestedLock) {
1323
- return this.findFirstConflictingByteRangeLock(requestedLock) !== void 0;
1324
- }
1325
- /**
1326
- * Check if a lock exists that conflicts with the requested whole-file lock.
1327
- *
1328
- * @param requestedLock The desired whole-file lock.
1329
- * @returns True if a conflicting lock exists, false otherwise.
1330
- */
1331
- isThereAConflictWithRequestedWholeFileLock(requestedLock) {
1332
- if (requestedLock.type === "exclusive") {
1333
- if (this.wholeFileLock.type === "exclusive" && (this.wholeFileLock.fd !== requestedLock.fd || this.wholeFileLock.pid !== requestedLock.pid)) {
1334
- return true;
1335
- }
1336
- if (this.wholeFileLock.type === "shared" && Array.from(this.wholeFileLock.pidFds).some(
1337
- ([pid]) => pid !== requestedLock.pid
1338
- )) {
1339
- return true;
1340
- }
1341
- const overlappingLocks = this.rangeLocks.findOverlapping({
1342
- type: "unlocked",
1343
- start: 0n,
1344
- end: MAX_64BIT_OFFSET,
1345
- pid: -1
1346
- });
1347
- if (overlappingLocks.length > 0) {
1348
- return true;
1349
- }
1350
- return false;
1351
- }
1352
- if (requestedLock.type === "shared") {
1353
- if (this.wholeFileLock.type === "exclusive" && this.wholeFileLock.pid !== requestedLock.pid) {
1354
- return true;
1355
- }
1356
- const overlappingLocks = this.rangeLocks.findOverlapping({
1357
- type: "unlocked",
1358
- start: 0n,
1359
- end: MAX_64BIT_OFFSET,
1360
- pid: -1
1361
- });
1362
- const exclusiveRangeLocks = overlappingLocks.filter(
1363
- (lock) => lock.type === "exclusive"
1364
- );
1365
- if (exclusiveRangeLocks.length > 0) {
1366
- return true;
1367
- }
1368
- return false;
1369
- }
1370
- return false;
1371
- }
1372
- };
1373
- var IntervalNode = class {
1374
- constructor(range) {
1375
- this.left = null;
1376
- this.right = null;
1377
- this.range = range;
1378
- this.max = range.end;
1379
- }
1380
- };
1381
- var FileLockIntervalTree = class {
1382
- constructor() {
1383
- this.root = null;
1384
- }
1385
- isEmpty() {
1386
- return this.root === null;
1387
- }
1388
- /**
1389
- * Insert a new locked range into the tree
1390
- */
1391
- insert(range) {
1392
- this.root = this.insertNode(this.root, range);
1393
- }
1394
- /**
1395
- * Find all ranges that overlap with the given range
1396
- */
1397
- findOverlapping(range) {
1398
- const result = [];
1399
- this.findOverlappingRanges(this.root, range, result);
1400
- return result;
1401
- }
1402
- /**
1403
- * Remove a lock range from the tree
1404
- */
1405
- remove(range) {
1406
- this.root = this.removeNode(this.root, range);
1407
- }
1408
- /**
1409
- * Find all ranges locked by the given process.
1410
- *
1411
- * @param pid The process ID to find locks for.
1412
- * @returns All locked ranges for the given process.
1413
- */
1414
- findLocksForProcess(pid) {
1415
- const result = [];
1416
- this.findLocksForProcessInNode(this.root, pid, result);
1417
- return result;
1418
- }
1419
- /**
1420
- * Find the strictest existing lock type in the range lock tree.
1421
- *
1422
- * @returns The strictest existing lock type, or 'unlocked' if no locks exist.
1423
- */
1424
- findStrictestExistingLockType() {
1425
- let maxType = "unlocked";
1426
- const traverse = (node) => {
1427
- if (!node) {
1428
- return;
1429
- }
1430
- if (node.range.type === "exclusive") {
1431
- maxType = "exclusive";
1432
- return;
1433
- }
1434
- if (node.range.type === "shared") {
1435
- maxType = "shared";
1436
- }
1437
- traverse(node.left);
1438
- traverse(node.right);
1439
- };
1440
- traverse(this.root);
1441
- return maxType;
1442
- }
1443
- insertNode(node, range) {
1444
- if (!node) {
1445
- return new IntervalNode(range);
1446
- }
1447
- if (range.start < node.range.start) {
1448
- node.left = this.insertNode(node.left, range);
1449
- } else {
1450
- node.right = this.insertNode(node.right, range);
1451
- }
1452
- node.max = this.bigintMax(node.max, range.end);
1453
- return node;
1454
- }
1455
- bigintMax(...args) {
1456
- return args.reduce((max, current) => {
1457
- return current > max ? current : max;
1458
- }, args[0]);
1459
- }
1460
- findOverlappingRanges(node, range, result) {
1461
- if (!node) {
1462
- return;
1463
- }
1464
- if (this.doRangesOverlap(node.range, range)) {
1465
- result.push(node.range);
1466
- }
1467
- if (node.left && node.left.max >= range.start) {
1468
- this.findOverlappingRanges(node.left, range, result);
1469
- }
1470
- if (node.right && node.range.start <= range.end) {
1471
- this.findOverlappingRanges(node.right, range, result);
1472
- }
1473
- }
1474
- doRangesOverlap(a, b) {
1475
- return a.start < b.end && b.start < a.end;
1476
- }
1477
- removeNode(node, range) {
1478
- if (!node) {
1479
- return null;
1480
- }
1481
- if (this.areRangesEqual(node.range, range)) {
1482
- if (!node.left) {
1483
- return node.right;
1484
- }
1485
- if (!node.right) {
1486
- return node.left;
1487
- }
1488
- const successor = this.findMin(node.right);
1489
- node.range = successor.range;
1490
- node.right = this.removeNode(node.right, successor.range);
1491
- } else if (range.start < node.range.start) {
1492
- node.left = this.removeNode(node.left, range);
1493
- } else {
1494
- node.right = this.removeNode(node.right, range);
1495
- }
1496
- node.max = node.range.end;
1497
- if (node.left) {
1498
- node.max = this.bigintMax(node.max, node.left.max);
1499
- }
1500
- if (node.right) {
1501
- node.max = this.bigintMax(node.max, node.right.max);
1502
- }
1503
- return node;
1504
- }
1505
- findMin(node) {
1506
- let current = node;
1507
- while (current.left) {
1508
- current = current.left;
1509
- }
1510
- return current;
1511
- }
1512
- areRangesEqual(a, b) {
1513
- return a.start === b.start && a.end === b.end && a.pid === b.pid;
1514
- }
1515
- findLocksForProcessInNode(node, pid, result) {
1516
- if (!node) {
1517
- return;
1518
- }
1519
- if (node.range.pid === pid) {
1520
- result.push(node.range);
1521
- }
1522
- this.findLocksForProcessInNode(node.left, pid, result);
1523
- this.findLocksForProcessInNode(node.right, pid, result);
1524
- }
1525
- };
1526
2050
  export {
1527
- FileLock,
1528
- FileLockManagerForNode,
2051
+ FileLockManagerForPosix,
2052
+ FileLockManagerForWindows,
2053
+ bindUserSpace,
1529
2054
  createNodeFsMountHandler,
1530
2055
  getPHPLoaderModule,
1531
2056
  loadNodeRuntime,