diff --git a/Cargo.lock b/Cargo.lock index 9530a5425a8..1ae04609126 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10,9 +10,9 @@ checksum = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3" [[package]] name = "addr2line" -version = "0.24.1" +version = "0.24.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5fb1d8e4442bd405fdfd1dacb42792696b0cf9cb15882e5d097b742a676d375" +checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" dependencies = [ "gimli", ] @@ -109,9 +109,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.87" +version = "1.0.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10f00e1f6e58a40e807377c75c6a7f97bf9044fab57816f2414e6f5f4499d7b8" +checksum = "c042108f3ed77fd83760a5fd79b53be043192bb3b9dba91d8c574c0ada7850c8" dependencies = [ "backtrace", ] @@ -127,9 +127,9 @@ dependencies = [ [[package]] name = "arrayref" -version = "0.3.8" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d151e35f61089500b617991b791fc8bfd237ae50cd5950803758a179b41e67a" +checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" [[package]] name = "arrayvec" @@ -155,9 +155,9 @@ checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" [[package]] name = "autocfg" -version = "1.3.0" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" [[package]] name = "backtrace" @@ -250,9 +250,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.7.1" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8318a53db07bb3f8dca91a600466bdb3f2eaadeedfdbcf02e1accbad9271ba50" +checksum = "9ac0150caa2ae65ca5bd83f25c7de183dea78d4d366469f148435e2acfbad0da" [[package]] name = "bzip2" @@ -277,9 +277,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.1.18" +version = "1.1.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b62ac837cdb5cb22e10a256099b4fc502b1dfe560cb282963a974d7abd80e476" +checksum = "c2e7962b54006dcfcc61cb72735f4d89bb97061dd6a7ed882ec6b8ee53714c6f" dependencies = [ "jobserver", "libc", @@ -331,9 +331,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.17" +version = "4.5.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e5a21b8495e732f1b3c364c9949b201ca7bae518c502c80256c96ad79eaf6ac" +checksum = "b97f376d85a664d5837dbae44bf546e6477a679ff6610010f17276f686d867e8" dependencies = [ "clap_builder", "clap_derive", @@ -341,9 +341,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.17" +version = "4.5.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8cf2dd12af7a047ad9d6da2b6b249759a22a7abc0f474c1dae1777afa4b21a73" +checksum = "19bc80abd44e4bed93ca373a0704ccbd1b710dc5749406201bb018272808dc54" dependencies = [ "anstream", "anstyle", @@ -353,14 +353,14 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.13" +version = "4.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "501d359d5f3dcaf6ecdeee48833ae73ec6e42723a1e52419c79abf9507eec0a0" +checksum = "4ac6a0c7b1a9e9a5186361f67dfa1b88213572f427fb9ab038efb2bd8c582dab" dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.85", ] [[package]] @@ -519,7 +519,7 @@ checksum = "67e77553c4162a157adbf834ebae5b415acbecbeafc7a74b0e886657506a7611" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.85", ] [[package]] @@ -562,7 +562,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.85", ] [[package]] @@ -654,6 +654,16 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "file-lock" +version = "2.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "040b48f80a749da50292d0f47a1e2d5bf1d772f52836c07f64bfccc62ba6e664" +dependencies = [ + "cc", + "libc", +] + [[package]] name = "filetime" version = "0.2.25" @@ -668,9 +678,9 @@ dependencies = [ [[package]] name = "flate2" -version = "1.0.33" +version = "1.0.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "324a1be68054ef05ad64b861cc9eaf1d623d2d8cb25b4bf2cb9cdd902b4bf253" +checksum = "a1b589b4dc103969ad3cf85c950899926ec64300a1a46d76c03a6072957036f0" dependencies = [ "crc32fast", "miniz_oxide", @@ -699,9 +709,9 @@ checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" [[package]] name = "futures" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" dependencies = [ "futures-channel", "futures-core", @@ -714,9 +724,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" dependencies = [ "futures-core", "futures-sink", @@ -724,15 +734,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" [[package]] name = "futures-executor" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" dependencies = [ "futures-core", "futures-task", @@ -741,38 +751,38 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" [[package]] name = "futures-macro" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.85", ] [[package]] name = "futures-sink" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" [[package]] name = "futures-task" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" [[package]] name = "futures-util" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" dependencies = [ "futures-channel", "futures-core", @@ -811,9 +821,9 @@ dependencies = [ [[package]] name = "gimli" -version = "0.31.0" +version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32085ea23f3234fc7846555e85283ba4de91e21016dc0455a16286d87a292d64" +checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" [[package]] name = "glob" @@ -874,9 +884,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.14.5" +version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +checksum = "1e087f84d4f86bf4b218b927129862374b72199ae7d8657835f1e89000eea4fb" [[package]] name = "heck" @@ -963,9 +973,9 @@ dependencies = [ [[package]] name = "httparse" -version = "1.9.4" +version = "1.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fcc0b4a115bf80b728eb8ea024ad5bd707b615bfed49e0665b6e0f86fd082d9" +checksum = "7d71d3574edd2771538b901e6549113b4006ece66150fb69c0fb6d9a2adae946" [[package]] name = "httpdate" @@ -981,9 +991,9 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" [[package]] name = "hyper" -version = "0.14.30" +version = "0.14.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a152ddd61dfaec7273fe8419ab357f33aee0d914c5f4efbf0d96fa749eea5ec9" +checksum = "8c08302e8fa335b151b788c775ff56e7a03ae64ff85c548ee820fecb70356e85" dependencies = [ "bytes", "futures-channel", @@ -1005,9 +1015,9 @@ dependencies = [ [[package]] name = "hyper" -version = "1.4.1" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50dfd22e0e76d0f662d429a5f80fcaf3855009297eab6a0a9f8543834744ba05" +checksum = "bbbff0a806a4728c99295b254c8838933b5b082d75e3cb70c8dab21fdfbcfa9a" dependencies = [ "bytes", "futures-channel", @@ -1031,7 +1041,7 @@ checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590" dependencies = [ "futures-util", "http 0.2.12", - "hyper 0.14.30", + "hyper 0.14.31", "rustls", "tokio", "tokio-rustls", @@ -1039,24 +1049,24 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.7" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cde7055719c54e36e95e8719f95883f22072a48ede39db7fc17a4e1d5281e9b9" +checksum = "41296eb09f183ac68eec06e03cdbea2e759633d4067b2f6552fc2e009bcad08b" dependencies = [ "bytes", "futures-util", "http 1.1.0", "http-body 1.0.1", - "hyper 1.4.1", + "hyper 1.5.0", "pin-project-lite", "tokio", ] [[package]] name = "iana-time-zone" -version = "0.1.60" +version = "0.1.61" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141" +checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220" dependencies = [ "android_system_properties", "core-foundation-sys", @@ -1097,9 +1107,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.5.0" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68b900aa2f7301e21c36462b170ee99994de34dff39a4a6a528e80e7376d07e5" +checksum = "707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da" dependencies = [ "equivalent", "hashbrown", @@ -1132,9 +1142,9 @@ dependencies = [ [[package]] name = "ipnet" -version = "2.10.0" +version = "2.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "187674a687eed5fe42285b40c6291f9a01517d415fad1c3cbc6a9f778af7fcd4" +checksum = "ddc24109865250148c2e0f3d25d4f0f479571723792d3802153c60922a4fb708" [[package]] name = "is_terminal_polyfill" @@ -1218,9 +1228,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.70" +version = "0.3.72" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1868808506b929d7b0cfa8f75951347aa71bb21144b7791bae35d9bccfcfe37a" +checksum = "6a88f1bda2bd75b0452a14784937d796722fdebfe50df998aeb3f0b7603019a9" dependencies = [ "wasm-bindgen", ] @@ -1248,9 +1258,9 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" -version = "0.2.158" +version = "0.2.161" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8adc4bb1803a324070e64a98ae98f38934d91957a99cfb3a43dcbc01bc56439" +checksum = "8e9489c2807c139ffd9c1794f4af0ebe86a828db53ecdc7fea2111d0fed085d1" [[package]] name = "libloading" @@ -1385,7 +1395,7 @@ dependencies = [ "http 1.1.0", "http-body 1.0.1", "http-body-util", - "hyper 1.4.1", + "hyper 1.5.0", "hyper-util", "log", "rand", @@ -1430,7 +1440,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c6813fde79b646e47e7ad75f480aa80ef76a5d9599e2717407961531169ee38b" dependencies = [ "quote", - "syn 2.0.77", + "syn 2.0.85", "syn-mid", ] @@ -1502,18 +1512,18 @@ dependencies = [ [[package]] name = "object" -version = "0.36.4" +version = "0.36.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "084f1a5821ac4c651660a94a7153d27ac9d8a53736203f58b31945ded098070a" +checksum = "aedf0a2d09c573ed1d8d85b30c119153926a2b36dce0ab28322c09a117a4683e" dependencies = [ "memchr", ] [[package]] name = "once_cell" -version = "1.19.0" +version = "1.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" +checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" [[package]] name = "option-ext" @@ -1592,9 +1602,9 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "pkg-config" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" +checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" [[package]] name = "powerfmt" @@ -1613,9 +1623,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.86" +version = "1.0.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" +checksum = "f139b0662de085916d1fb67d2b4169d1addddda1919e696f3252b740b629986e" dependencies = [ "unicode-ident", ] @@ -1677,9 +1687,9 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.5.3" +version = "0.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a908a6e00f1fdd0dfd9c0eb08ce85126f6d8bbda50017e74bc4a4b7d4a926a4" +checksum = "9b6dfecf2c74bce2466cabf93f6664d6998a69eb21e39f4207930065b27b771f" dependencies = [ "bitflags 2.6.0", ] @@ -1697,9 +1707,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.10.6" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4219d74c6b67a3654a9fbebc4b419e22126d13d2f3c4a07ee0cb61ff79a79619" +checksum = "38200e5ee88914975b69f657f0801b6f6dccafd44fd9326302a4aaeecfacb1d8" dependencies = [ "aho-corasick", "memchr", @@ -1709,9 +1719,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.7" +version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df" +checksum = "368758f23274712b504848e9d5a6f010445cc8b87a7cdb4d7cbee666c1288da3" dependencies = [ "aho-corasick", "memchr", @@ -1720,9 +1730,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.4" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" [[package]] name = "reqwest" @@ -1740,7 +1750,7 @@ dependencies = [ "h2 0.3.26", "http 0.2.12", "http-body 0.4.6", - "hyper 0.14.30", + "hyper 0.14.31", "hyper-rustls", "ipnet", "js-sys", @@ -1818,9 +1828,9 @@ checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" [[package]] name = "rustix" -version = "0.38.36" +version = "0.38.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f55e80d50763938498dd5ebb18647174e0c76dc38c5505294bb224624f30f36" +checksum = "8acb788b847c24f28525660c4d7758620a7210875711f79e7f663cc152726811" dependencies = [ "bitflags 2.6.0", "errno", @@ -1898,7 +1908,7 @@ dependencies = [ "proc-macro2", "quote", "serde_derive_internals", - "syn 2.0.77", + "syn 2.0.85", ] [[package]] @@ -1931,9 +1941,9 @@ checksum = "cd0b0ec5f1c1ca621c432a25813d8d60c88abe6d3e08a3eb9cf37d97a0fe3d73" [[package]] name = "serde" -version = "1.0.210" +version = "1.0.213" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a" +checksum = "3ea7893ff5e2466df8d720bb615088341b295f849602c6956047f8f80f0e9bc1" dependencies = [ "serde_derive", ] @@ -1952,13 +1962,13 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.210" +version = "1.0.213" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f" +checksum = "7e85ad2009c50b58e87caa8cd6dac16bdf511bbfb7af6c33df902396aa480fa5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.85", ] [[package]] @@ -1969,14 +1979,14 @@ checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.85", ] [[package]] name = "serde_json" -version = "1.0.128" +version = "1.0.132" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ff5456707a1de34e7e37f2a6fd3d3f808c318259cbd01ab6377795054b483d8" +checksum = "d726bfaff4b320266d395898905d0eba0345aae23b54aee3a737e260fd46db03" dependencies = [ "itoa", "memchr", @@ -1996,9 +2006,9 @@ dependencies = [ [[package]] name = "serde_spanned" -version = "0.6.7" +version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb5b1b31579f3811bf615c144393417496f152e12ac8b7663bf664f4a815306d" +checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" dependencies = [ "serde", ] @@ -2162,9 +2172,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.77" +version = "2.0.85" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f35bcdf61fd8e7be6caf75f429fdca8beb3ed76584befb503b1569faee373ed" +checksum = "5023162dfcd14ef8f32034d8bcd4cc5ddc61ef7a247c024a33e24e1f24d21b56" dependencies = [ "proc-macro2", "quote", @@ -2179,7 +2189,7 @@ checksum = "b5dc35bb08dd1ca3dfb09dce91fd2d13294d6711c88897d9a9d60acf39bce049" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.85", ] [[package]] @@ -2211,9 +2221,9 @@ dependencies = [ [[package]] name = "tar" -version = "0.4.41" +version = "0.4.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb797dad5fb5b76fcf519e702f4a589483b5ef06567f160c392832c1f5e44909" +checksum = "4ff6c40d3aedb5e06b57c6f669ad17ab063dd1e63d977c6a88e7f4dfa4f04020" dependencies = [ "filetime", "libc", @@ -2222,9 +2232,9 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.12.0" +version = "3.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04cbcdd0c794ebb0d4cf35e88edd2f7d2c4c3e9a5a6dab322839b321c6a87a64" +checksum = "f0f2c9fc62d0beef6951ccffd757e241266a2c833136efbe35af6cd2567dca5b" dependencies = [ "cfg-if", "fastrand", @@ -2244,22 +2254,22 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.63" +version = "1.0.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0342370b38b6a11b6cc11d6a805569958d54cfa061a29969c3b5ce2ea405724" +checksum = "5d11abd9594d9b38965ef50805c5e469ca9cc6f197f883f717e0269a3057b3d5" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.63" +version = "1.0.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261" +checksum = "ae71770322cbd277e69d762a16c444af02aa0575ac0d174f0b9562d3b37f8602" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.85", ] [[package]] @@ -2312,7 +2322,7 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tmc-langs" -version = "0.36.3" +version = "0.36.4" dependencies = [ "base64 0.22.1", "blake3", @@ -2353,7 +2363,7 @@ dependencies = [ [[package]] name = "tmc-langs-cli" -version = "0.36.3" +version = "0.36.4" dependencies = [ "anyhow", "base64 0.22.1", @@ -2380,7 +2390,7 @@ dependencies = [ [[package]] name = "tmc-langs-csharp" -version = "0.36.3" +version = "0.36.4" dependencies = [ "dirs", "log", @@ -2398,7 +2408,7 @@ dependencies = [ [[package]] name = "tmc-langs-framework" -version = "0.36.3" +version = "0.36.4" dependencies = [ "fd-lock", "isolang", @@ -2423,7 +2433,7 @@ dependencies = [ [[package]] name = "tmc-langs-java" -version = "0.36.3" +version = "0.36.4" dependencies = [ "dirs", "flate2", @@ -2444,7 +2454,7 @@ dependencies = [ [[package]] name = "tmc-langs-make" -version = "0.36.3" +version = "0.36.4" dependencies = [ "log", "once_cell", @@ -2463,7 +2473,7 @@ dependencies = [ [[package]] name = "tmc-langs-node" -version = "0.36.3" +version = "0.36.4" dependencies = [ "base64 0.22.1", "env_logger", @@ -2481,7 +2491,7 @@ dependencies = [ [[package]] name = "tmc-langs-notests" -version = "0.36.3" +version = "0.36.4" dependencies = [ "log", "simple_logger", @@ -2493,7 +2503,7 @@ dependencies = [ [[package]] name = "tmc-langs-plugins" -version = "0.36.3" +version = "0.36.4" dependencies = [ "log", "simple_logger", @@ -2515,7 +2525,7 @@ dependencies = [ [[package]] name = "tmc-langs-python3" -version = "0.36.3" +version = "0.36.4" dependencies = [ "dunce", "hex", @@ -2537,7 +2547,7 @@ dependencies = [ [[package]] name = "tmc-langs-r" -version = "0.36.3" +version = "0.36.4" dependencies = [ "log", "serde", @@ -2553,10 +2563,11 @@ dependencies = [ [[package]] name = "tmc-langs-util" -version = "0.36.3" +version = "0.36.4" dependencies = [ "dunce", "fd-lock", + "file-lock", "log", "nom", "once_cell", @@ -2576,7 +2587,7 @@ dependencies = [ [[package]] name = "tmc-mooc-client" -version = "0.36.3" +version = "0.36.4" dependencies = [ "bytes", "chrono", @@ -2596,7 +2607,7 @@ dependencies = [ [[package]] name = "tmc-server-mock" -version = "0.36.3" +version = "0.36.4" dependencies = [ "mockito", "serde_json", @@ -2604,7 +2615,7 @@ dependencies = [ [[package]] name = "tmc-testmycode-client" -version = "0.36.3" +version = "0.36.4" dependencies = [ "chrono", "dirs", @@ -2632,9 +2643,9 @@ dependencies = [ [[package]] name = "tokio" -version = "1.40.0" +version = "1.41.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2b070231665d27ad9ec9b8df639893f46727666c6767db40317fbe920a5d998" +checksum = "145f3413504347a2be84393cc8a7d2fb4d863b375909ea59f2158261aa258bbb" dependencies = [ "backtrace", "bytes", @@ -2692,9 +2703,9 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.22.20" +version = "0.22.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "583c44c02ad26b0c3f3066fe629275e50627026c51ac2e595cca4c230ce1ce1d" +checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5" dependencies = [ "indexmap", "serde", @@ -2755,7 +2766,7 @@ dependencies = [ "Inflector", "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.85", "termcolor", ] @@ -2776,30 +2787,27 @@ checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" [[package]] name = "unicase" -version = "2.7.0" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7d2d4dafb69621809a81864c9c1b864479e1235c0dd4e199924b9742439ed89" -dependencies = [ - "version_check", -] +checksum = "7e51b68083f157f853b6379db119d1c1be0e6e4dec98101079dec41f6f5cf6df" [[package]] name = "unicode-bidi" -version = "0.3.15" +version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" +checksum = "5ab17db44d7388991a428b2ee655ce0c212e862eff1768a455c58f9aad6e7893" [[package]] name = "unicode-ident" -version = "1.0.12" +version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" +checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" [[package]] name = "unicode-normalization" -version = "0.1.23" +version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a56d1686db2308d901306f92a263857ef59ea39678a5458e7cb17f01415101f5" +checksum = "5033c97c4262335cded6d6fc3e5c18ab755e1a3dc96376350f3d8e9f009ad956" dependencies = [ "tinyvec", ] @@ -2836,9 +2844,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.10.0" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81dfa00651efa65069b0b6b651f4aaa31ba9e3c3ce0137aaad053604ee7e0314" +checksum = "f8c5f0a0af699448548ad1a2fbf920fb4bee257eae39953ba95cb84891a0446a" dependencies = [ "getrandom", "serde", @@ -2877,9 +2885,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.93" +version = "0.2.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a82edfc16a6c469f5f44dc7b571814045d60404b55a0ee849f9bcfa2e63dd9b5" +checksum = "128d1e363af62632b8eb57219c8fd7877144af57558fb2ef0368d0087bddeb2e" dependencies = [ "cfg-if", "once_cell", @@ -2888,24 +2896,24 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.93" +version = "0.2.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9de396da306523044d3302746f1208fa71d7532227f15e347e2d93e4145dd77b" +checksum = "cb6dd4d3ca0ddffd1dd1c9c04f94b868c37ff5fac97c30b97cff2d74fce3a358" dependencies = [ "bumpalo", "log", "once_cell", "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.85", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-futures" -version = "0.4.43" +version = "0.4.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61e9300f63a621e96ed275155c108eb6f843b6a26d053f122ab69724559dc8ed" +checksum = "cc7ec4f8827a71586374db3e87abdb5a2bb3a15afed140221307c3ec06b1f63b" dependencies = [ "cfg-if", "js-sys", @@ -2915,9 +2923,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.93" +version = "0.2.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "585c4c91a46b072c92e908d99cb1dcdf95c5218eeb6f3bf1efa991ee7a68cccf" +checksum = "e79384be7f8f5a9dd5d7167216f022090cf1f9ec128e6e6a482a2cb5c5422c56" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -2925,28 +2933,28 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.93" +version = "0.2.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "afc340c74d9005395cf9dd098506f7f44e38f2b4a21c6aaacf9a105ea5e1e836" +checksum = "26c6ab57572f7a24a4985830b120de1594465e5d500f24afe89e16b4e833ef68" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.85", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.93" +version = "0.2.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c62a0a307cb4a311d3a07867860911ca130c3494e8c2719593806c08bc5d0484" +checksum = "65fc09f10666a9f147042251e0dda9c18f166ff7de300607007e96bdebc1068d" [[package]] name = "web-sys" -version = "0.3.70" +version = "0.3.72" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26fdeaafd9bd129f65e7c031593c24d62186301e0c72c8978fa1678be7d532c0" +checksum = "f6488b90108c040df0fe62fa815cbdee25124641df01814dd7282749234c6112" dependencies = [ "js-sys", "wasm-bindgen", @@ -3148,9 +3156,9 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "winnow" -version = "0.6.18" +version = "0.6.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68a9bda4691f099d435ad181000724da8e5899daa10713c2d432552b9ccd3a6f" +checksum = "36c1fec1a2bb5866f07c25f68c26e565c4c200aebb96d7e55710c19d3e8ac49b" dependencies = [ "memchr", ] @@ -3200,7 +3208,7 @@ checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.85", ] [[package]] @@ -3220,7 +3228,7 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.85", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 8b47da24c55..c0172d3fc42 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,7 +22,7 @@ authors = [ edition = "2021" license = "MIT OR Apache-2.0" rust-version = "1.70.0" -version = "0.36.3" +version = "0.36.4" [workspace.dependencies] mooc-langs-api = { git = "https://github.com/rage/secret-project-331.git", rev = "9fb5f894c72932e77dafa6d0f00df7a8abdfa84c" } diff --git a/crates/bindings/tmc-langs-node/src/de.rs b/crates/bindings/tmc-langs-node/src/de.rs index 0c6ed7d486e..f660e81ecc9 100644 --- a/crates/bindings/tmc-langs-node/src/de.rs +++ b/crates/bindings/tmc-langs-node/src/de.rs @@ -10,7 +10,7 @@ use serde::de::{ pub fn from_value<'j, C, T>(cx: &mut C, value: Handle<'j, JsValue>) -> LibResult where C: Context<'j>, - T: DeserializeOwned + ?Sized, + T: DeserializeOwned, { let mut deserializer: Deserializer = Deserializer::new(cx, value); let t = T::deserialize(&mut deserializer)?; diff --git a/crates/bindings/tmc-langs-node/src/helpers.rs b/crates/bindings/tmc-langs-node/src/helpers.rs index 011a47b8af1..7cf4cf063b4 100644 --- a/crates/bindings/tmc-langs-node/src/helpers.rs +++ b/crates/bindings/tmc-langs-node/src/helpers.rs @@ -9,8 +9,8 @@ macro_rules! lock { ( $cx: ident, $( $path: expr ),+ ) => { $( let path_buf: PathBuf = (&$path).into(); - let mut fl = $crate::file_util::FileLock::new(path_buf).map_err(|e| $crate::helpers::convert_err(&mut $cx, e))?; - let _lock = fl.lock().map_err(|e| $crate::helpers::convert_err(&mut $cx, e))?; + let mut lock = $crate::file_util::Lock::dir(path_buf, $crate::file_util::LockOptions::Write).map_err(|e| $crate::helpers::convert_err(&mut $cx, e))?; + let _guard = lock.lock().map_err(|e| $crate::helpers::convert_err(&mut $cx, e))?; )* }; } diff --git a/crates/bindings/tmc-langs-node/src/lib.rs b/crates/bindings/tmc-langs-node/src/lib.rs index 12ed50c4352..086a26258fb 100644 --- a/crates/bindings/tmc-langs-node/src/lib.rs +++ b/crates/bindings/tmc-langs-node/src/lib.rs @@ -134,11 +134,14 @@ fn extract_project(mut cx: FunctionContext) -> JsResult { compression: Compression ); - let mut archive = - file_util::open_file_locked(archive_path).map_err(|e| convert_err(&mut cx, e))?; - let mut guard = archive.write().expect("failed to lock file"); + let mut archive_lock = file_util::Lock::file(archive_path, file_util::LockOptions::Read) + .map_err(|e| convert_err(&mut cx, e))?; + let mut archive_guard = archive_lock.lock().map_err(|e| convert_err(&mut cx, e))?; let mut data = vec![]; - guard.read_to_end(&mut data).expect("failed to read data"); + archive_guard + .get_file_mut() + .read_to_end(&mut data) + .expect("failed to read data"); let res = tmc_langs::extract_project(Cursor::new(data), &output_path, compression, false, false); diff --git a/crates/bindings/tmc-langs-node/ts/generated.d.ts b/crates/bindings/tmc-langs-node/ts/generated.d.ts index 18af2a11cd9..8c37ac88510 100644 --- a/crates/bindings/tmc-langs-node/ts/generated.d.ts +++ b/crates/bindings/tmc-langs-node/ts/generated.d.ts @@ -1,76 +1,244 @@ -export interface StyleValidationResult { strategy: StyleValidationStrategy, validationErrors: Record> | null, } +export type StyleValidationResult = { strategy: StyleValidationStrategy, validationErrors: Record> | null, } -export interface StyleValidationError { column: number, line: number, message: string, sourceName: string, } +export type StyleValidationError = { column: number, line: number, message: string, sourceName: string, } export type StyleValidationStrategy = "FAIL" | "WARN" | "DISABLED"; -export interface ExercisePackagingConfiguration { student_file_paths: Array, exercise_file_paths: Array, } +export type ExercisePackagingConfiguration = { +/** + * Student folders or files which are copied from submission. + */ +student_file_paths: Array, +/** + * Exercise folders or files which are copied from exercise template or clone. + */ +exercise_file_paths: Array, } -export interface LocalExercise { "exercise-slug": string, "exercise-path": string, } +export type LocalExercise = { "exercise-slug": string, "exercise-path": string, } export type Compression = "tar" | "zip" | "zstd"; -export interface RefreshData { "new-cache-path": string, "course-options": object, exercises: Array, } - -export interface RefreshExercise { name: string, checksum: string, points: Array, "sandbox-image": string, "tmcproject-yml": TmcProjectYml | null, } - -export interface TmcProjectYml { extra_student_files: Array, extra_exercise_files: Array, force_update: Array, tests_timeout_ms?: number, fail_on_valgrind_error?: boolean, minimum_python_version?: PythonVer, sandbox_image?: string, } - -export interface PythonVer { major: number, minor: number | null, patch: number | null, } - -export interface RunResult { status: RunStatus, testResults: Array, logs: Record, } +export type RefreshData = { "new-cache-path": string, "course-options": object, exercises: Array, } + +export type RefreshExercise = { name: string, checksum: string, points: Array, "sandbox-image": string, "tmcproject-yml": TmcProjectYml | null, } + +export type TmcProjectYml = { +/** + * A list of files or directories that will always be considered student files. + */ +extra_student_files: Array, +/** + * A list of files or directories that will always be considered exercise files. + * `extra_student_files` takes precedence if a file is both an extra student file and an extra exercise file. + */ +extra_exercise_files: Array, +/** + * A list of files that should always be overwritten by updates even if they are student files. + */ +force_update: Array, +/** + * If set, tests are forcibly stopped after this duration. + */ +tests_timeout_ms?: number, +/** + * If set, Valgrind errors will be considered test errors. + */ +fail_on_valgrind_error?: boolean, +/** + * If set, will cause an error telling the student to update their Python if their version is older than the minimum. + */ +minimum_python_version?: PythonVer, +/** + * Overrides the default sandbox image. e.g. `eu.gcr.io/moocfi-public/tmc-sandbox-python:latest` + */ +sandbox_image?: string, } + +export type PythonVer = { major: number, minor: number | null, patch: number | null, } + +export type RunResult = { +/** + * The overall status of a test run. + */ +status: RunStatus, +/** + * Whether each test passed and which points were awarded. + */ +testResults: Array, +/** + * Logs from the test run. + * The key may be an arbitrary string identifying the type of log. + */ +logs: Record, } export type RunStatus = "PASSED" | "TESTS_FAILED" | "COMPILE_FAILED" | "TESTRUN_INTERRUPTED" | "GENERIC_ERROR"; -export interface TestResult { name: string, successful: boolean, points: Array, message: string, exception: Array, } - -export interface ExerciseDesc { name: string, tests: Array, } - -export interface TestDesc { name: string, points: Array, } - -export interface UpdatedExercise { id: number, } - -export interface DownloadOrUpdateCourseExercisesResult { downloaded: Array, skipped: Array, failed?: Array<[ExerciseDownload, Array]>, } - -export interface ExerciseDownload { id: number, "course-slug": string, "exercise-slug": string, path: string, } - -export interface CombinedCourseData { details: CourseDetails, exercises: Array, settings: CourseData, } - -export interface CourseDetails { id: number, name: string, title: string, description: string | null, details_url: string, unlock_url: string, reviews_url: string, comet_url: string, spyware_urls: Array, unlockables: Array, exercises: Array, } - -export interface Exercise { id: number, name: string, locked: boolean, deadline_description: string | null, deadline: string | null, soft_deadline: string | null, soft_deadline_description: string | null, checksum: string, return_url: string, zip_url: string, returnable: boolean, requires_review: boolean, attempted: boolean, completed: boolean, reviewed: boolean, all_review_points_given: boolean, memory_limit: number | null, runtime_params: Array, valgrind_strategy: string | null, code_review_requests_enabled: boolean, run_tests_locally_action_enabled: boolean, latest_submission_url: string | null, latest_submission_id: number | null, solution_zip_url: string | null, } +export type TestResult = { name: string, successful: boolean, +/** + * List of points that were received from the exercise from passed tests. + */ +points: Array, message: string, exception: Array, } + +export type ExerciseDesc = { +/** + * The name of the exercise to be shown to the user. + * Does not necessarily match or even contain the directory name. + */ +name: string, +/** + * Descriptions of the tests that will be run for this exercise. + */ +tests: Array, } + +export type TestDesc = { +/** + * The full name of the test. + * + * If the language organises tests into suites or classes, it is customary + * to name the test as "class_name.method_name". + */ +name: string, +/** + * The list of point names that passing this test may give. + * + * To obtain a point X, the user must pass all exercises that require point X. + */ +points: Array, } + +export type UpdatedExercise = { id: number, } + +export type DownloadOrUpdateCourseExercisesResult = { downloaded: Array, skipped: Array, failed?: Array<[ExerciseDownload, Array]>, } + +export type ExerciseDownload = { id: number, "course-slug": string, "exercise-slug": string, path: string, } + +export type CombinedCourseData = { details: CourseDetails, exercises: Array, settings: CourseData, } + +export type CourseDetails = { unlockables: Array, exercises: Array, id: number, name: string, title: string, description: string | null, +/** + * /api/v8/core/courses/{course_id} + */ +details_url: string, +/** + * /api/v8/core/courses/{course_id}/unlock + */ +unlock_url: string, +/** + * /api/v8/core/courses/{course_id}/reviews + */ +reviews_url: string, +/** + * Typically empty. + */ +comet_url: string, spyware_urls: Array, } + +export type Exercise = { id: number, name: string, locked: boolean, deadline_description: string | null, deadline: string | null, soft_deadline: string | null, soft_deadline_description: string | null, checksum: string, +/** + * /api/v8/core/exercises/{exercise_id}/submissions + */ +return_url: string, +/** + * /api/v8/core/exercises/{exercise_id}/download + */ +zip_url: string, returnable: boolean, requires_review: boolean, attempted: boolean, completed: boolean, reviewed: boolean, all_review_points_given: boolean, memory_limit: number | null, runtime_params: Array, valgrind_strategy: string | null, code_review_requests_enabled: boolean, run_tests_locally_action_enabled: boolean, +/** + * Typically null. + */ +latest_submission_url: string | null, latest_submission_id: number | null, +/** + * /api/v8/core/exercises/{exercise_id}/solution/download + */ +solution_zip_url: string | null, } + +export type CourseExercise = { id: number, available_points: Array, awarded_points: Array, name: string, publish_time: string | null, solution_visible_after: string | null, deadline: string | null, soft_deadline: string | null, disabled: boolean, unlocked: boolean, } + +export type ExercisePoint = { id: number, exercise_id: number, name: string, requires_review: boolean, } + +export type CourseData = { name: string, hide_after: string | null, hidden: boolean, cache_version: number | null, spreadsheet_key: string | null, hidden_if_registered_after: string | null, refreshed_at: string | null, locked_exercise_points_visible: boolean, description: string | null, paste_visibility: number | null, formal_name: string | null, certificate_downloadable: boolean | null, certificate_unlock_spec: string | null, organization_id: number | null, disabled_status: string | null, title: string | null, +/** + * Typically empty. + */ +material_url: string | null, course_template_id: number | null, hide_submission_results: boolean, +/** + * Typically empty. + */ +external_scoreboard_url: string | null, organization_slug: string | null, } + +export type ExerciseDetails = { course_name: string, course_id: number, code_review_requests_enabled: boolean, run_tests_locally_action_enabled: boolean, exercise_name: string, exercise_id: number, unlocked_at: string | null, deadline: string | null, submissions: Array, } + +export type ExerciseSubmission = { exercise_name: string, id: number, user_id: number, course_id: number, created_at: string, all_tests_passed: boolean, points: string | null, +/** + * /api/v8/core/submissions/{submission_id}/download + */ +submitted_zip_url: string, +/** + * https://tmc.mooc.fi/paste/{paste_code} + */ +paste_url: string | null, processing_time: number | null, reviewed: boolean, requests_review: boolean, } + +export type Submission = { id: number, user_id: number, pretest_error: string | null, created_at: string, exercise_name: string, course_id: number, processed: boolean, all_tests_passed: boolean, points: string | null, processing_tried_at: string | null, processing_began_at: string | null, processing_completed_at: string | null, times_sent_to_sandbox: number, processing_attempts_started_at: string, params_json: string | null, requires_review: boolean, requests_review: boolean, reviewed: boolean, message_for_reviewer: string, newer_submission_reviewed: boolean, review_dismissed: boolean, paste_available: boolean, message_for_paste: string, paste_key: string | null, } + +export type UpdateResult = { created: Array, updated: Array, } + +export type Organization = { name: string, information: string, slug: string, logo_path: string, pinned: boolean, } + +export type Review = { submission_id: number, exercise_name: string, id: number, marked_as_read: boolean, reviewer_name: string, review_body: string, points: Array, points_not_awarded: Array, +/** + * https://tmc.mooc.fi/submissions/{submission_id}/reviews + */ +url: string, +/** + * /api/v8/core/courses/{course_id}/reviews/{review_id} + */ +update_url: string, created_at: string, updated_at: string, } + +export type NewSubmission = { +/** + * https://tmc.mooc.fi/api/v8/core/submissions/{submission_id} + */ +show_submission_url: string, +/** + * https://tmc.mooc.fi/paste/{paste_code} + */ +paste_url: string, +/** + * https://tmc.mooc.fi/submissions/{submission_id} + */ +submission_url: string, } + +export type SubmissionFeedbackResponse = { api_version: number, status: SubmissionStatus, } -export interface CourseExercise { id: number, available_points: Array, awarded_points: Array, name: string, publish_time: string | null, solution_visible_after: string | null, deadline: string | null, soft_deadline: string | null, disabled: boolean, unlocked: boolean, } - -export interface ExercisePoint { id: number, exercise_id: number, name: string, requires_review: boolean, } - -export interface CourseData { name: string, hide_after: string | null, hidden: boolean, cache_version: number | null, spreadsheet_key: string | null, hidden_if_registered_after: string | null, refreshed_at: string | null, locked_exercise_points_visible: boolean, description: string | null, paste_visibility: number | null, formal_name: string | null, certificate_downloadable: boolean | null, certificate_unlock_spec: string | null, organization_id: number | null, disabled_status: string | null, title: string | null, material_url: string | null, course_template_id: number | null, hide_submission_results: boolean, external_scoreboard_url: string | null, organization_slug: string | null, } - -export interface ExerciseDetails { course_name: string, course_id: number, code_review_requests_enabled: boolean, run_tests_locally_action_enabled: boolean, exercise_name: string, exercise_id: number, unlocked_at: string | null, deadline: string | null, submissions: Array, } - -export interface ExerciseSubmission { exercise_name: string, id: number, user_id: number, course_id: number, created_at: string, all_tests_passed: boolean, points: string | null, submitted_zip_url: string, paste_url: string | null, processing_time: number | null, reviewed: boolean, requests_review: boolean, } - -export interface Submission { id: number, user_id: number, pretest_error: string | null, created_at: string, exercise_name: string, course_id: number, processed: boolean, all_tests_passed: boolean, points: string | null, processing_tried_at: string | null, processing_began_at: string | null, processing_completed_at: string | null, times_sent_to_sandbox: number, processing_attempts_started_at: string, params_json: string | null, requires_review: boolean, requests_review: boolean, reviewed: boolean, message_for_reviewer: string, newer_submission_reviewed: boolean, review_dismissed: boolean, paste_available: boolean, message_for_paste: string, paste_key: string | null, } - -export interface UpdateResult { created: Array, updated: Array, } - -export interface Organization { name: string, information: string, slug: string, logo_path: string, pinned: boolean, } +export type SubmissionStatus = "processing" | "fail" | "ok" | "error" | "hidden"; -export interface Review { submission_id: number, exercise_name: string, id: number, marked_as_read: boolean, reviewer_name: string, review_body: string, points: Array, points_not_awarded: Array, url: string, update_url: string, created_at: string, updated_at: string, } +export type TmcStyleValidationResult = { strategy: TmcStyleValidationStrategy, validation_errors: Record> | null, } -export interface NewSubmission { show_submission_url: string, paste_url: string, submission_url: string, } +export type TmcStyleValidationError = { column: number, line: number, message: string, source_name: string, } -export interface SubmissionFeedbackResponse { api_version: number, status: SubmissionStatus, } +export type TmcStyleValidationStrategy = "FAIL" | "WARN" | "DISABLED"; -export type SubmissionStatus = "processing" | "fail" | "ok" | "error" | "hidden"; +export type SubmissionFinished = { api_version: number, all_tests_passed: boolean | null, user_id: number, login: string, course: string, exercise_name: string, status: SubmissionStatus, points: Array, valgrind: string | null, +/** + * https://tmc.mooc.fi/submissions/{submission_id}} + */ +submission_url: string, +/** + * https://tmc.mooc.fi/exercises/{exercise_id}/solution + */ +solution_url: string | null, submitted_at: string, processing_time: number | null, reviewed: boolean, requests_review: boolean, +/** + * https://tmc.mooc.fi/paste/{paste_code} + */ +paste_url: string | null, message_for_paste: string | null, missing_review_points: Array, test_cases: Array | null, feedback_questions: Array | null, +/** + * /api/v8/core/submissions/{submission_id}/feedback + */ +feedback_answer_url: string | null, error: string | null, validations: TmcStyleValidationResult | null, } -export interface SubmissionFinished { api_version: number, all_tests_passed: boolean | null, user_id: number, login: string, course: string, exercise_name: string, status: SubmissionStatus, points: Array, valgrind: string | null, submission_url: string, solution_url: string | null, submitted_at: string, processing_time: number | null, reviewed: boolean, requests_review: boolean, paste_url: string | null, message_for_paste: string | null, missing_review_points: Array, test_cases: Array | null, feedback_questions: Array | null, feedback_answer_url: string | null, error: string | null, validations: StyleValidationResult | null, } +export type TestCase = { name: string, successful: boolean, message: string | null, exception: Array | null, detailed_message: string | null, } -export interface TestCase { name: string, successful: boolean, message: string | null, exception: Array | null, detailed_message: string | null, } - -export interface SubmissionFeedbackQuestion { id: number, question: string, kind: SubmissionFeedbackKind, } +export type SubmissionFeedbackQuestion = { id: number, question: string, kind: SubmissionFeedbackKind, } export type SubmissionFeedbackKind = "Text" | { "IntRange": { lower: number, upper: number, } }; -export interface TmcConfig { projects_dir: string, } +export type TmcConfig = { projects_dir: string, } + +export type CourseInstance = { id: string, course_id: string, course_slug: string, course_name: string, course_description: string | null, instance_name: string | null, instance_description: string | null, } diff --git a/crates/plugins/make/src/check_log.rs b/crates/plugins/make/src/check_log.rs index e5b0e2362e3..5cfca286894 100644 --- a/crates/plugins/make/src/check_log.rs +++ b/crates/plugins/make/src/check_log.rs @@ -5,6 +5,7 @@ use std::collections::HashMap; use tmc_langs_framework::{RunResult, RunStatus, TestResult}; #[derive(Debug, Deserialize)] +#[allow(dead_code)] pub struct CheckLog { #[allow(dead_code)] pub datetime: String, @@ -52,6 +53,7 @@ impl CheckLog { } #[derive(Debug, Deserialize)] +#[allow(dead_code)] pub struct TestSuite { #[allow(dead_code)] pub title: String, @@ -60,6 +62,7 @@ pub struct TestSuite { } #[derive(Debug, Deserialize)] +#[allow(dead_code)] pub struct Test { pub result: String, #[allow(dead_code)] diff --git a/crates/plugins/make/src/valgrind_log.rs b/crates/plugins/make/src/valgrind_log.rs index 8105443042e..86e0d76f531 100644 --- a/crates/plugins/make/src/valgrind_log.rs +++ b/crates/plugins/make/src/valgrind_log.rs @@ -11,6 +11,7 @@ use std::{ use tmc_langs_util::{file_util, FileError}; #[derive(Debug)] +#[allow(dead_code)] pub struct ValgrindLog { #[allow(dead_code)] pub header: (String, Vec), @@ -80,6 +81,7 @@ impl ValgrindLog { } #[derive(Debug)] +#[allow(dead_code)] pub struct ValgrindResult { #[allow(dead_code)] pub pid: String, diff --git a/crates/tmc-langs-cli/src/lib.rs b/crates/tmc-langs-cli/src/lib.rs index b566e299de6..61ff25cfe73 100644 --- a/crates/tmc-langs-cli/src/lib.rs +++ b/crates/tmc-langs-cli/src/lib.rs @@ -20,19 +20,19 @@ use serde_json::Value; use std::{ collections::HashMap, env, - fs::File, io::{self, BufReader, Cursor, Read}, - ops::Deref, path::{Path, PathBuf}, }; use tmc_langs::{ - file_util, mooc::MoocClient, tmc::{request::FeedbackAnswer, TestMyCodeClient, TestMyCodeClientError}, CommandError, Compression, Credentials, DownloadOrUpdateCourseExercisesResult, DownloadResult, Language, StyleValidationResult, TmcConfig, UpdatedExercise, }; -use tmc_langs_util::deserialize; +use tmc_langs_util::{ + deserialize, + file_util::{self, Lock, LockOptions}, +}; pub enum ParsingResult { Ok(Cli), @@ -180,14 +180,18 @@ fn run_app(cli: Cli) -> Result { locale: Locale(locale), output_path, } => { - file_util::lock!(exercise_path); + let mut lock = Lock::dir(&exercise_path, LockOptions::Read)?; + let _guard = lock.lock()?; + let check_result = run_checkstyle_write_results(&exercise_path, output_path.as_deref(), locale)?; CliOutput::finished_with_data("ran checkstyle", check_result.map(DataKind::Validation)) } Command::Clean { exercise_path } => { - file_util::lock!(exercise_path); + let mut lock = Lock::dir(&exercise_path, LockOptions::Write)?; + let _guard = lock.lock()?; + tmc_langs::clean(&exercise_path)?; CliOutput::finished(format!("cleaned exercise at {}", exercise_path.display())) } @@ -199,7 +203,9 @@ fn run_app(cli: Cli) -> Result { deterministic, naive, } => { - file_util::lock!(exercise_path); + let mut lock = Lock::dir(&exercise_path, LockOptions::Read)?; + let _guard = lock.lock()?; + let hash = tmc_langs::compress_project_to( &exercise_path, &output_path, @@ -227,11 +233,11 @@ fn run_app(cli: Cli) -> Result { compression, naive, } => { - let mut archive = file_util::open_file_locked(&archive_path)?; - let mut guard = archive.write()?; + let mut archive_lock = Lock::file(&archive_path, LockOptions::Read)?; + let mut archive_guard = archive_lock.lock()?; let mut data = vec![]; - guard.read_to_end(&mut data)?; + archive_guard.get_file_mut().read_to_end(&mut data)?; tmc_langs::extract_project(Cursor::new(data), &output_path, compression, true, naive)?; @@ -243,7 +249,9 @@ fn run_app(cli: Cli) -> Result { } Command::FastAvailablePoints { exercise_path } => { - file_util::lock!(exercise_path); + let mut lock = Lock::dir(&exercise_path, LockOptions::Read)?; + let _guard = lock.lock()?; + let points = tmc_langs::get_available_points(&exercise_path)?; CliOutput::finished_with_data( format!("found {} available points", points.len()), @@ -255,7 +263,9 @@ fn run_app(cli: Cli) -> Result { search_path, output_path, } => { - file_util::lock!(search_path); + let mut lock = Lock::dir(&search_path, LockOptions::Read)?; + let _guard = lock.lock()?; + let exercises = tmc_langs::find_exercise_directories(&search_path).with_context(|| { format!( @@ -276,7 +286,9 @@ fn run_app(cli: Cli) -> Result { exercise_path, output_path, } => { - file_util::lock!(exercise_path); + let mut lock = Lock::dir(&exercise_path, LockOptions::Read)?; + let _guard = lock.lock()?; + let config = tmc_langs::get_exercise_packaging_configuration(&exercise_path) .with_context(|| { format!( @@ -313,7 +325,9 @@ fn run_app(cli: Cli) -> Result { exercise_path, output_path, } => { - file_util::lock!(exercise_path); + let mut lock = Lock::dir(&exercise_path, LockOptions::Read)?; + let _guard = lock.lock()?; + tmc_langs::prepare_solution(&exercise_path, &output_path).with_context(|| { format!( "Failed to prepare solutions for exercise at {}", @@ -331,7 +345,9 @@ fn run_app(cli: Cli) -> Result { exercise_path, output_path, } => { - file_util::lock!(exercise_path); + let mut lock = Lock::dir(&exercise_path, LockOptions::Read)?; + let _guard = lock.lock()?; + tmc_langs::prepare_stub(&exercise_path, &output_path).with_context(|| { format!( "Failed to prepare stubs for exercise at {}", @@ -357,6 +373,9 @@ fn run_app(cli: Cli) -> Result { tmc_param, no_archive_prefix, } => { + let mut clone_lock = Lock::dir(&clone_path, file_util::LockOptions::Read)?; + let _clone_guard = clone_lock.lock()?; + // will contain for each key all the values with that key in a list let mut tmc_params_grouped = HashMap::new(); for value in &tmc_param { @@ -439,7 +458,8 @@ fn run_app(cli: Cli) -> Result { output_path, wait_for_secret, } => { - file_util::lock!(exercise_path); + let mut lock = Lock::dir(&exercise_path, LockOptions::Read)?; + let _guard = lock.lock()?; let secret = if wait_for_secret { let mut s = String::new(); @@ -496,7 +516,8 @@ fn run_app(cli: Cli) -> Result { exercise_path, output_path, } => { - file_util::lock!(exercise_path); + let mut lock = Lock::dir(&exercise_path, LockOptions::Read)?; + let _guard = lock.lock()?; let exercise_name = exercise_path.file_name().with_context(|| { format!( @@ -584,6 +605,9 @@ fn run_tmc_inner( exercise_id, target, } => { + let mut output_lock = Lock::dir(&target, file_util::LockOptions::WriteTruncate)?; + let _output_guard = output_lock.lock()?; + client .download_model_solution(exercise_id, &target) .context("Failed to download model solution")?; @@ -596,6 +620,9 @@ fn run_tmc_inner( exercise_id, output_path, } => { + let mut output_lock = Lock::dir(&output_path, file_util::LockOptions::Write)?; + let _output_guard = output_lock.lock()?; + tmc_langs::download_old_submission( client, exercise_id, @@ -837,7 +864,9 @@ fn run_tmc_inner( paste_message, submission_path, } => { - file_util::lock!(submission_path); + let mut lock = Lock::dir(&submission_path, LockOptions::Read)?; + let _guard = lock.lock()?; + let locale = locale.map(|l| l.0); let new_submission = client .paste(exercise_id, &submission_path, paste_message, locale) @@ -851,7 +880,9 @@ fn run_tmc_inner( message_for_reviewer, submission_path, } => { - file_util::lock!(submission_path); + let mut lock = Lock::dir(&submission_path, LockOptions::Read)?; + let _guard = lock.lock()?; + let new_submission = client .request_code_review( exercise_id, @@ -871,7 +902,9 @@ fn run_tmc_inner( save_old_state, exercise_path, } => { - file_util::lock!(exercise_path); + let mut lock = Lock::dir(&exercise_path, LockOptions::Write)?; + let _guard = lock.lock()?; + if save_old_state { // submit current state client.submit(exercise_id, &exercise_path, None)?; @@ -923,7 +956,9 @@ fn run_tmc_inner( submission_path, exercise_id, } => { - file_util::lock!(submission_path); + let mut lock = Lock::dir(&submission_path, LockOptions::Read)?; + let _guard = lock.lock()?; + let locale = locale.map(|l| l.0); let new_submission = client .submit(exercise_id, &submission_path, locale) @@ -1040,7 +1075,9 @@ fn run_mooc_inner(mooc: Mooc, client: &mut MoocClient) -> Result { task_id, submission_path, } => { - file_util::lock!(submission_path); + let mut lock = Lock::dir(&submission_path, LockOptions::Read)?; + let _guard = lock.lock()?; + let temp = file_util::named_temp_file()?; tmc_langs::compress_project_to( &submission_path, @@ -1128,27 +1165,28 @@ fn write_result_to_file_as_json( pretty: bool, secret: Option, ) -> Result<()> { - let mut output_file = file_util::create_file_locked(output_path).with_context(|| { - format!( - "Failed to create results JSON file at {}", - output_path.display() - ) - })?; - let guard = output_file.write()?; + let mut output_lock = + Lock::file(output_path, LockOptions::WriteTruncate).with_context(|| { + format!( + "Failed to create results JSON file at {}", + output_path.display() + ) + })?; + let mut output_guard = output_lock.lock()?; if let Some(secret) = secret { let token = tmc_langs::sign_with_jwt(result, secret.as_bytes())?; - file_util::write_to_writer(token, guard.deref()) + file_util::write_to_writer(token, output_guard.get_file_mut()) .with_context(|| format!("Failed to write result to {}", output_path.display()))?; } else if pretty { - serde_json::to_writer_pretty(guard.deref(), result).with_context(|| { + serde_json::to_writer_pretty(output_guard.get_file_mut(), result).with_context(|| { format!( "Failed to write result as JSON to {}", output_path.display() ) })?; } else { - serde_json::to_writer(guard.deref(), result).with_context(|| { + serde_json::to_writer(output_guard.get_file_mut(), result).with_context(|| { format!( "Failed to write result as JSON to {}", output_path.display() @@ -1172,13 +1210,16 @@ fn run_checkstyle_write_results( ) })?; if let Some(output_path) = output_path { - let output_file = File::create(output_path).with_context(|| { - format!( - "Failed to create code style check results file at {}", - output_path.display() - ) - })?; - serde_json::to_writer(output_file, &check_result).with_context(|| { + let mut output_lock = + Lock::file(output_path, LockOptions::WriteTruncate).with_context(|| { + format!( + "Failed to create code style check results file at {}", + output_path.display() + ) + })?; + let mut output_guard = output_lock.lock()?; + + serde_json::to_writer(output_guard.get_file_mut(), &check_result).with_context(|| { format!( "Failed to write code style check results as JSON to {}", output_path.display() diff --git a/crates/tmc-langs-framework/src/archive.rs b/crates/tmc-langs-framework/src/archive.rs index 9279da65c41..87a170372f6 100644 --- a/crates/tmc-langs-framework/src/archive.rs +++ b/crates/tmc-langs-framework/src/archive.rs @@ -365,7 +365,7 @@ fn walk_dir_for_compression( .sort_by_file_name() .into_iter() // filter windows lock files - .filter_entry(|e| e.file_name() != ".tmc.lock") + .filter_entry(|e| e.file_name() != file_util::LOCK_FILE_NAME) { let entry = entry?; let stripped = entry diff --git a/crates/tmc-langs-framework/src/tmc_project_yml.rs b/crates/tmc-langs-framework/src/tmc_project_yml.rs index 0e96757be96..1f5e871744a 100644 --- a/crates/tmc-langs-framework/src/tmc_project_yml.rs +++ b/crates/tmc-langs-framework/src/tmc_project_yml.rs @@ -7,10 +7,12 @@ use serde::{ }; use std::{ fmt::{self, Display}, - ops::Deref, path::{Path, PathBuf}, }; -use tmc_langs_util::{deserialize, file_util, FileError}; +use tmc_langs_util::{ + deserialize, + file_util::{self, Lock, LockOptions}, +}; /// Extra data from a `.tmcproject.yml` file. #[derive(Debug, Serialize, Deserialize, Default, Clone)] @@ -125,11 +127,9 @@ impl TmcProjectYml { /// Saves the TmcProjectYml to the given directory. pub fn save_to_dir(&self, dir: &Path) -> Result<(), TmcError> { let config_path = Self::path_in_dir(dir); - let mut file = file_util::create_file_locked(&config_path)?; - let guard = file - .write() - .map_err(|e| FileError::FdLock(config_path.clone(), e))?; - serde_yaml::to_writer(guard.deref(), &self)?; + let mut lock = Lock::file(&config_path, LockOptions::WriteCreate)?; + let mut guard = lock.lock()?; + serde_yaml::to_writer(guard.get_file_mut(), &self)?; Ok(()) } } diff --git a/crates/tmc-langs-util/Cargo.toml b/crates/tmc-langs-util/Cargo.toml index d6bf51b8caf..2be05577eab 100644 --- a/crates/tmc-langs-util/Cargo.toml +++ b/crates/tmc-langs-util/Cargo.toml @@ -10,7 +10,6 @@ rust-version.workspace = true ts-rs = { workspace = true, features = ["serde-compat"], optional = true } dunce = "1.0.3" -fd-lock = "4.0.0" log = "0.4.14" nom = "7.1.0" once_cell = "1.9.0" @@ -25,7 +24,11 @@ type-map = "0.5.0" walkdir = "2.3.2" [target.'cfg(windows)'.dependencies] +fd-lock = "4.0.0" winapi = "0.3.9" +[target.'cfg(unix)'.dependencies] +file-lock = "2.1.11" + [dev-dependencies] simple_logger = "5.0.0" diff --git a/crates/tmc-langs-util/src/error.rs b/crates/tmc-langs-util/src/error.rs index 9748f2ed3c4..cd0bf1d2f32 100644 --- a/crates/tmc-langs-util/src/error.rs +++ b/crates/tmc-langs-util/src/error.rs @@ -47,8 +47,6 @@ pub enum FileError { Canonicalize(PathBuf, #[source] std::io::Error), // lock errors - #[error("Failed to lock file at path {0}")] - FdLock(PathBuf, #[source] std::io::Error), #[error("Failed to lock {0}: not a file or directory")] InvalidLockPath(PathBuf), diff --git a/crates/tmc-langs-util/src/file_util.rs b/crates/tmc-langs-util/src/file_util.rs index 8dabdb34340..2e5a6bb8427 100644 --- a/crates/tmc-langs-util/src/file_util.rs +++ b/crates/tmc-langs-util/src/file_util.rs @@ -2,59 +2,54 @@ #[cfg(unix)] mod lock_unix; -#[cfg(unix)] -pub use lock_unix::*; - #[cfg(windows)] mod lock_windows; + use crate::error::FileError; -use fd_lock::RwLock; +#[cfg(unix)] +pub use lock_unix::*; #[cfg(windows)] pub use lock_windows::*; use std::{ - fs::{self, File, ReadDir}, + fs::{self, File, OpenOptions, ReadDir}, io::{Read, Write}, - ops::{Deref, DerefMut}, path::{Path, PathBuf}, }; use tempfile::NamedTempFile; use walkdir::WalkDir; -/// Convenience macro for locking a path. -#[macro_export] -macro_rules! lock { - ( $( $path: expr ),+ ) => { - $( - let path_buf: PathBuf = (&$path).into(); - let mut fl = $crate::file_util::FileLock::new(path_buf)?; - let _lock = fl.lock()?; - )* - }; -} - -// macros always live at the top-level, re-export here -pub use crate::lock; - -pub struct FdLockWrapper(RwLock); - -impl Deref for FdLockWrapper { - type Target = RwLock; - - fn deref(&self) -> &Self::Target { - &self.0 - } +pub const LOCK_FILE_NAME: &str = ".tmc.lock"; + +#[derive(Debug, Clone, Copy)] +pub enum LockOptions { + /// Shared read lock + Read, + /// Shared read lock, create file if it doesn't exist instead of erroring (including intermediate directories) + ReadCreate, + /// Shared write lock, create file if it doesn't exist, truncate if it does + ReadTruncate, + /// Exclusive write lock + Write, + /// Exclusive write lock, create file if it doesn't exist instead of erroring (including intermediate directories) + WriteCreate, + /// Exclusive write lock, create file if it doesn't exist, truncate if it does + WriteTruncate, } -impl DerefMut for FdLockWrapper { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.0 - } -} - -impl Drop for FdLockWrapper { - fn drop(&mut self) { - // todo: print the path - log::trace!("unlocking file"); +impl LockOptions { + fn into_open_options(self) -> OpenOptions { + let mut opts = OpenOptions::new(); + match self { + Self::Read => opts.read(true), + // create requires write + Self::ReadCreate => opts.read(true).write(true).create(true), + // truncate requires write + Self::ReadTruncate => opts.write(true).create(true).truncate(true), + Self::Write => opts.write(true), + Self::WriteCreate => opts.write(true).create(true), + Self::WriteTruncate => opts.write(true).create(true).truncate(true), + }; + opts } } @@ -75,15 +70,6 @@ pub fn open_file(path: impl AsRef) -> Result { File::open(path).map_err(|e| FileError::FileOpen(path.to_path_buf(), e)) } -/// Opens and locks the given file. Note: Does not work on directories on Windows. -pub fn open_file_locked(path: impl AsRef) -> Result { - log::trace!("locking file {}", path.as_ref().display()); - - let file = open_file(path)?; - let lock = FdLockWrapper(RwLock::new(file)); - Ok(lock) -} - pub fn read_file>(path: P) -> Result, FileError> { let path = path.as_ref(); let mut file = open_file(path)?; @@ -117,21 +103,6 @@ pub fn create_file>(path: P) -> Result { File::create(path).map_err(|e| FileError::FileCreate(path.to_path_buf(), e)) } -/// Creates a file and wraps it in a lock. If a file already exists at the path, it acquires a lock on it first and then recreates it. -/// Note: creates all intermediary directories if needed. -pub fn create_file_locked>(path: P) -> Result { - log::trace!("locking file {}", path.as_ref().display()); - - if let Ok(existing) = open_file(&path) { - // wait for an existing process to be done with the file before rewriting - let mut lock = RwLock::new(existing); - let _ = lock.write().expect("\"On Unix this may return an error if the operation was interrupted by a signal handler.\"; sounds unlikely to ever actually cause problems"); - } - let file = create_file(path)?; - let lock = FdLockWrapper(RwLock::new(file)); - Ok(lock) -} - /// Removes whatever is at the path, whether it is a directory or file. The _all suffix hopefully makes the function sound at least slightly dangerous. pub fn remove_all>(path: P) -> Result<(), FileError> { let path = path.as_ref(); @@ -149,6 +120,12 @@ pub fn remove_file>(path: P) -> Result<(), FileError> { fs::remove_file(path).map_err(|e| FileError::FileRemove(path.to_path_buf(), e)) } +pub fn remove_file_locked>(path: P) -> Result<(), FileError> { + let path = path.as_ref(); + let _lock = Lock::file(path, LockOptions::Write)?; + fs::remove_file(path).map_err(|e| FileError::FileRemove(path.to_path_buf(), e)) +} + pub fn write_to_file, P: AsRef>( source: S, target: P, diff --git a/crates/tmc-langs-util/src/file_util/lock_unix.rs b/crates/tmc-langs-util/src/file_util/lock_unix.rs index 4999f61061e..002552d6420 100644 --- a/crates/tmc-langs-util/src/file_util/lock_unix.rs +++ b/crates/tmc-langs-util/src/file_util/lock_unix.rs @@ -1,131 +1,192 @@ -//! File locking utilities on Unix platforms. - -use crate::{error::FileError, file_util::*}; -use fd_lock::{RwLock, RwLockWriteGuard}; -use std::{fs::File, path::PathBuf}; - -/// Wrapper for fd_lock::FdLock. Used to lock files/directories to prevent concurrent access -/// from multiple instances of tmc-langs. -pub struct FileLock { - path: PathBuf, - fd_lock: RwLock, +//! File locking utilities on Unix-based platforms. + +use super::LockOptions; +use crate::{ + error::FileError, + file_util::{self, LOCK_FILE_NAME}, +}; +use file_lock::{FileLock, FileOptions}; +use std::{ + fs::File, + path::{Path, PathBuf}, +}; + +/// Blocks until the lock can be acquired. +#[derive(Debug)] +pub struct Lock { + pub path: PathBuf, + options: LockOptions, + lock_file_path: Option, } -impl FileLock { - pub fn new(path: PathBuf) -> Result { - let file = open_file(&path)?; +impl Lock { + pub fn file(path: impl AsRef, options: LockOptions) -> Result { + let path = path.as_ref().to_path_buf(); + + if matches!(options, LockOptions::ReadCreate | LockOptions::WriteCreate) { + if let Some(parent) = path.parent() { + file_util::create_dir_all(parent)?; + } + } Ok(Self { path, - fd_lock: RwLock::new(file), + options, + lock_file_path: None, }) } - /// Blocks until the lock can be acquired. - pub fn lock(&mut self) -> Result { - log::trace!("locking {}", self.path.display()); - let path = &self.path; - let fd_lock = &mut self.fd_lock; - let guard = fd_lock - .write() - .map_err(|e| FileError::FdLock(path.clone(), e))?; - log::trace!("locked {}", self.path.display()); - Ok(FileLockGuard { + pub fn dir(path: impl AsRef, options: LockOptions) -> Result { + let path = path.as_ref().to_path_buf(); + + if matches!(options, LockOptions::ReadCreate | LockOptions::WriteCreate) { + file_util::create_dir_all(&path)?; + } + + let lock_path = path.join(LOCK_FILE_NAME); + // first, try to create the lock file. this requires write options + // blocking set to false so it will fail if the lock file already exists, + // which is okay since we're not actually locking it here + let _creator_lock = FileLock::lock( + &lock_path, + false, + FileOptions::new().write(true).create(true), + ); + + Ok(Self { path, - _guard: guard, + options, + lock_file_path: Some(lock_path), }) } + + pub fn lock(&mut self) -> Result, FileError> { + log::trace!("locking {}", self.path.display()); + let path = match &self.lock_file_path { + Some(lock_file) => lock_file, + None => &self.path, + }; + let lock = match FileLock::lock(path, true, self.options.into_file_options()) { + Ok(lock) => { + log::trace!("locked {}", path.display()); + FileOrLock::Lock(lock) + } + Err(err) => { + // the file locking is mostly a safeguard rather than something absolutely necessary + // so rather than preventing the program from runningg here we'll just continue and things will probably work out + log::error!("Failed to lock {}: {err}", path.display()); + let file = self + .options + .into_open_options() + .open(&self.path) + .map_err(|e| FileError::FileOpen(path.to_path_buf(), e))?; + FileOrLock::File(file) + } + }; + Ok(Guard { lock, path }) + } +} + +impl Drop for Lock { + fn drop(&mut self) { + // check if we created a lock file + if let Some(lock_file_path) = self.lock_file_path.take() { + // try to get a write lock and delete file + // if we can't get the lock, something else probably has it locked and we leave it there + match FileLock::lock( + &lock_file_path, + false, + FileOptions::new().read(true).write(true), + ) { + Ok(_) => { + let _ = file_util::remove_file(&lock_file_path); + } + Err(err) => { + log::warn!( + "Failed to remove lock file {}: {err}", + lock_file_path.display() + ); + } + } + } + } } -/// Guard that holds the locked file. #[derive(Debug)] -pub struct FileLockGuard<'a> { +pub struct Guard<'a> { + lock: FileOrLock, path: &'a Path, - _guard: RwLockWriteGuard<'a, File>, } -impl Drop for FileLockGuard<'_> { +impl Guard<'_> { + pub fn get_file(&self) -> &File { + match &self.lock { + FileOrLock::File(f) => f, + FileOrLock::Lock(l) => &l.file, + } + } + + pub fn get_file_mut(&mut self) -> &mut File { + match &mut self.lock { + FileOrLock::File(f) => f, + FileOrLock::Lock(l) => &mut l.file, + } + } +} + +impl Drop for Guard<'_> { fn drop(&mut self) { - log::trace!("unlocking {}", self.path.display()); + log::trace!("unlocking {}", self.path.display()) + } +} + +#[derive(Debug)] +enum FileOrLock { + File(File), + Lock(FileLock), +} + +impl LockOptions { + fn into_file_options(self) -> FileOptions { + match self { + LockOptions::Read => FileOptions::new().read(true), + LockOptions::ReadCreate => FileOptions::new().read(true).create(true), + LockOptions::ReadTruncate => FileOptions::new().read(true).create(true).truncate(true), + LockOptions::Write => FileOptions::new().read(true).write(true).append(true), + LockOptions::WriteCreate => FileOptions::new() + .read(true) + .write(true) + .append(true) + .create(true), + LockOptions::WriteTruncate => FileOptions::new() + .read(true) + .write(true) + .create(true) + .truncate(true), + } } } #[cfg(test)] -#[allow(clippy::unwrap_used)] mod test { use super::*; - use std::sync::{Arc, Mutex}; - use tempfile::NamedTempFile; - fn init() { - use log::*; - use simple_logger::*; - let _ = SimpleLogger::new().with_level(LevelFilter::Debug).init(); + #[test] + fn can_lock_file() { + let file = tempfile::NamedTempFile::new().unwrap(); + let _lock = Lock::file(file.path(), LockOptions::Read).unwrap(); } #[test] - fn locks_file() { - init(); - - let temp = NamedTempFile::new().unwrap(); - let temp_path = temp.path(); - let mut lock = FileLock::new(temp_path.to_path_buf()).unwrap(); - let mutex = Arc::new(Mutex::new(vec![])); - - // take file lock and then refcell - let guard = lock.lock().unwrap(); - let mut mguard = mutex.try_lock().unwrap(); - - let handle = { - let temp_path = temp_path.to_path_buf(); - let mutex = mutex.clone(); - - std::thread::spawn(move || { - let mut lock = FileLock::new(temp_path).unwrap(); - let _guard = lock.lock().unwrap(); - mutex.try_lock().unwrap().push(1); - }) - }; - - std::thread::sleep(std::time::Duration::from_millis(100)); - mguard.push(1); - - drop(mguard); - drop(guard); - handle.join().unwrap(); + fn can_lock_dir() { + let dir = tempfile::tempdir().unwrap(); + let _lock = Lock::dir(dir.path(), LockOptions::Read).unwrap(); } #[test] - fn locks_dir() { - init(); - - let temp = tempfile::tempdir().unwrap(); - let temp_path = temp.path(); - let mut lock = FileLock::new(temp_path.to_path_buf()).unwrap(); - let mutex = Arc::new(Mutex::new(vec![])); - - // take file lock and then refcell - let guard = lock.lock().unwrap(); - let mut mguard = mutex.try_lock().unwrap(); - - let handle = { - let temp_path = temp_path.to_path_buf(); - let refcell = mutex.clone(); - - std::thread::spawn(move || { - let mut lock = FileLock::new(temp_path).unwrap(); - // block on file lock and use refcell - let _guard = lock.lock().unwrap(); - refcell.try_lock().unwrap().push(1); - }) - }; - - // wait for the other thread to actually lock - std::thread::sleep(std::time::Duration::from_millis(100)); - mguard.push(1); - - // drop mutex guard then file lock - drop(mguard); - drop(guard); - handle.join().unwrap(); + fn can_delete_locked_file() { + let file = tempfile::NamedTempFile::new().unwrap(); + let _lock = Lock::file(file.path(), LockOptions::Read).unwrap(); + let _delete_lock = Lock::file(file.path(), LockOptions::Write).unwrap(); + file_util::remove_file(file.path()).unwrap(); } } diff --git a/crates/tmc-langs-util/src/file_util/lock_windows.rs b/crates/tmc-langs-util/src/file_util/lock_windows.rs index 8dd08ef98f2..d2b697addfe 100644 --- a/crates/tmc-langs-util/src/file_util/lock_windows.rs +++ b/crates/tmc-langs-util/src/file_util/lock_windows.rs @@ -1,10 +1,9 @@ //! File locking utilities on Windows. //! -//! Windows directories can't be locked with fd-lock, so a different solution is needed. -//! Currently, regular files are locked with fd-lock, but for directories a .tmc.lock file is created. +//! file-lock doesn't support Windows, so a different solution is needed. use crate::{error::FileError, file_util::*}; -use fd_lock::{RwLock, RwLockWriteGuard}; +use fd_lock::{RwLock, RwLockReadGuard, RwLockWriteGuard}; use std::{ borrow::Cow, fs::OpenOptions, @@ -18,120 +17,144 @@ use winapi::um::{ winnt::{FILE_ATTRIBUTE_HIDDEN, FILE_ATTRIBUTE_TEMPORARY}, }; -/// Wrapper for fd_lock::RwLock. Used to lock files/directories to prevent concurrent access -/// from multiple instances of tmc-langs. -pub struct FileLock { - path: PathBuf, - // this is re-set in every lock command if the target is a file - // ideally it would be set to none when the guard is dropped, but doing so is probably not worth the trouble - lock: Option>, +/// Blocks until the lock can be acquired. +#[derive(Debug)] +pub struct Lock { + pub path: PathBuf, + options: LockOptions, + lock: RwLock, } -impl FileLock { - pub fn new(path: PathBuf) -> Result { - Ok(Self { path, lock: None }) +impl Lock { + pub fn file(path: impl AsRef, options: LockOptions) -> Result { + let open_options = options.into_open_options(); + let file = open_options + .open(&path) + .map_err(|e| FileError::FileOpen(path.as_ref().to_path_buf(), e))?; + let lock = RwLock::new(file); + Ok(Self { + path: path.as_ref().to_path_buf(), + options, + lock, + }) } - /// Blocks until the lock can be acquired. - /// On Windows, directories cannot be locked, so we use a lock file instead. - pub fn lock(&mut self) -> Result { - log::trace!("locking {}", self.path.display()); + pub fn dir(path: impl AsRef, options: LockOptions) -> Result { + // for directories, we'll create/open a .tmc.lock file + let lock_path = path.as_ref().join(LOCK_FILE_NAME); + let start_time = Instant::now(); let mut warning_timer = Instant::now(); - - if self.path.is_file() { - // for files, just use the path - let file = open_file(&self.path)?; - let lock = RwLock::new(file); - self.lock = Some(lock); - let lock = self.lock.as_mut().expect("set to Some before this call"); - let guard = lock.write().expect("cannot fail on Windows"); - Ok(FileLockGuard { - _guard: LockInner::RwLockWriteGuard { _guard: guard }, - path: Cow::Borrowed(&self.path), - }) - } else if self.path.is_dir() { - // for directories, we'll create/open a .tmc.lock file - let lock_path = self.path.join(".tmc.lock"); - loop { - // try to create a new lock file - match OpenOptions::new() - // needed for create_new - .write(true) - // only creates file if it exists, check and creation are atomic - .create_new(true) - // hidden, so it won't be a problem when going through the directory - .attributes(FILE_ATTRIBUTE_HIDDEN) - // just tells windows there's probably no point in writing this to disk; - // this might further reduce the risk of leftover lock files - .attributes(FILE_ATTRIBUTE_TEMPORARY) - // windows deletes the lock file when the handle is closed = when the lock is dropped - .custom_flags(FILE_FLAG_DELETE_ON_CLOSE) - .open(&lock_path) - { - Ok(file) => { - // was able to create a new lock file - return Ok(FileLockGuard { - _guard: LockInner::LockFile { _file: file }, - path: Cow::Owned(lock_path), - }); - } - Err(err) => { - if err.kind() == ErrorKind::AlreadyExists { - // lock file already exists, let's wait a little and try again - // after 30 seconds, print a warning in the logs every 10 seconds - // after 120 seconds, print an error in the logs every 10 seconds - if start_time.elapsed() > Duration::from_secs(30) - && warning_timer.elapsed() > Duration::from_secs(10) - { - warning_timer = Instant::now(); - log::warn!( + loop { + // try to create a new lock file + match OpenOptions::new() + // needed for create_new + .write(true) + // only creates file if it exists, check and creation are atomic + .create(true) + // hidden, so it won't be a problem when going through the directory + .attributes(FILE_ATTRIBUTE_HIDDEN) + // just tells windows there's probably no point in writing this to disk; + // this might further reduce the risk of leftover lock files + .attributes(FILE_ATTRIBUTE_TEMPORARY) + // windows deletes the lock file when the handle is closed = when the lock is dropped + .custom_flags(FILE_FLAG_DELETE_ON_CLOSE) + .open(&lock_path) + { + Ok(file) => { + // was able to create/open the lock file + let lock = RwLock::new(file); + return Ok(Self { + path: path.as_ref().to_path_buf(), + options, + lock, + }); + } + Err(err) => { + if err.kind() == ErrorKind::AlreadyExists { + // lock file already exists, let's wait a little and try again + // after 30 seconds, print a warning in the logs every 10 seconds + // after 120 seconds, print an error in the logs every 10 seconds + if start_time.elapsed() > Duration::from_secs(30) + && warning_timer.elapsed() > Duration::from_secs(10) + { + warning_timer = Instant::now(); + log::warn!( "The program has been waiting for lock file {} to be deleted for {} seconds, the lock file might have been left over from a previous run due to an error.", lock_path.display(), start_time.elapsed().as_secs() ); - } else if start_time.elapsed() > Duration::from_secs(120) - && warning_timer.elapsed() > Duration::from_secs(10) - { - warning_timer = Instant::now(); - log::error!( + } else if start_time.elapsed() > Duration::from_secs(120) + && warning_timer.elapsed() > Duration::from_secs(10) + { + warning_timer = Instant::now(); + log::error!( "The program has been waiting for lock file {} to be deleted for {} seconds, the lock file might have been left over from a previous run due to an error.", lock_path.display(), start_time.elapsed().as_secs() ); - } - std::thread::sleep(Duration::from_millis(500)); - } else { - // something else went wrong, propagate error - return Err(FileError::FileCreate(lock_path, err)); } + std::thread::sleep(Duration::from_millis(500)); + } else { + // something else went wrong, propagate error + return Err(FileError::FileCreate(lock_path, err)); } } } - } else { - Err(FileError::InvalidLockPath(self.path.to_path_buf())) } } + + pub fn lock(&mut self) -> Result, FileError> { + log::trace!("locking {}", self.path.display()); + let guard = match self.options { + LockOptions::Read | LockOptions::ReadCreate | LockOptions::ReadTruncate => { + GuardInner::FdLockRead(self.lock.read().expect("cannot fail on Windows")) + } + LockOptions::Write | LockOptions::WriteCreate | LockOptions::WriteTruncate => { + GuardInner::FdLockWrite(self.lock.write().expect("cannot fail on Windows")) + } + }; + Ok(Guard { + guard, + path: Cow::Borrowed(&self.path), + }) + } } -pub struct FileLockGuard<'a> { - _guard: LockInner<'a>, +pub struct Guard<'a> { + guard: GuardInner<'a>, path: Cow<'a, PathBuf>, } -enum LockInner<'a> { - LockFile { _file: File }, - RwLockWriteGuard { _guard: RwLockWriteGuard<'a, File> }, +impl Guard<'_> { + pub fn get_file(&self) -> &File { + match &self.guard { + GuardInner::FdLockRead(guard) => guard, + GuardInner::FdLockWrite(guard) => guard, + } + } + + pub fn get_file_mut(&mut self) -> &File { + match &mut self.guard { + GuardInner::FdLockRead(guard) => guard, + GuardInner::FdLockWrite(guard) => guard, + } + } } -impl Drop for FileLockGuard<'_> { +impl Drop for Guard<'_> { fn drop(&mut self) { log::trace!("unlocking {}", self.path.display()); } } +enum GuardInner<'a> { + FdLockRead(RwLockReadGuard<'a, File>), + FdLockWrite(RwLockWriteGuard<'a, File>), +} + #[cfg(test)] mod test { use super::*; @@ -150,7 +173,7 @@ mod test { let temp = NamedTempFile::new().unwrap(); let temp_path = temp.path(); - let mut lock = FileLock::new(temp_path.to_path_buf()).unwrap(); + let mut lock = Lock::file(temp_path.to_path_buf(), LockOptions::Read).unwrap(); let mutex = Arc::new(Mutex::new(vec![])); // take file lock and then mutex @@ -163,7 +186,7 @@ mod test { std::thread::spawn(move || { // if the file lock doesn't block, the mutex lock will panic and the test will fail - let mut lock = FileLock::new(temp_path).unwrap(); + let mut lock = Lock::file(temp_path, LockOptions::Write).unwrap(); let _guard = lock.lock().unwrap(); mutex.try_lock().unwrap().push(1); }) @@ -186,7 +209,7 @@ mod test { let temp = tempfile::tempdir().unwrap(); let temp_path = temp.path(); - let mut lock = FileLock::new(temp_path.to_path_buf()).unwrap(); + let mut lock = Lock::dir(temp_path.to_path_buf(), LockOptions::Read).unwrap(); let mutex = Arc::new(Mutex::new(vec![])); // take file lock and mutex @@ -199,7 +222,7 @@ mod test { std::thread::spawn(move || { // if the file lock doesn't block, the mutex lock will panic and the test will fail - let mut lock = FileLock::new(temp_path).unwrap(); + let mut lock = Lock::dir(temp_path, LockOptions::Write).unwrap(); let _guard = lock.lock().unwrap(); mutex.try_lock().unwrap().push(1); }) @@ -221,12 +244,13 @@ mod test { init(); let temp = tempfile::tempdir().unwrap(); - let mut lock = FileLock::new(temp.path().to_path_buf()).unwrap(); + let mut lock = Lock::dir(temp.path().to_path_buf(), LockOptions::Read).unwrap(); let lock_path = temp.path().join(".tmc.lock"); - assert!(!lock_path.exists()); + assert!(lock_path.exists()); let guard = lock.lock().unwrap(); assert!(lock_path.exists()); drop(guard); + drop(lock); assert!(!lock_path.exists()); } } diff --git a/crates/tmc-langs/src/config.rs b/crates/tmc-langs/src/config.rs index 1e97d5729b0..024f6db1e99 100644 --- a/crates/tmc-langs/src/config.rs +++ b/crates/tmc-langs/src/config.rs @@ -15,7 +15,10 @@ use std::{ env, path::{Path, PathBuf}, }; -use tmc_langs_util::{file_util, FileError}; +use tmc_langs_util::{ + file_util::{self, Lock, LockOptions}, + FileError, +}; // base directory for a given plugin's settings files fn get_tmc_dir(client_name: &str) -> Result { @@ -70,9 +73,8 @@ pub fn migrate_exercise( exercise_path.display() ); - let mut lock = file_util::FileLock::new(exercise_path.to_path_buf())?; - let guard = lock.lock()?; - + let mut lock = Lock::dir(exercise_path, LockOptions::Write)?; + let _guard = lock.lock()?; let mut projects_config = ProjectsConfig::load(&tmc_config.projects_dir)?; let course_config = projects_config .courses @@ -99,7 +101,7 @@ pub fn migrate_exercise( }, ); - super::move_dir(exercise_path, guard, &target_dir)?; + super::move_dir(exercise_path, &target_dir)?; course_config.save_to_projects_dir(&tmc_config.projects_dir)?; Ok(()) } @@ -128,10 +130,9 @@ pub fn move_projects_dir(mut tmc_config: TmcConfig, target: PathBuf) -> Result<( let old_projects_dir = tmc_config.set_projects_dir(target.clone())?; - let mut lock = file_util::FileLock::new(old_projects_dir.clone())?; - let guard = lock.lock()?; - - super::move_dir(&old_projects_dir, guard, &target)?; + let mut lock = Lock::dir(old_projects_dir.clone(), LockOptions::Write)?; + let _guard = lock.lock()?; + super::move_dir(&old_projects_dir, &target)?; tmc_config.save()?; Ok(()) } diff --git a/crates/tmc-langs/src/config/credentials.rs b/crates/tmc-langs/src/config/credentials.rs index 00943cf1fee..49b87c9713a 100644 --- a/crates/tmc-langs/src/config/credentials.rs +++ b/crates/tmc-langs/src/config/credentials.rs @@ -2,8 +2,11 @@ use crate::{tmc::Token, LangsError}; use serde::{Deserialize, Serialize}; -use std::{ops::Deref, path::PathBuf}; -use tmc_langs_util::{deserialize, file_util, FileError}; +use std::path::PathBuf; +use tmc_langs_util::{ + deserialize, + file_util::{self, Lock, LockOptions}, +}; /// Credentials for authenticating with tmc-server. #[derive(Debug, Serialize, Deserialize)] @@ -31,12 +34,9 @@ impl Credentials { } log::debug!("Loading credentials from {}", credentials_path.display()); - let mut credentials_file = file_util::open_file_locked(&credentials_path)?; - let guard = credentials_file - .write() - .map_err(|e| FileError::FdLock(credentials_path.clone(), e))?; - - match deserialize::json_from_reader(guard.deref()) { + let mut credentials_lock = Lock::file(&credentials_path, LockOptions::Read)?; + let credentials_guard = credentials_lock.lock()?; + match deserialize::json_from_reader(credentials_guard.get_file()) { Ok(token) => Ok(Some(Credentials { path: credentials_path, token, @@ -58,13 +58,10 @@ impl Credentials { if let Some(p) = credentials_path.parent() { file_util::create_dir_all(p)?; } - let mut credentials_file = file_util::create_file_locked(&credentials_path)?; - let guard = credentials_file - .write() - .map_err(|e| FileError::FdLock(credentials_path.clone(), e))?; - + let mut credentials_lock = Lock::file(&credentials_path, LockOptions::WriteTruncate)?; + let mut credentials_guard = credentials_lock.lock()?; // write token - if let Err(e) = serde_json::to_writer(guard.deref(), &token) { + if let Err(e) = serde_json::to_writer(credentials_guard.get_file_mut(), &token) { // failed to write token, removing credentials file file_util::remove_file(&credentials_path)?; return Err(LangsError::Json(e)); @@ -73,9 +70,7 @@ impl Credentials { } pub fn remove(self) -> Result<(), LangsError> { - file_util::lock!(self.path); - - file_util::remove_file(&self.path)?; + file_util::remove_file_locked(self.path)?; Ok(()) } diff --git a/crates/tmc-langs/src/config/projects_config.rs b/crates/tmc-langs/src/config/projects_config.rs index 72edf2f9f41..dfd686fb327 100644 --- a/crates/tmc-langs/src/config/projects_config.rs +++ b/crates/tmc-langs/src/config/projects_config.rs @@ -6,10 +6,16 @@ use std::{ collections::{BTreeMap, HashMap}, path::{Path, PathBuf}, }; -use tmc_langs_util::{deserialize, file_util, FileError}; +use tmc_langs_util::{ + deserialize, + file_util::{self, Lock, LockOptions}, + FileError, +}; use walkdir::WalkDir; /// A project directory is a directory which contains directories of courses (which contain a `course_config.toml`). +const COURSE_CONFIG_FILE_NAME: &str = "course_config.toml"; + #[derive(Debug)] pub struct ProjectsConfig { // BTreeMap used so the exercises in the config file are ordered by key @@ -18,13 +24,14 @@ pub struct ProjectsConfig { impl ProjectsConfig { pub fn load(projects_dir: &Path) -> Result { - file_util::lock!(projects_dir); + let mut lock = Lock::dir(projects_dir, LockOptions::Read)?; + let _guard = lock.lock()?; let mut course_configs = HashMap::new(); let mut unexpected_entries = Vec::new(); for entry in WalkDir::new(projects_dir).min_depth(1).max_depth(1) { let entry = entry?; - let course_config_path = entry.path().join("course_config.toml"); + let course_config_path = entry.path().join(COURSE_CONFIG_FILE_NAME); if course_config_path.exists() { let file_name = entry.file_name(); let course_dir_name = file_name.to_str().ok_or_else(|| { @@ -138,7 +145,7 @@ impl CourseConfig { if !course_dir.exists() { file_util::create_dir_all(&course_dir)?; } - let target = course_dir.join("course_config.toml"); + let target = course_dir.join(COURSE_CONFIG_FILE_NAME); let s = toml::to_string_pretty(&self)?; file_util::write_to_file(s.as_bytes(), target)?; Ok(()) diff --git a/crates/tmc-langs/src/config/tmc_config.rs b/crates/tmc-langs/src/config/tmc_config.rs index 1f6fb212bf5..42835448a18 100644 --- a/crates/tmc-langs/src/config/tmc_config.rs +++ b/crates/tmc-langs/src/config/tmc_config.rs @@ -7,7 +7,11 @@ use std::{ io::{Read, Write}, path::{Path, PathBuf}, }; -use tmc_langs_util::{deserialize, file_util, FileError}; +use tmc_langs_util::{ + deserialize, + file_util::{self, Lock, LockOptions}, + FileError, +}; use toml::{value::Table, Value}; /// The main configuration file. A separate one is used for each client. @@ -35,44 +39,30 @@ impl TmcConfig { /// Reads or initialises for the client from the given path. pub fn load_from(client_name: &str, path: PathBuf) -> Result { // try to open config file - let config = match file_util::open_file_locked(&path) { - Ok(mut lock) => { - // found config file, lock and read - let mut guard = lock - .write() - .map_err(|e| FileError::FdLock(path.clone(), e))?; - let mut buf = String::new(); - let _bytes = guard - .read_to_string(&mut buf) - .map_err(|e| FileError::FileRead(path.clone(), e))?; - match deserialize::toml_from_str::(&buf) { - // successfully read file, try to deserialize - Ok(mut config) => { - // set the path which was set to default during deserialization - config.location = path; - config // successfully read and deserialized the config - } - Err(e) => { - log::error!( - "Failed to deserialize config at {} due to {}, resetting", - path.display(), - e - ); - drop(guard); // unlock file before recreating it - Self::init_at(client_name, path)? - } + let config = if path.exists() { + // found config file + let data = file_util::read_file_to_string(&path)?; + match deserialize::toml_from_str::(&data) { + // successfully read file, try to deserialize + Ok(mut config) => { + // set the path which was set to default during deserialization + config.location = path; + config // successfully read and deserialized the config + } + Err(e) => { + log::error!( + "Failed to deserialize config at {} due to {}, resetting", + path.display(), + e + ); + Self::init_at(client_name, path)? } } - Err(e) => { - // failed to open config file, create new one - log::info!( - "could not open config file at {} due to {}, initializing a new config file", - path.display(), - e - ); - // todo: check the cause to make sure this makes sense, might be necessary to propagate some error kinds - Self::init_at(client_name, path)? - } + } else { + // failed to open config file, create new one + log::info!("initializing a new config file at {}", path.display()); + // todo: check the cause to make sure this makes sense, might be necessary to propagate some error kinds + Self::init_at(client_name, path)? }; if !config.projects_dir.exists() { @@ -87,11 +77,8 @@ impl TmcConfig { file_util::create_dir_all(parent)?; } - let mut lock = file_util::create_file_locked(&path)?; - let mut guard = lock - .write() - .map_err(|e| FileError::FdLock(path.to_path_buf(), e))?; - + let mut lock = Lock::file(&path, LockOptions::WriteTruncate)?; + let mut guard = lock.lock()?; let default_project_dir = get_projects_dir_root()?.join(get_client_stub(client_name)); file_util::create_dir_all(&default_project_dir)?; @@ -103,6 +90,7 @@ impl TmcConfig { let toml = toml::to_string_pretty(&config).expect("this should never fail"); guard + .get_file_mut() .write_all(toml.as_bytes()) .map_err(|e| FileError::FileWrite(config.location.to_path_buf(), e))?; Ok(config) diff --git a/crates/tmc-langs/src/lib.rs b/crates/tmc-langs/src/lib.rs index b75c1f6a5a8..5466d6670c8 100644 --- a/crates/tmc-langs/src/lib.rs +++ b/crates/tmc-langs/src/lib.rs @@ -51,10 +51,7 @@ use tmc_langs_plugins::{ CSharpPlugin, MakePlugin, NoTestsPlugin, Plugin, PluginType, Python3Plugin, RPlugin, }; // the Java plugin is disabled on musl -pub use tmc_langs_util::{ - file_util::{self, FileLockGuard}, - notification_reporter, progress_reporter, -}; +pub use tmc_langs_util::{file_util, notification_reporter, progress_reporter}; pub use tmc_mooc_client as mooc; pub use tmc_testmycode_client as tmc; use toml::Value as TomlValue; @@ -976,7 +973,7 @@ pub fn extract_student_files( Ok(()) } -fn move_dir(source: &Path, source_lock: FileLockGuard, target: &Path) -> Result<(), LangsError> { +fn move_dir(source: &Path, target: &Path) -> Result<(), LangsError> { let mut file_count_copied = 0; let mut file_count_total = 0; for entry in WalkDir::new(source) { @@ -1031,7 +1028,8 @@ fn move_dir(source: &Path, source_lock: FileLockGuard, target: &Path) -> Result< } } - drop(source_lock); + // remove lock file if any + file_util::remove_file(source.join(file_util::LOCK_FILE_NAME)).ok(); file_util::remove_dir_empty(source)?; finish_stage("Finished moving project directory"); diff --git a/crates/tmc-langs/src/submission_packaging.rs b/crates/tmc-langs/src/submission_packaging.rs index 8338b9559d8..189e0d356a9 100644 --- a/crates/tmc-langs/src/submission_packaging.rs +++ b/crates/tmc-langs/src/submission_packaging.rs @@ -50,6 +50,7 @@ pub fn prepare_submission( "Thumbs.db", ".directory", "__MACOSX", + file_util::LOCK_FILE_NAME, ]; if let Some((stub_zip, compression)) = stub_archive { // if defined, extract and use as the base @@ -172,18 +173,44 @@ pub fn prepare_submission( match output_format { Compression::Tar => { let mut archive = tar::Builder::new(archive_file); - log::debug!( - "appending \"{}\" at \"{}\"", - extract_dest_path.display(), - prefix.display() - ); + for entry in WalkDir::new(&extract_dest_path) + .into_iter() + .filter_entry(|e| e.file_name() != file_util::LOCK_FILE_NAME) + .skip(1) + { + let entry = entry?; + let entry_path = entry.path(); + let stripped = prefix.join( + entry_path + .strip_prefix(&extract_dest_path) + .expect("the entry is inside dest"), + ); + log::debug!( + "adding {} to tar at {}", + entry_path.display(), + stripped.display() + ); + if entry_path.is_dir() { + archive + .append_dir(&stripped, entry_path) + .map_err(|e| LangsError::TarAppend(entry_path.to_path_buf(), e))?; + } else { + archive + .append_path_with_name(entry_path, stripped.to_string_lossy().as_ref()) + .map_err(|e| LangsError::TarAppend(entry_path.to_path_buf(), e))?; + } + } archive - .append_dir_all(prefix, &extract_dest_path) - .map_err(|e| LangsError::TarAppend(extract_dest_path, e))?; + .finish() + .map_err(|e| LangsError::TarAppend(extract_dest_path.clone(), e))?; } Compression::Zip => { let mut archive = ZipWriter::new(archive_file); - for entry in WalkDir::new(&extract_dest_path).into_iter().skip(1) { + for entry in WalkDir::new(&extract_dest_path) + .into_iter() + .filter_entry(|e| e.file_name() != file_util::LOCK_FILE_NAME) + .skip(1) + { let entry = entry?; let entry_path = entry.path(); let stripped = prefix.join( @@ -216,14 +243,34 @@ pub fn prepare_submission( Compression::TarZstd => { let buf = Cursor::new(vec![]); let mut archive = tar::Builder::new(buf); - log::debug!( - "appending \"{}\" at \"{}\"", - extract_dest_path.display(), - prefix.display() - ); - archive - .append_dir_all(prefix, &extract_dest_path) - .map_err(|e| LangsError::TarAppend(extract_dest_path, e))?; + for entry in WalkDir::new(&extract_dest_path) + .into_iter() + .filter_entry(|e| e.file_name() != file_util::LOCK_FILE_NAME) + .skip(1) + { + let entry = entry?; + let entry_path = entry.path(); + let stripped = prefix.join( + entry_path + .strip_prefix(&extract_dest_path) + .expect("the entry is inside dest"), + ); + log::debug!( + "adding {} to zip at {}", + entry_path.display(), + stripped.display() + ); + if entry_path.is_dir() { + archive + .append_dir(stripped.to_string_lossy().as_ref(), entry_path) + .map_err(|e| LangsError::TarAppend(entry_path.to_path_buf(), e))?; + } else { + archive + .append_path_with_name(entry_path, stripped.to_string_lossy().as_ref()) + .map_err(|e| LangsError::TarAppend(entry_path.to_path_buf(), e))?; + } + } + archive.finish().map_err(LangsError::TarFinish)?; let mut tar = archive.into_inner().map_err(LangsError::TarIntoInner)?; tar.set_position(0); // reset the cursor