From 93d83a84dbe1f6ecf93b596f88bc85ba378fa986 Mon Sep 17 00:00:00 2001 From: Tilman Roeder Date: Tue, 24 Aug 2021 14:21:31 +0100 Subject: [PATCH] feat(unstable): Add file locking APIs (#11746) This commit adds following unstable APIs: - Deno.flock() - Deno.flockSync() - Deno.funlock() - Deno.funlockSync() --- Cargo.lock | 12 ++++ cli/dts/lib.deno.ns.d.ts | 26 ++++++++ cli/tests/unit/flock_test.ts | 102 ++++++++++++++++++++++++++++++ runtime/Cargo.toml | 1 + runtime/js/30_fs.js | 20 ++++++ runtime/js/90_deno_ns.js | 4 ++ runtime/ops/fs.rs | 118 +++++++++++++++++++++++++++++++++++ 7 files changed, 283 insertions(+) create mode 100644 cli/tests/unit/flock_test.ts diff --git a/Cargo.lock b/Cargo.lock index 81e8ef78eb..f3189660b8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -828,6 +828,7 @@ dependencies = [ "dlopen", "encoding_rs", "filetime", + "fs3", "fwdansi", "http", "hyper", @@ -1294,6 +1295,17 @@ dependencies = [ "syn 1.0.65", ] +[[package]] +name = "fs3" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb17cf6ed704f72485332f6ab65257460c4f9f3083934cf402bf9f5b3b600a90" +dependencies = [ + "libc", + "rustc_version 0.2.3", + "winapi 0.3.9", +] + [[package]] name = "fsevent-sys" version = "4.0.0" diff --git a/cli/dts/lib.deno.ns.d.ts b/cli/dts/lib.deno.ns.d.ts index 420a08da4f..996aff3633 100644 --- a/cli/dts/lib.deno.ns.d.ts +++ b/cli/dts/lib.deno.ns.d.ts @@ -787,6 +787,32 @@ declare namespace Deno { */ export function fdatasync(rid: number): Promise; + /** **UNSTABLE**: New API should be tested first. + * + * Acquire an advisory file-system lock for the provided file. `exclusive` + * defaults to `false`. + */ + export function flock(rid: number, exclusive?: boolean): Promise; + + /** **UNSTABLE**: New API should be tested first. + * + * Acquire an advisory file-system lock for the provided file. `exclusive` + * defaults to `false`. + */ + export function flockSync(rid: number, exclusive?: boolean): void; + + /** **UNSTABLE**: New API should be tested first. + * + * Release an advisory file-system lock for the provided file. + */ + export function funlock(rid: number): Promise; + + /** **UNSTABLE**: New API should be tested first. + * + * Release an advisory file-system lock for the provided file. + */ + export function funlockSync(rid: number): void; + /** Close the given resource ID (rid) which has been previously opened, such * as via opening or creating a file. Closing a file when you are finished * with it is important to avoid leaking resources. diff --git a/cli/tests/unit/flock_test.ts b/cli/tests/unit/flock_test.ts new file mode 100644 index 0000000000..13d09bcf56 --- /dev/null +++ b/cli/tests/unit/flock_test.ts @@ -0,0 +1,102 @@ +// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license. +import { assertEquals, unitTest } from "./test_util.ts"; + +unitTest( + { perms: { read: true, run: true, hrtime: true } }, + async function flockFileSync() { + const path = "cli/tests/testdata/fixture.json"; + const script = (exclusive: boolean, wait: number) => ` + const { rid } = Deno.openSync("${path}"); + Deno.flockSync(rid, ${exclusive ? "true" : "false"}); + await new Promise(res => setTimeout(res, ${wait})); + Deno.funlockSync(rid); + `; + const run = (e: boolean, w: number) => + Deno.run({ cmd: [Deno.execPath(), "eval", "--unstable", script(e, w)] }); + const firstBlocksSecond = async ( + first: boolean, + second: boolean, + ): Promise => { + const firstPs = run(first, 1000); + await new Promise((res) => setTimeout(res, 250)); + const start = performance.now(); + const secondPs = run(second, 0); + await secondPs.status(); + const didBlock = (performance.now() - start) > 500; + firstPs.close(); + secondPs.close(); + return didBlock; + }; + + assertEquals( + await firstBlocksSecond(true, false), + true, + "exclusive blocks shared", + ); + assertEquals( + await firstBlocksSecond(false, true), + true, + "shared blocks exclusive", + ); + assertEquals( + await firstBlocksSecond(true, true), + true, + "exclusive blocks exclusive", + ); + assertEquals( + await firstBlocksSecond(false, false), + false, + "shared does not block shared", + ); + }, +); + +unitTest( + { perms: { read: true, run: true, hrtime: true } }, + async function flockFileAsync() { + const path = "cli/tests/testdata/fixture.json"; + const script = (exclusive: boolean, wait: number) => ` + const { rid } = await Deno.open("${path}"); + await Deno.flock(rid, ${exclusive ? "true" : "false"}); + await new Promise(res => setTimeout(res, ${wait})); + await Deno.funlock(rid); + `; + const run = (e: boolean, w: number) => + Deno.run({ cmd: [Deno.execPath(), "eval", "--unstable", script(e, w)] }); + const firstBlocksSecond = async ( + first: boolean, + second: boolean, + ): Promise => { + const firstPs = run(first, 1000); + await new Promise((res) => setTimeout(res, 250)); + const start = performance.now(); + const secondPs = run(second, 0); + await secondPs.status(); + const didBlock = (performance.now() - start) > 500; + firstPs.close(); + secondPs.close(); + return didBlock; + }; + + assertEquals( + await firstBlocksSecond(true, false), + true, + "exclusive blocks shared", + ); + assertEquals( + await firstBlocksSecond(false, true), + true, + "shared blocks exclusive", + ); + assertEquals( + await firstBlocksSecond(true, true), + true, + "exclusive blocks exclusive", + ); + assertEquals( + await firstBlocksSecond(false, false), + false, + "shared does not block shared", + ); + }, +); diff --git a/runtime/Cargo.toml b/runtime/Cargo.toml index 491516dbb4..5205094504 100644 --- a/runtime/Cargo.toml +++ b/runtime/Cargo.toml @@ -65,6 +65,7 @@ atty = "0.2.14" dlopen = "0.1.8" encoding_rs = "0.8.28" filetime = "0.2.14" +fs3 = "0.5.0" http = "0.2.4" hyper = { version = "0.14.10", features = ["server", "stream", "http1", "http2", "runtime"] } # TODO(lucacasonato): unlock when https://github.com/tkaitchuck/aHash/issues/95 is resolved diff --git a/runtime/js/30_fs.js b/runtime/js/30_fs.js index e45cda3215..feb9f8f54d 100644 --- a/runtime/js/30_fs.js +++ b/runtime/js/30_fs.js @@ -385,6 +385,22 @@ await core.opAsync("op_fsync_async", rid); } + function flockSync(rid, exclusive) { + core.opSync("op_flock_sync", rid, exclusive === true); + } + + async function flock(rid, exclusive) { + await core.opAsync("op_flock_async", rid, exclusive === true); + } + + function funlockSync(rid) { + core.opSync("op_funlock_sync", rid); + } + + async function funlock(rid) { + await core.opAsync("op_funlock_async", rid); + } + window.__bootstrap.fs = { cwd, chdir, @@ -433,5 +449,9 @@ fdatasyncSync, fsync, fsyncSync, + flock, + flockSync, + funlock, + funlockSync, }; })(this); diff --git a/runtime/js/90_deno_ns.js b/runtime/js/90_deno_ns.js index 796361d7ac..71c8bd0f09 100644 --- a/runtime/js/90_deno_ns.js +++ b/runtime/js/90_deno_ns.js @@ -136,5 +136,9 @@ createHttpClient: __bootstrap.fetch.createHttpClient, http: __bootstrap.http, dlopen: __bootstrap.ffi.dlopen, + flock: __bootstrap.fs.flock, + flockSync: __bootstrap.fs.flockSync, + funlock: __bootstrap.fs.funlock, + funlockSync: __bootstrap.fs.funlockSync, }; })(this); diff --git a/runtime/ops/fs.rs b/runtime/ops/fs.rs index 419e41718f..819f3f3ace 100644 --- a/runtime/ops/fs.rs +++ b/runtime/ops/fs.rs @@ -48,6 +48,10 @@ pub fn init() -> Extension { ("op_fsync_async", op_async(op_fsync_async)), ("op_fstat_sync", op_sync(op_fstat_sync)), ("op_fstat_async", op_async(op_fstat_async)), + ("op_flock_sync", op_sync(op_flock_sync)), + ("op_flock_async", op_async(op_flock_async)), + ("op_funlock_sync", op_sync(op_funlock_sync)), + ("op_funlock_async", op_async(op_funlock_async)), ("op_umask", op_sync(op_umask)), ("op_chdir", op_sync(op_chdir)), ("op_mkdir_sync", op_sync(op_mkdir_sync)), @@ -346,6 +350,120 @@ async fn op_fstat_async( Ok(get_stat(metadata)) } +fn op_flock_sync( + state: &mut OpState, + rid: ResourceId, + exclusive: bool, +) -> Result<(), AnyError> { + use fs3::FileExt; + super::check_unstable(state, "Deno.flockSync"); + + StdFileResource::with(state, rid, |r| match r { + Ok(std_file) => { + if exclusive { + std_file.lock_exclusive()?; + } else { + std_file.lock_shared()?; + } + Ok(()) + } + Err(_) => Err(type_error("cannot lock this type of resource".to_string())), + }) +} + +async fn op_flock_async( + state: Rc>, + rid: ResourceId, + exclusive: bool, +) -> Result<(), AnyError> { + use fs3::FileExt; + super::check_unstable2(&state, "Deno.flock"); + + let resource = state + .borrow_mut() + .resource_table + .get::(rid)?; + + if resource.fs_file.is_none() { + return Err(bad_resource_id()); + } + + let mut fs_file = RcRef::map(&resource, |r| r.fs_file.as_ref().unwrap()) + .borrow_mut() + .await; + + let std_file = (*fs_file) + .0 + .as_mut() + .unwrap() + .try_clone() + .await? + .into_std() + .await; + tokio::task::spawn_blocking(move || -> Result<(), AnyError> { + if exclusive { + std_file.lock_exclusive()?; + } else { + std_file.lock_shared()?; + } + Ok(()) + }) + .await? +} + +fn op_funlock_sync( + state: &mut OpState, + rid: ResourceId, + _: (), +) -> Result<(), AnyError> { + use fs3::FileExt; + super::check_unstable(state, "Deno.funlockSync"); + + StdFileResource::with(state, rid, |r| match r { + Ok(std_file) => { + std_file.unlock()?; + Ok(()) + } + Err(_) => Err(type_error("cannot lock this type of resource".to_string())), + }) +} + +async fn op_funlock_async( + state: Rc>, + rid: ResourceId, + _: (), +) -> Result<(), AnyError> { + use fs3::FileExt; + super::check_unstable2(&state, "Deno.funlock"); + + let resource = state + .borrow_mut() + .resource_table + .get::(rid)?; + + if resource.fs_file.is_none() { + return Err(bad_resource_id()); + } + + let mut fs_file = RcRef::map(&resource, |r| r.fs_file.as_ref().unwrap()) + .borrow_mut() + .await; + + let std_file = (*fs_file) + .0 + .as_mut() + .unwrap() + .try_clone() + .await? + .into_std() + .await; + tokio::task::spawn_blocking(move || -> Result<(), AnyError> { + std_file.unlock()?; + Ok(()) + }) + .await? +} + fn op_umask( state: &mut OpState, mask: Option,