// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. import { assertEquals } from "./test_util.ts"; Deno.test( { permissions: { read: true, run: true, hrtime: true } }, async function flockFileSync() { await runFlockTests({ sync: true }); }, ); Deno.test( { permissions: { read: true, run: true, hrtime: true } }, async function flockFileAsync() { await runFlockTests({ sync: false }); }, ); async function runFlockTests(opts: { sync: boolean }) { assertEquals( await checkFirstBlocksSecond({ firstExclusive: true, secondExclusive: false, sync: opts.sync, }), true, "exclusive blocks shared", ); assertEquals( await checkFirstBlocksSecond({ firstExclusive: false, secondExclusive: true, sync: opts.sync, }), true, "shared blocks exclusive", ); assertEquals( await checkFirstBlocksSecond({ firstExclusive: true, secondExclusive: true, sync: opts.sync, }), true, "exclusive blocks exclusive", ); assertEquals( await checkFirstBlocksSecond({ firstExclusive: false, secondExclusive: false, sync: opts.sync, // need to wait for both to enter the lock to prevent the case where the // first process enters and exits the lock before the second even enters waitBothEnteredLock: true, }), false, "shared does not block shared", ); } async function checkFirstBlocksSecond(opts: { firstExclusive: boolean; secondExclusive: boolean; sync: boolean; waitBothEnteredLock?: boolean; }) { const firstProcess = runFlockTestProcess({ exclusive: opts.firstExclusive, sync: opts.sync, }); const secondProcess = runFlockTestProcess({ exclusive: opts.secondExclusive, sync: opts.sync, }); try { const sleep = (time: number) => new Promise((r) => setTimeout(r, time)); await Promise.all([ firstProcess.waitStartup(), secondProcess.waitStartup(), ]); await firstProcess.enterLock(); await firstProcess.waitEnterLock(); await secondProcess.enterLock(); await sleep(100); if (!opts.waitBothEnteredLock) { await firstProcess.exitLock(); } await secondProcess.waitEnterLock(); if (opts.waitBothEnteredLock) { await firstProcess.exitLock(); } await secondProcess.exitLock(); // collect the final output const firstPsTimes = await firstProcess.getTimes(); const secondPsTimes = await secondProcess.getTimes(); return firstPsTimes.exitTime < secondPsTimes.enterTime; } finally { await firstProcess.close(); await secondProcess.close(); } } function runFlockTestProcess(opts: { exclusive: boolean; sync: boolean }) { const path = "tests/testdata/assets/lock_target.txt"; const scriptText = ` const { rid } = Deno.openSync("${path}"); // ready signal Deno.stdout.writeSync(new Uint8Array(1)); // wait for enter lock signal Deno.stdin.readSync(new Uint8Array(1)); // entering signal Deno.stdout.writeSync(new Uint8Array(1)); // lock and record the entry time ${ opts.sync ? `Deno.flockSync(rid, ${opts.exclusive ? "true" : "false"});` : `await Deno.flock(rid, ${opts.exclusive ? "true" : "false"});` } const enterTime = new Date().getTime(); // entered signal Deno.stdout.writeSync(new Uint8Array(1)); // wait for exit lock signal Deno.stdin.readSync(new Uint8Array(1)); // record the exit time and wait a little bit before releasing // the lock so that the enter time of the next process doesn't // occur at the same time as this exit time const exitTime = new Date().getTime(); await new Promise(resolve => setTimeout(resolve, 100)); // release the lock ${opts.sync ? "Deno.funlockSync(rid);" : "await Deno.funlock(rid);"} // exited signal Deno.stdout.writeSync(new Uint8Array(1)); // output the enter and exit time console.log(JSON.stringify({ enterTime, exitTime })); `; const process = new Deno.Command(Deno.execPath(), { args: ["eval", "--unstable", scriptText], stdin: "piped", stdout: "piped", stderr: "null", }).spawn(); const waitSignal = async () => { const reader = process.stdout.getReader({ mode: "byob" }); await reader.read(new Uint8Array(1)); reader.releaseLock(); }; const signal = async () => { const writer = process.stdin.getWriter(); await writer.write(new Uint8Array(1)); writer.releaseLock(); }; return { async waitStartup() { await waitSignal(); }, async enterLock() { await signal(); await waitSignal(); // entering signal }, async waitEnterLock() { await waitSignal(); }, async exitLock() { await signal(); await waitSignal(); }, getTimes: async () => { const { stdout } = await process.output(); const text = new TextDecoder().decode(stdout); return JSON.parse(text) as { enterTime: number; exitTime: number; }; }, close: async () => { await process.status; await process.stdin.close(); }, }; }