Selaa lähdekoodia

fix(webworker): support file handle chmod

imccyu 5 päivää sitten
vanhempi
sitoutus
60a3d0fbc2

+ 9 - 2
apps/web/tests/preview-boot.e2e.ts

@@ -355,7 +355,12 @@ async function bootPreview(origin: string, browser: Browser): Promise<void> {
         if (!body.result.ok) throw new Error(`${endpoint} failed: ${body.result.error.message}`)
         return body.result.value
       }
-      const renamed = await remote<{ title: string }>('session/rename', {
+      // Keep the fixture title stable for later UI assertions; increasing seqs
+      // prove that the cold Session acquired its write lease and appended.
+      const firstRename = await remote<{ title: string; seq: number }>('session/rename', {
+        request: { sessionId: seededSessionId, title: seededSessionTitle },
+      })
+      const secondRename = await remote<{ title: string; seq: number }>('session/rename', {
         request: { sessionId: seededSessionId, title: seededSessionTitle },
       })
       const skills = await remote<{ skills: Array<{ name: string }> }>(
@@ -390,7 +395,8 @@ async function bootPreview(origin: string, browser: Browser): Promise<void> {
       await remote('credentials/unset', { ref: 'PREVIEW_TEST_SECRET' })
       await new Promise((resolve) => { setTimeout(resolve, 250) })
       return {
-        renamedTitle: renamed.title,
+        renamedTitle: secondRename.title,
+        renameAdvanced: secondRename.seq > firstRename.seq,
         skillCount: skills.skills.length,
         credentialConfigured: credentials.PREVIEW_TEST_SECRET?.configured,
       }
@@ -399,6 +405,7 @@ async function bootPreview(origin: string, browser: Browser): Promise<void> {
       seededSessionTitle: SHOWCASE_TITLE,
     })
     expect(exercised.renamedTitle).toBe(SHOWCASE_TITLE)
+    expect(exercised.renameAdvanced).toBe(true)
     expect(exercised.skillCount).toBeGreaterThan(0)
     expect(exercised.credentialConfigured).toBe(true)
 

+ 9 - 1
packages/experimental/webworker-runtime/src/node/builtin_modules/implemented/fs.ts

@@ -36,6 +36,9 @@ const encodingOf = (options: EncodingOption): BufferEncoding | undefined => {
   return options.encoding ?? undefined
 }
 
+const numericMode = (mode: number | string): number =>
+  typeof mode === 'string' ? Number.parseInt(mode, 8) : mode
+
 const bytesOf = (path: string): Uint8Array => vfs().readFileSync(path) as Uint8Array
 
 /** Share the VFS bytes rather than copying them. */
@@ -186,7 +189,7 @@ export function stat(
  * @param mode - new permission bits (`0o777` mask), numeric or Node's octal string form.
  */
 export function chmodSync(path: PathArg, mode: number | string): void {
-  vfs().chmodSync(asPath(path), typeof mode === 'string' ? Number.parseInt(mode, 8) : mode)
+  vfs().chmodSync(asPath(path), numericMode(mode))
 }
 
 /**
@@ -399,6 +402,7 @@ export interface FileHandle {
   writeFile(data: string | Uint8Array, encoding?: BufferEncoding): Promise<void>
   write(data: string | Uint8Array): Promise<{ bytesWritten: number }>
   read(buffer: Uint8Array, offset?: number, length?: number, position?: number | null): Promise<{ bytesRead: number; buffer: Uint8Array }>
+  chmod(mode: number | string): Promise<void>
   stat(options?: VfsStatOptions): Promise<VfsStats | VfsBigIntStats>
   truncate(length?: number): Promise<void>
   sync(): Promise<void>
@@ -441,6 +445,10 @@ export function openHandleSync(path: PathArg, flags = 'r', mode?: number): FileH
       bytesRead: readSync(fd, buffer, offset, length, position),
       buffer,
     }),
+    chmod: async (mode: number | string) => {
+      if (directory) chmodSync(target, mode)
+      else descriptor('fchmod').file.chmod(numericMode(mode))
+    },
     stat: async (options?: VfsStatOptions) => directory
       ? statSync(target, options)
       : options?.bigint === true

+ 15 - 7
packages/experimental/webworker-runtime/src/storage/memory.ts

@@ -426,6 +426,16 @@ export class MemoryVfs implements Vfs {
     this.replaceFile(node, resize(node.bytes, length))
   }
 
+  /** Change one file identity's permission bits and notify every linked path. */
+  private chmodFile(node: FileNode, mode: number): void {
+    node.mode = mode & 0o777
+    if (typeof node.paths === 'string') {
+      this.publish({ kind: 'chmod', path: node.paths, mode: node.mode })
+    } else if (node.paths !== undefined) {
+      for (const path of node.paths) this.publish({ kind: 'chmod', path, mode: node.mode })
+    }
+  }
+
   /** @returns Plain stats for an open file, including after its last name is removed. */
   private fileStats(node: FileNode): VfsStats {
     return statsOf(node.bytes.length, node.mtimeMs, false, this.identityOfFile(node), node.mode)
@@ -659,6 +669,7 @@ export class MemoryVfs implements Vfs {
       truncate: async (length = 0): Promise<void> => {
         current('ftruncate').truncate(length)
       },
+      chmod: async (mode: number): Promise<void> => { current('fchmod').chmod(mode) },
       stat: async (options?: VfsStatOptions): Promise<VfsStats | VfsBigIntStats> => options?.bigint === true
         ? current('fstat').statBigInt()
         : current('fstat').stat(),
@@ -705,6 +716,7 @@ export class MemoryVfs implements Vfs {
         if (!access.writable) fail('EINVAL', 'ftruncate', target)
         this.truncateFile(node, length)
       },
+      chmod: mode => { this.chmodFile(node, mode) },
       stat: () => this.fileStats(node),
       statBigInt: () => this.fileBigIntStats(node),
     }
@@ -717,8 +729,9 @@ export class MemoryVfs implements Vfs {
    * @param target - Normalized path the handle was opened on.
    * @returns Metadata plus the no-op durability and release calls.
    */
-  private handleTail(target: string): Pick<VfsFileHandle, 'stat' | 'sync' | 'datasync' | 'close'> {
+  private handleTail(target: string): Pick<VfsFileHandle, 'chmod' | 'stat' | 'sync' | 'datasync' | 'close'> {
     return {
+      chmod: async (mode: number): Promise<void> => { this.chmodSync(target, mode) },
       stat: async (options?: VfsStatOptions): Promise<VfsStats | VfsBigIntStats> => this.statSync(target, options),
       sync: async (): Promise<void> => { await this.flush() },
       datasync: async (): Promise<void> => { await this.flush() },
@@ -851,12 +864,7 @@ export class MemoryVfs implements Vfs {
     const target = this.key(path)
     const node = this.files.get(target)
     if (node !== undefined) {
-      node.mode = mode & 0o777
-      if (typeof node.paths === 'string') {
-        this.publish({ kind: 'chmod', path: node.paths, mode: node.mode })
-      } else if (node.paths !== undefined) {
-        for (const path of node.paths) this.publish({ kind: 'chmod', path, mode: node.mode })
-      }
+      this.chmodFile(node, mode)
       return
     }
     if (this.directories.has(target)) {

+ 3 - 0
packages/experimental/webworker-runtime/src/storage/types.ts

@@ -119,6 +119,7 @@ export interface VfsFileHandle {
   writeFile(data: string | Uint8Array): Promise<void>
   readFile(options?: VfsReadOptions): Promise<string | Uint8Array>
   truncate(length?: number): Promise<void>
+  chmod(mode: number): Promise<void>
   stat(options?: VfsStatOptions): Promise<VfsStats | VfsBigIntStats>
   sync(): Promise<void>
   datasync(): Promise<void>
@@ -152,6 +153,8 @@ export interface VfsOpenFile {
    * @param length - Target byte length.
    */
   truncate(length: number): void
+  /** Change permission bits on the opened file identity. */
+  chmod(mode: number): void
   /**
    * Read metadata from the opened file identity.
    * @returns Current file metadata, including after rename or unlink.

+ 4 - 0
packages/experimental/webworker-runtime/tests/node/fs.spec.ts

@@ -212,6 +212,10 @@ fs.renameSync('/dsh/secrets.tmp', '/dsh/secrets.yaml')
 check('a wx write with mode 600 stats as 600 after rename', plainMode('/dsh/secrets.yaml'), 0o600)
 fs.writeFileSync('/dsh/secrets.yaml', 'k: w\n')
 check('a rewrite keeps the creation bits', plainMode('/dsh/secrets.yaml'), 0o600)
+const chmodHandle = await fsp.open('/dsh/secrets.yaml', 'r+')
+await chmodHandle.chmod(0o640)
+await chmodHandle.close()
+check('FileHandle.chmod updates the opened file', plainMode('/dsh/secrets.yaml'), 0o640)
 fs.chmodSync('/dsh/secrets.yaml', 0o640)
 check('chmod reads back exactly what was set', plainMode('/dsh/secrets.yaml'), 0o640)
 await fsp.chmod('/dsh/secrets.yaml', 0o600)

+ 3 - 1
packages/experimental/webworker-runtime/tests/storage/memory-vfs.spec.ts

@@ -206,11 +206,13 @@ describe('mutation publication', () => {
   it('reports the path identity through a BigInt file handle stat', async () => {
     const vfs = new MemoryVfs()
     vfs.seed('/dsh/session.lock', '')
-    const handle = vfs.open('/dsh/session.lock', 'r')
+    const handle = vfs.open('/dsh/session.lock', 'w')
     const held = await handle.stat({ bigint: true }) as VfsBigIntStats
     const current = vfs.statSync('/dsh/session.lock', { bigint: true }) as VfsBigIntStats
 
     expect([held.dev, held.ino]).toEqual([current.dev, current.ino])
+    await handle.chmod(0o600)
+    expect((vfs.statSync('/dsh/session.lock') as VfsStats).mode & 0o777).toBe(0o600)
     await handle.close()
   })