@rehearsal-labs/retest 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -5
- package/dist/api/source-location.d.ts.map +1 -1
- package/dist/api/source-location.js +3 -1
- package/dist/browser/builds.d.ts +35 -6
- package/dist/browser/builds.d.ts.map +1 -1
- package/dist/browser/builds.js +115 -16
- package/dist/browser/cdp/transport.d.ts.map +1 -1
- package/dist/browser/cdp/transport.js +4 -4
- package/dist/browser/chromium-process.d.ts +23 -0
- package/dist/browser/chromium-process.d.ts.map +1 -1
- package/dist/browser/chromium-process.js +78 -13
- package/dist/browser/electron.d.ts +1 -1
- package/dist/browser/electron.d.ts.map +1 -1
- package/dist/browser/electron.js +6 -3
- package/dist/browser/executables.d.ts +16 -1
- package/dist/browser/executables.d.ts.map +1 -1
- package/dist/browser/executables.js +50 -18
- package/dist/browser/firefox/accessible-names.d.ts +5 -4
- package/dist/browser/firefox/accessible-names.d.ts.map +1 -1
- package/dist/browser/firefox/accessible-names.js +11 -6
- package/dist/browser/firefox/bridge.js +3 -2
- package/dist/browser/firefox/executable.d.ts +14 -14
- package/dist/browser/firefox/executable.d.ts.map +1 -1
- package/dist/browser/firefox/executable.js +29 -21
- package/dist/browser/firefox/launch.js +1 -1
- package/dist/browser/firefox/navigation.d.ts +5 -2
- package/dist/browser/firefox/navigation.d.ts.map +1 -1
- package/dist/browser/firefox/navigation.js +48 -14
- package/dist/browser/firefox/orphans.d.ts.map +1 -1
- package/dist/browser/firefox/orphans.js +34 -2
- package/dist/browser/firefox/page.d.ts.map +1 -1
- package/dist/browser/firefox/page.js +113 -19
- package/dist/browser/firefox/process.d.ts +20 -1
- package/dist/browser/firefox/process.d.ts.map +1 -1
- package/dist/browser/firefox/process.js +124 -22
- package/dist/browser/firefox/profile.d.ts +5 -0
- package/dist/browser/firefox/profile.d.ts.map +1 -1
- package/dist/browser/firefox/profile.js +17 -1
- package/dist/browser/firefox/storage.d.ts.map +1 -1
- package/dist/browser/firefox/storage.js +4 -2
- package/dist/browser/firefox/subscriptions.d.ts +26 -0
- package/dist/browser/firefox/subscriptions.d.ts.map +1 -0
- package/dist/browser/firefox/subscriptions.js +10 -0
- package/dist/browser/launch.d.ts +6 -0
- package/dist/browser/launch.d.ts.map +1 -1
- package/dist/browser/launch.js +14 -1
- package/dist/cli/commands/doctor.js +3 -3
- package/dist/cli/commands/inspect.js +3 -2
- package/dist/cli/commands/install.d.ts.map +1 -1
- package/dist/cli/commands/install.js +26 -13
- package/dist/cli/commands/licences.d.ts +22 -0
- package/dist/cli/commands/licences.d.ts.map +1 -1
- package/dist/cli/commands/licences.js +62 -20
- package/dist/cli/commands/run.js +2 -2
- package/dist/cli/doctor/checks.js +4 -1
- package/dist/cli/inspect/diagnostics.js +3 -3
- package/dist/cli/inspect/test-timeline.js +10 -10
- package/dist/cli/install/doctor-rows.d.ts.map +1 -1
- package/dist/cli/install/doctor-rows.js +5 -4
- package/dist/cli/install/file-changes.d.ts +44 -0
- package/dist/cli/install/file-changes.d.ts.map +1 -0
- package/dist/cli/install/file-changes.js +81 -0
- package/dist/cli/install/install-archive.d.ts +3 -1
- package/dist/cli/install/install-archive.d.ts.map +1 -1
- package/dist/cli/install/install-archive.js +80 -33
- package/dist/cli/install/lock.d.ts.map +1 -1
- package/dist/cli/install/lock.js +108 -21
- package/dist/cli/install/media-doctor.d.ts.map +1 -1
- package/dist/cli/install/media-doctor.js +2 -1
- package/dist/cli/install/media-install.d.ts.map +1 -1
- package/dist/cli/install/media-install.js +38 -19
- package/dist/cli/install/media-pins.d.ts +7 -1
- package/dist/cli/install/media-pins.d.ts.map +1 -1
- package/dist/cli/install/media-pins.js +23 -12
- package/dist/cli/install/media-record.d.ts.map +1 -1
- package/dist/cli/install/media-record.js +10 -9
- package/dist/cli/install/media-report.d.ts +3 -1
- package/dist/cli/install/media-report.d.ts.map +1 -1
- package/dist/cli/install/media-report.js +7 -5
- package/dist/cli/install/media-tools.d.ts.map +1 -1
- package/dist/cli/install/media-tools.js +6 -4
- package/dist/cli/install/process-start.d.ts +3 -2
- package/dist/cli/install/process-start.d.ts.map +1 -1
- package/dist/cli/install/process-start.js +4 -0
- package/dist/cli/install/report.d.ts.map +1 -1
- package/dist/cli/install/report.js +11 -7
- package/dist/cli/install/supervisor-install.d.ts +33 -0
- package/dist/cli/install/supervisor-install.d.ts.map +1 -0
- package/dist/cli/install/supervisor-install.js +158 -0
- package/dist/cli/install/supervisor-locate.d.ts +47 -0
- package/dist/cli/install/supervisor-locate.d.ts.map +1 -0
- package/dist/cli/install/supervisor-locate.js +113 -0
- package/dist/cli/install/supervisor-pins.d.ts +21 -0
- package/dist/cli/install/supervisor-pins.d.ts.map +1 -0
- package/dist/cli/install/supervisor-pins.js +28 -0
- package/dist/cli/install/supervisor-record.d.ts +31 -0
- package/dist/cli/install/supervisor-record.d.ts.map +1 -0
- package/dist/cli/install/supervisor-record.js +104 -0
- package/dist/cli/install/supervisor-report.d.ts +11 -0
- package/dist/cli/install/supervisor-report.d.ts.map +1 -0
- package/dist/cli/install/supervisor-report.js +33 -0
- package/dist/cli/install/supervisor-tools.d.ts +43 -0
- package/dist/cli/install/supervisor-tools.d.ts.map +1 -0
- package/dist/cli/install/supervisor-tools.js +114 -0
- package/dist/cli/install/unpack.d.ts +23 -3
- package/dist/cli/install/unpack.d.ts.map +1 -1
- package/dist/cli/install/unpack.js +124 -10
- package/dist/cli/install/zip-member.d.ts +15 -0
- package/dist/cli/install/zip-member.d.ts.map +1 -0
- package/dist/cli/install/zip-member.js +77 -0
- package/dist/cli/interrupt.d.ts +1 -1
- package/dist/cli/interrupt.d.ts.map +1 -1
- package/dist/cli/interrupt.js +15 -10
- package/dist/cli/terminal.d.ts +9 -1
- package/dist/cli/terminal.d.ts.map +1 -1
- package/dist/cli/terminal.js +14 -0
- package/dist/config/define.d.ts +2 -2
- package/dist/config/define.js +2 -2
- package/dist/diagnostics/firefox-collector.d.ts +11 -10
- package/dist/diagnostics/firefox-collector.d.ts.map +1 -1
- package/dist/diagnostics/firefox-collector.js +16 -11
- package/dist/media/client.d.ts +2 -1
- package/dist/media/client.d.ts.map +1 -1
- package/dist/media/client.js +16 -5
- package/dist/media/host.d.ts +32 -0
- package/dist/media/host.d.ts.map +1 -0
- package/dist/media/host.js +50 -0
- package/dist/media/locate.d.ts.map +1 -1
- package/dist/media/locate.js +4 -3
- package/dist/native/processes.d.ts.map +1 -1
- package/dist/native/processes.js +7 -3
- package/dist/protocol/files.d.ts +1 -1
- package/dist/protocol/files.d.ts.map +1 -1
- package/dist/protocol/files.js +10 -5
- package/dist/reporters/commands.d.ts +12 -3
- package/dist/reporters/commands.d.ts.map +1 -1
- package/dist/reporters/commands.js +29 -11
- package/dist/reporters/human-card.js +1 -1
- package/dist/reporters/human.d.ts +3 -1
- package/dist/reporters/human.d.ts.map +1 -1
- package/dist/reporters/human.js +8 -8
- package/dist/reporters/style.d.ts +21 -2
- package/dist/reporters/style.d.ts.map +1 -1
- package/dist/reporters/style.js +11 -2
- package/dist/runner/app-server.d.ts +27 -7
- package/dist/runner/app-server.d.ts.map +1 -1
- package/dist/runner/app-server.js +74 -24
- package/dist/runner/child.js +6 -2
- package/dist/runner/media-leftovers.d.ts +7 -3
- package/dist/runner/media-leftovers.d.ts.map +1 -1
- package/dist/runner/media-leftovers.js +27 -6
- package/dist/runner/resources.d.ts +8 -0
- package/dist/runner/resources.d.ts.map +1 -1
- package/dist/runner/resources.js +12 -1
- package/dist/runner/run-session.d.ts.map +1 -1
- package/dist/runner/run-session.js +9 -4
- package/dist/runner/run.d.ts.map +1 -1
- package/dist/runner/run.js +2 -1
- package/dist/runner/target-drivers.d.ts +5 -2
- package/dist/runner/target-drivers.d.ts.map +1 -1
- package/dist/runner/target-drivers.js +10 -1
- package/dist/runner/test-file-process.d.ts +8 -0
- package/dist/runner/test-file-process.d.ts.map +1 -1
- package/dist/runner/test-file-process.js +26 -3
- package/dist/runner/test-pages.d.ts.map +1 -1
- package/dist/runner/test-pages.js +5 -2
- package/dist/shared/environment.d.ts +17 -0
- package/dist/shared/environment.d.ts.map +1 -0
- package/dist/shared/environment.js +27 -0
- package/dist/shared/metadata-process-worker.js +15 -7
- package/dist/shared/metadata-process.d.ts +5 -0
- package/dist/shared/metadata-process.d.ts.map +1 -1
- package/dist/shared/metadata-process.js +36 -6
- package/dist/shared/posix-path.d.ts +12 -0
- package/dist/shared/posix-path.d.ts.map +1 -1
- package/dist/shared/posix-path.js +16 -0
- package/dist/shared/private-file.d.ts +38 -0
- package/dist/shared/private-file.d.ts.map +1 -0
- package/dist/shared/private-file.js +132 -0
- package/dist/shared/process-ownership.d.ts +52 -2
- package/dist/shared/process-ownership.d.ts.map +1 -1
- package/dist/shared/process-ownership.js +104 -11
- package/dist/shared/process-supervisor.d.ts +309 -0
- package/dist/shared/process-supervisor.d.ts.map +1 -0
- package/dist/shared/process-supervisor.js +737 -0
- package/dist/shared/process-system.d.ts +113 -0
- package/dist/shared/process-system.d.ts.map +1 -0
- package/dist/shared/process-system.js +254 -0
- package/dist/shared/regular-file.d.ts +2 -2
- package/dist/shared/regular-file.d.ts.map +1 -1
- package/dist/shared/regular-file.js +19 -2
- package/dist/store/artifacts.d.ts +5 -4
- package/dist/store/artifacts.d.ts.map +1 -1
- package/dist/store/artifacts.js +18 -5
- package/dist/store/run-store.d.ts +5 -2
- package/dist/store/run-store.d.ts.map +1 -1
- package/dist/store/run-store.js +48 -4
- package/media/Cargo.lock +16 -0
- package/media/Cargo.toml +16 -1
- package/media/src/encoder.rs +56 -12
- package/media/src/frame.rs +7 -1
- package/media/src/main.rs +20 -2
- package/media/src/place.rs +230 -11
- package/media/src/process_ownership_windows.rs +1058 -0
- package/media/src/recording.rs +15 -7
- package/media/src/replies.rs +55 -0
- package/media/src/server.rs +72 -2
- package/media/src/store.rs +71 -2
- package/package.json +9 -4
- package/src/cli/install/media-notices.txt +465 -1
- package/supervisor/Cargo.lock +25 -0
- package/supervisor/Cargo.toml +22 -0
- package/supervisor/NOTICES +464 -0
- package/supervisor/build.rs +44 -0
- package/supervisor/src/command.rs +612 -0
- package/supervisor/src/jobs.rs +1650 -0
- package/supervisor/src/main.rs +71 -0
- package/supervisor/src/reply.rs +367 -0
package/dist/store/run-store.js
CHANGED
|
@@ -7,6 +7,7 @@ import { diagnosticsFolder, eventsFile, logsFolder, resultFile, statesFolder } f
|
|
|
7
7
|
import { parse } from "../protocol/schema.js";
|
|
8
8
|
import { storageStateSchema } from "../protocol/storage-state.js";
|
|
9
9
|
import { errorCode } from "../shared/error-code.js";
|
|
10
|
+
import { keepFolderToUser, PrivateFolderError } from "../shared/private-file.js";
|
|
10
11
|
/** Thrown when the run folder cannot be created or already holds another run. */
|
|
11
12
|
export class RunFolderError extends Error {
|
|
12
13
|
name = 'RunFolderError';
|
|
@@ -27,6 +28,7 @@ export class RunStore {
|
|
|
27
28
|
#resultWritten = false;
|
|
28
29
|
#closed = false;
|
|
29
30
|
#removeStatesOnExit;
|
|
31
|
+
#statesReady;
|
|
30
32
|
// Where the run's clock stands at zero on the host's monotonic clock, as the events written so far place it.
|
|
31
33
|
#clockZero;
|
|
32
34
|
#clock;
|
|
@@ -140,16 +142,44 @@ export class RunStore {
|
|
|
140
142
|
}
|
|
141
143
|
/**
|
|
142
144
|
* Saves a sign-in state where only this user can read it, never over another. States hold session cookies, so
|
|
143
|
-
* they are removed when the run ends, and when the process exits before it could.
|
|
145
|
+
* they are removed when the run ends, and when the process exits before it could. Their folder is made, by this
|
|
146
|
+
* process alone, and kept to this user before any state is in it (on Windows, whose files take their folder's access
|
|
147
|
+
* list, that is when its list is replaced); each state is then made exclusively and written through the descriptor
|
|
148
|
+
* that made it, so nothing written ever follows a link or lands in a file someone else made.
|
|
144
149
|
*/
|
|
145
|
-
writeState(path, state) {
|
|
150
|
+
async writeState(path, state) {
|
|
146
151
|
const target = this.#path(path);
|
|
147
|
-
|
|
152
|
+
await this.#statesFolder(dirname(target));
|
|
148
153
|
if (this.#removeStatesOnExit === undefined) {
|
|
149
154
|
this.#removeStatesOnExit = () => this.removeStates();
|
|
150
155
|
process.once('exit', this.#removeStatesOnExit);
|
|
151
156
|
}
|
|
152
|
-
|
|
157
|
+
const descriptor = openSync(target, 'wx', 0o600);
|
|
158
|
+
try {
|
|
159
|
+
writeAll(descriptor, JSON.stringify(state));
|
|
160
|
+
}
|
|
161
|
+
catch (error) {
|
|
162
|
+
closeSync(descriptor);
|
|
163
|
+
throw withLeftover(error, target, () => rmSync(target, { force: true }));
|
|
164
|
+
}
|
|
165
|
+
closeSync(descriptor);
|
|
166
|
+
}
|
|
167
|
+
// The states folder, made once per run by this process: a folder already there is refused, so no one else's folder
|
|
168
|
+
// ever holds a state. It is kept to this user before anything is made in it; a folder that cannot be is removed.
|
|
169
|
+
#statesFolder(folder) {
|
|
170
|
+
this.#statesReady ??= (async () => {
|
|
171
|
+
mkdirSync(folder, { mode: 0o700 });
|
|
172
|
+
try {
|
|
173
|
+
await keepFolderToUser(folder);
|
|
174
|
+
}
|
|
175
|
+
catch (error) {
|
|
176
|
+
throw withLeftover(error, folder, () => rmSync(folder, { recursive: true, force: true }));
|
|
177
|
+
}
|
|
178
|
+
})().catch((error) => {
|
|
179
|
+
this.#statesReady = undefined;
|
|
180
|
+
throw error;
|
|
181
|
+
});
|
|
182
|
+
return this.#statesReady;
|
|
153
183
|
}
|
|
154
184
|
/** Reads a state `writeState` saved, checked against its schema. */
|
|
155
185
|
readState(path) {
|
|
@@ -163,6 +193,7 @@ export class RunStore {
|
|
|
163
193
|
if (this.#removeStatesOnExit !== undefined)
|
|
164
194
|
process.removeListener('exit', this.#removeStatesOnExit);
|
|
165
195
|
this.#removeStatesOnExit = undefined;
|
|
196
|
+
this.#statesReady = undefined;
|
|
166
197
|
rmSync(this.#path(statesFolder), { recursive: true, force: true });
|
|
167
198
|
}
|
|
168
199
|
/** The absolute path of a file inside the run folder. */
|
|
@@ -201,6 +232,19 @@ function claim(directory) {
|
|
|
201
232
|
throw new RunFolderError(`Retest could not write to the run folder ${directory}: ${errorMessage(error)}`, { cause: error });
|
|
202
233
|
}
|
|
203
234
|
}
|
|
235
|
+
// The failure as it was, with what its cleanup could not remove added to it, so a cleanup that fails never replaces it.
|
|
236
|
+
function withLeftover(error, path, remove) {
|
|
237
|
+
try {
|
|
238
|
+
remove();
|
|
239
|
+
return error;
|
|
240
|
+
}
|
|
241
|
+
catch (removal) {
|
|
242
|
+
const leftover = `${path} is still there: ${errorMessage(removal)}`;
|
|
243
|
+
if (error instanceof PrivateFolderError)
|
|
244
|
+
return new PrivateFolderError(error.message, `${error.detail}; ${leftover}`);
|
|
245
|
+
return new Error(`${errorMessage(error)} ${leftover}`, { cause: error });
|
|
246
|
+
}
|
|
247
|
+
}
|
|
204
248
|
function writeAll(descriptor, text) {
|
|
205
249
|
const bytes = Buffer.from(text, 'utf8');
|
|
206
250
|
let offset = 0;
|
package/media/Cargo.lock
CHANGED
|
@@ -184,6 +184,7 @@ dependencies = [
|
|
|
184
184
|
"libc",
|
|
185
185
|
"serde",
|
|
186
186
|
"serde_json",
|
|
187
|
+
"windows-sys",
|
|
187
188
|
]
|
|
188
189
|
|
|
189
190
|
[[package]]
|
|
@@ -252,6 +253,21 @@ version = "1.0.26"
|
|
|
252
253
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
253
254
|
checksum = "d245f478577f809a851594d02313b640fb437e0bb33866753cff937863096954"
|
|
254
255
|
|
|
256
|
+
[[package]]
|
|
257
|
+
name = "windows-link"
|
|
258
|
+
version = "0.2.1"
|
|
259
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
260
|
+
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
|
261
|
+
|
|
262
|
+
[[package]]
|
|
263
|
+
name = "windows-sys"
|
|
264
|
+
version = "0.61.2"
|
|
265
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
266
|
+
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
|
|
267
|
+
dependencies = [
|
|
268
|
+
"windows-link",
|
|
269
|
+
]
|
|
270
|
+
|
|
255
271
|
[[package]]
|
|
256
272
|
name = "zlib-rs"
|
|
257
273
|
version = "0.6.8"
|
package/media/Cargo.toml
CHANGED
|
@@ -14,10 +14,25 @@ path = "src/main.rs"
|
|
|
14
14
|
|
|
15
15
|
[dependencies]
|
|
16
16
|
image = { version = "0.25", default-features = false, features = ["png", "jpeg"] }
|
|
17
|
-
libc = "0.2"
|
|
18
17
|
serde = { version = "1", features = ["derive"] }
|
|
19
18
|
serde_json = "1"
|
|
20
19
|
|
|
20
|
+
[target.'cfg(unix)'.dependencies]
|
|
21
|
+
libc = "0.2"
|
|
22
|
+
|
|
23
|
+
# The encoder's job, its members' identities and their ending, the exclusive rename, positional reads of kept frames,
|
|
24
|
+
# the reply handle and Ctrl+C. Win32_Security is only what CreateJobObjectW's signature names.
|
|
25
|
+
[target.'cfg(windows)'.dependencies]
|
|
26
|
+
windows-sys = { version = "0.61", features = [
|
|
27
|
+
"Win32_Foundation",
|
|
28
|
+
"Win32_Security",
|
|
29
|
+
"Win32_Storage_FileSystem",
|
|
30
|
+
"Win32_System_Console",
|
|
31
|
+
"Win32_System_Diagnostics_ToolHelp",
|
|
32
|
+
"Win32_System_JobObjects",
|
|
33
|
+
"Win32_System_Threading",
|
|
34
|
+
] }
|
|
35
|
+
|
|
21
36
|
[features]
|
|
22
37
|
# Counts heap allocations and prints them at exit, for measuring one route against another. Never in a shipped build.
|
|
23
38
|
allocation-counts = []
|
package/media/src/encoder.rs
CHANGED
|
@@ -9,7 +9,10 @@
|
|
|
9
9
|
use std::collections::VecDeque;
|
|
10
10
|
use std::ffi::OsString;
|
|
11
11
|
use std::io::{self, Read};
|
|
12
|
+
#[cfg(unix)]
|
|
12
13
|
use std::os::unix::process::CommandExt;
|
|
14
|
+
#[cfg(windows)]
|
|
15
|
+
use std::os::windows::process::CommandExt;
|
|
13
16
|
use std::path::Path;
|
|
14
17
|
use std::process::{Child, Command, ExitStatus, Stdio};
|
|
15
18
|
use std::sync::{Arc, Mutex};
|
|
@@ -205,8 +208,39 @@ pub fn encode_arguments(
|
|
|
205
208
|
arguments
|
|
206
209
|
}
|
|
207
210
|
|
|
211
|
+
/// The variables an encoder inherits: where programs and temporary files are. Windows programs also expect the
|
|
212
|
+
/// system folder's variable, and name their temporary folder differently.
|
|
213
|
+
#[cfg(unix)]
|
|
214
|
+
const ENCODER_VARIABLES: [&str; 2] = ["PATH", "TMPDIR"];
|
|
215
|
+
#[cfg(windows)]
|
|
216
|
+
const ENCODER_VARIABLES: [&str; 4] = ["PATH", "SystemRoot", "TEMP", "TMP"];
|
|
217
|
+
|
|
218
|
+
/// Starts the encoder and the ownership that will record it. On Unix the encoder leads a process group of its own; on
|
|
219
|
+
/// Windows it is created suspended and runs only once it is in a job of its own, so everything it starts is in that job
|
|
220
|
+
/// from birth. A Windows encoder that cannot be put in a job is ended through its own handle before it ever ran.
|
|
221
|
+
pub fn spawn_owned(
|
|
222
|
+
ffmpeg: &Path,
|
|
223
|
+
arguments: &[OsString],
|
|
224
|
+
stdin: Stdio,
|
|
225
|
+
stdout: Stdio,
|
|
226
|
+
) -> io::Result<(Child, Ownership)> {
|
|
227
|
+
let mut child = spawn_in_group(ffmpeg, arguments, stdin, stdout)?;
|
|
228
|
+
#[cfg(unix)]
|
|
229
|
+
let held: io::Result<Ownership> = Ok(Ownership::new(child.id()));
|
|
230
|
+
#[cfg(windows)]
|
|
231
|
+
let held: io::Result<Ownership> = Ownership::hold(&mut child);
|
|
232
|
+
match held {
|
|
233
|
+
Ok(ownership) => Ok((child, ownership)),
|
|
234
|
+
Err(error) => {
|
|
235
|
+
let _ = child.kill();
|
|
236
|
+
let _ = child.wait();
|
|
237
|
+
Err(error)
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
208
242
|
/// Starts the encoder in its own process group. Ownership is recorded separately before any cleanup signal.
|
|
209
|
-
|
|
243
|
+
fn spawn_in_group(
|
|
210
244
|
ffmpeg: &Path,
|
|
211
245
|
arguments: &[OsString],
|
|
212
246
|
stdin: Stdio,
|
|
@@ -217,16 +251,29 @@ pub fn spawn_in_group(
|
|
|
217
251
|
if ffmpeg.components().count() > 1 {
|
|
218
252
|
std::fs::metadata(ffmpeg)?;
|
|
219
253
|
}
|
|
220
|
-
Command::new(ffmpeg)
|
|
254
|
+
let mut command = Command::new(ffmpeg);
|
|
255
|
+
command
|
|
221
256
|
.env_clear()
|
|
222
257
|
.envs(
|
|
223
|
-
|
|
258
|
+
ENCODER_VARIABLES
|
|
224
259
|
.into_iter()
|
|
225
260
|
.filter_map(|name| std::env::var_os(name).map(|value| (name, value))),
|
|
226
261
|
)
|
|
227
262
|
.env("LC_ALL", "C")
|
|
228
|
-
.args(arguments)
|
|
229
|
-
|
|
263
|
+
.args(arguments);
|
|
264
|
+
#[cfg(unix)]
|
|
265
|
+
command.process_group(0);
|
|
266
|
+
// The encoder gets a console of its own without a window: nothing appears on screen, a Ctrl+C typed in another
|
|
267
|
+
// console never reaches it, so, as on Unix, only the media process ends it, and the programs a wrapper starts
|
|
268
|
+
// share that console and the wrapper's pipes. Without any console, a wrapper's child would get a new console in
|
|
269
|
+
// place of those pipes. Windows signals no process group; the encoder starts suspended instead, so `spawn_owned`
|
|
270
|
+
// can put it in a job before it runs.
|
|
271
|
+
#[cfg(windows)]
|
|
272
|
+
command.creation_flags(
|
|
273
|
+
windows_sys::Win32::System::Threading::CREATE_NO_WINDOW
|
|
274
|
+
| windows_sys::Win32::System::Threading::CREATE_SUSPENDED,
|
|
275
|
+
);
|
|
276
|
+
command
|
|
230
277
|
.stdin(stdin)
|
|
231
278
|
.stdout(stdout)
|
|
232
279
|
.stderr(Stdio::piped())
|
|
@@ -328,14 +375,11 @@ struct Listing {
|
|
|
328
375
|
// what was asked: `asked` names it, such as "its encoders".
|
|
329
376
|
fn listing(ffmpeg: &Path, arguments: &[&str], asked: &str) -> Result<Listing, EncoderFailure> {
|
|
330
377
|
let arguments: Vec<OsString> = arguments.iter().map(OsString::from).collect();
|
|
331
|
-
let mut child =
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
exit: None,
|
|
336
|
-
}
|
|
378
|
+
let (mut child, mut ownership) = spawn_owned(ffmpeg, &arguments, Stdio::null(), Stdio::piped())
|
|
379
|
+
.map_err(|error| EncoderFailure {
|
|
380
|
+
message: format!("{} could not be started: {error}", ffmpeg.display()),
|
|
381
|
+
exit: None,
|
|
337
382
|
})?;
|
|
338
|
-
let mut ownership = Ownership::new(child.id());
|
|
339
383
|
let mut problems = ownership.capture();
|
|
340
384
|
let stdout = child.stdout.take().map(read_bounded);
|
|
341
385
|
let stderr = child.stderr.take().map(read_bounded);
|
package/media/src/frame.rs
CHANGED
|
@@ -781,10 +781,16 @@ mod tests {
|
|
|
781
781
|
let candidates: Vec<std::path::PathBuf> = if path.components().count() > 1 {
|
|
782
782
|
vec![path.to_owned()]
|
|
783
783
|
} else {
|
|
784
|
+
// Windows finds a program on PATH by its name with `.exe`.
|
|
785
|
+
let name = if cfg!(windows) && path.extension().is_none() {
|
|
786
|
+
path.with_extension("exe")
|
|
787
|
+
} else {
|
|
788
|
+
path.to_owned()
|
|
789
|
+
};
|
|
784
790
|
std::env::split_paths(
|
|
785
791
|
&std::env::var_os("PATH").expect("PATH for the ffmpeg prerequisite"),
|
|
786
792
|
)
|
|
787
|
-
.map(|folder| folder.join(
|
|
793
|
+
.map(|folder| folder.join(&name))
|
|
788
794
|
.collect()
|
|
789
795
|
};
|
|
790
796
|
let ffmpeg = candidates
|
package/media/src/main.rs
CHANGED
|
@@ -5,8 +5,10 @@
|
|
|
5
5
|
//! recording's kept frames for an interval, and sends a live view of a recording's newest frame. The protocol is in
|
|
6
6
|
//! `protocol.rs`.
|
|
7
7
|
|
|
8
|
-
#[cfg(not(unix))]
|
|
9
|
-
compile_error!(
|
|
8
|
+
#[cfg(not(any(unix, windows)))]
|
|
9
|
+
compile_error!(
|
|
10
|
+
"retest-media runs on Unix and Windows: it verifies each encoder process before stopping it"
|
|
11
|
+
);
|
|
10
12
|
|
|
11
13
|
#[cfg(feature = "allocation-counts")]
|
|
12
14
|
mod allocations;
|
|
@@ -16,7 +18,10 @@ mod jobs;
|
|
|
16
18
|
mod ledger;
|
|
17
19
|
mod live;
|
|
18
20
|
mod place;
|
|
21
|
+
#[cfg(unix)]
|
|
19
22
|
mod process_ownership;
|
|
23
|
+
#[cfg(windows)]
|
|
24
|
+
mod process_ownership_windows;
|
|
20
25
|
mod protocol;
|
|
21
26
|
mod queue;
|
|
22
27
|
mod recording;
|
|
@@ -25,6 +30,11 @@ mod server;
|
|
|
25
30
|
mod store;
|
|
26
31
|
mod timeline;
|
|
27
32
|
|
|
33
|
+
// Windows has no `ps` and no signals: an encoder is witnessed by a job of its own, and each process in it by its own
|
|
34
|
+
// handle.
|
|
35
|
+
#[cfg(windows)]
|
|
36
|
+
use process_ownership_windows as process_ownership;
|
|
37
|
+
|
|
28
38
|
use std::ffi::OsString;
|
|
29
39
|
use std::io::{self, BufReader};
|
|
30
40
|
use std::path::PathBuf;
|
|
@@ -54,6 +64,14 @@ fn main() -> ExitCode {
|
|
|
54
64
|
return ExitCode::from(2);
|
|
55
65
|
}
|
|
56
66
|
};
|
|
67
|
+
// A media process started in its runner's console would end at the first Ctrl+C typed there, before the runner
|
|
68
|
+
// could finish its recordings. As on Unix, where it is not in the terminal's process group, the runner ends it.
|
|
69
|
+
// Its encoders and finalizers have consoles of their own and hear no Ctrl+C either.
|
|
70
|
+
#[cfg(windows)]
|
|
71
|
+
// SAFETY: a null handler with TRUE only sets this process's own flag to ignore Ctrl+C.
|
|
72
|
+
unsafe {
|
|
73
|
+
windows_sys::Win32::System::Console::SetConsoleCtrlHandler(None, 1);
|
|
74
|
+
}
|
|
57
75
|
let replies = match replies::stdout_pipe().and_then(|output| Replies::new(Box::new(output))) {
|
|
58
76
|
Ok(replies) => replies,
|
|
59
77
|
Err(error) => {
|
package/media/src/place.rs
CHANGED
|
@@ -4,10 +4,16 @@
|
|
|
4
4
|
//! as FAT or exFAT, refuses the link; then the file is copied to `<path>.copying` beside it, synced, and renamed to
|
|
5
5
|
//! the path with a rename that refuses to replace a file, so nothing is ever at the path but a whole file. A process
|
|
6
6
|
//! killed during the copy leaves the `.copying` file, which `leftovers` lists and removes.
|
|
7
|
+
//!
|
|
8
|
+
//! Windows refuses to rename or remove a file while another program holds it open without allowing that, as a virus
|
|
9
|
+
//! scanner reading a new file briefly does. There each move and removal waits a bounded time for the file to be let go,
|
|
10
|
+
//! and a failure names the file as open in another program.
|
|
7
11
|
|
|
8
12
|
use std::fs::{self, File, OpenOptions};
|
|
9
13
|
use std::io::{self, Read, Write};
|
|
10
14
|
use std::path::{Path, PathBuf};
|
|
15
|
+
#[cfg(windows)]
|
|
16
|
+
use std::time::{Duration, Instant};
|
|
11
17
|
|
|
12
18
|
use serde::{Deserialize, Serialize};
|
|
13
19
|
|
|
@@ -51,7 +57,7 @@ pub fn place(partial: &Path, path: &Path) -> io::Result<Placed> {
|
|
|
51
57
|
/// Publishes a complete file without removing its original partial.
|
|
52
58
|
pub fn retain_source(partial: &Path, path: &Path) -> io::Result<Placed> {
|
|
53
59
|
delayed_if_a_test_asks();
|
|
54
|
-
match link(partial, path) {
|
|
60
|
+
match while_in_use(partial, || link(partial, path)) {
|
|
55
61
|
Ok(()) => Ok(Placed::Link),
|
|
56
62
|
Err(error) if links_unavailable(&error) => {
|
|
57
63
|
copy_exclusively(partial, path)?;
|
|
@@ -85,7 +91,7 @@ pub fn finalize_file(partial: &Path, path: &Path) -> Finalization {
|
|
|
85
91
|
}
|
|
86
92
|
}
|
|
87
93
|
match retain_source(partial, path) {
|
|
88
|
-
Ok(placed) => match fs::remove_file(partial) {
|
|
94
|
+
Ok(placed) => match while_in_use(partial, || fs::remove_file(partial)) {
|
|
89
95
|
Ok(()) => Finalization::Placed(placed),
|
|
90
96
|
Err(error) => Finalization::Failed(format!(
|
|
91
97
|
"the video was placed at {}, but its source at {} could not be removed: {error}",
|
|
@@ -118,7 +124,7 @@ fn delayed_if_a_test_asks() {}
|
|
|
118
124
|
#[cfg(debug_assertions)]
|
|
119
125
|
fn link(partial: &Path, path: &Path) -> io::Result<()> {
|
|
120
126
|
if std::env::var_os("RETEST_MEDIA_TEST_NO_HARD_LINKS").is_some() {
|
|
121
|
-
return Err(io::Error::from_raw_os_error(
|
|
127
|
+
return Err(io::Error::from_raw_os_error(NOT_SUPPORTED));
|
|
122
128
|
}
|
|
123
129
|
fs::hard_link(partial, path)
|
|
124
130
|
}
|
|
@@ -135,10 +141,86 @@ fn links_unavailable(error: &io::Error) -> bool {
|
|
|
135
141
|
}
|
|
136
142
|
matches!(
|
|
137
143
|
error.raw_os_error(),
|
|
138
|
-
Some(code) if
|
|
144
|
+
Some(code) if LINKS_UNAVAILABLE.contains(&code)
|
|
139
145
|
)
|
|
140
146
|
}
|
|
141
147
|
|
|
148
|
+
// The codes a failed link or rename gives when the filesystem cannot do it at all, rather than because of what is at
|
|
149
|
+
// the path, as each host numbers them.
|
|
150
|
+
#[cfg(unix)]
|
|
151
|
+
const LINKS_UNAVAILABLE: [i32; 6] = [
|
|
152
|
+
libc::EPERM,
|
|
153
|
+
libc::ENOTSUP,
|
|
154
|
+
libc::EOPNOTSUPP,
|
|
155
|
+
libc::EXDEV,
|
|
156
|
+
libc::EMLINK,
|
|
157
|
+
libc::ENOSYS,
|
|
158
|
+
];
|
|
159
|
+
#[cfg(unix)]
|
|
160
|
+
const RENAME_UNSUPPORTED: [i32; 4] = [libc::ENOTSUP, libc::EOPNOTSUPP, libc::EINVAL, libc::ENOSYS];
|
|
161
|
+
// What the test hook that hides hard links answers.
|
|
162
|
+
#[cfg(all(unix, any(debug_assertions, test)))]
|
|
163
|
+
const NOT_SUPPORTED: i32 = libc::ENOTSUP;
|
|
164
|
+
|
|
165
|
+
// FAT and exFAT answer a hard link with ERROR_INVALID_FUNCTION, a share without links with ERROR_NOT_SUPPORTED.
|
|
166
|
+
#[cfg(windows)]
|
|
167
|
+
const LINKS_UNAVAILABLE: [i32; 5] = [
|
|
168
|
+
windows_sys::Win32::Foundation::ERROR_INVALID_FUNCTION as i32,
|
|
169
|
+
windows_sys::Win32::Foundation::ERROR_NOT_SUPPORTED as i32,
|
|
170
|
+
windows_sys::Win32::Foundation::ERROR_NOT_SAME_DEVICE as i32,
|
|
171
|
+
windows_sys::Win32::Foundation::ERROR_TOO_MANY_LINKS as i32,
|
|
172
|
+
windows_sys::Win32::Foundation::ERROR_CALL_NOT_IMPLEMENTED as i32,
|
|
173
|
+
];
|
|
174
|
+
#[cfg(windows)]
|
|
175
|
+
const RENAME_UNSUPPORTED: [i32; 3] = [
|
|
176
|
+
windows_sys::Win32::Foundation::ERROR_INVALID_FUNCTION as i32,
|
|
177
|
+
windows_sys::Win32::Foundation::ERROR_NOT_SUPPORTED as i32,
|
|
178
|
+
windows_sys::Win32::Foundation::ERROR_CALL_NOT_IMPLEMENTED as i32,
|
|
179
|
+
];
|
|
180
|
+
#[cfg(all(windows, any(debug_assertions, test)))]
|
|
181
|
+
const NOT_SUPPORTED: i32 = windows_sys::Win32::Foundation::ERROR_NOT_SUPPORTED as i32;
|
|
182
|
+
|
|
183
|
+
/// How long a move or removal waits for another program to let go of a file on Windows.
|
|
184
|
+
#[cfg(windows)]
|
|
185
|
+
const IN_USE_WAIT: Duration = Duration::from_secs(2);
|
|
186
|
+
|
|
187
|
+
// Runs a move or removal of `path` again while another program holds the file open against it, up to `IN_USE_WAIT`.
|
|
188
|
+
// A file still held then is named as open in another program.
|
|
189
|
+
#[cfg(windows)]
|
|
190
|
+
fn while_in_use<T>(path: &Path, mut operation: impl FnMut() -> io::Result<T>) -> io::Result<T> {
|
|
191
|
+
use windows_sys::Win32::Foundation::{ERROR_LOCK_VIOLATION, ERROR_SHARING_VIOLATION};
|
|
192
|
+
let started = Instant::now();
|
|
193
|
+
loop {
|
|
194
|
+
match operation() {
|
|
195
|
+
Err(error)
|
|
196
|
+
if matches!(
|
|
197
|
+
error.raw_os_error(),
|
|
198
|
+
Some(code) if code == ERROR_SHARING_VIOLATION as i32 || code == ERROR_LOCK_VIOLATION as i32
|
|
199
|
+
) =>
|
|
200
|
+
{
|
|
201
|
+
if started.elapsed() >= IN_USE_WAIT {
|
|
202
|
+
return Err(io::Error::new(
|
|
203
|
+
error.kind(),
|
|
204
|
+
format!(
|
|
205
|
+
"{} is open in another program, which kept it from being moved or removed for {} s: {error}",
|
|
206
|
+
path.display(),
|
|
207
|
+
IN_USE_WAIT.as_secs()
|
|
208
|
+
),
|
|
209
|
+
));
|
|
210
|
+
}
|
|
211
|
+
std::thread::sleep(Duration::from_millis(50));
|
|
212
|
+
}
|
|
213
|
+
done => return done,
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// A Unix file that is open can always be renamed and removed.
|
|
219
|
+
#[cfg(unix)]
|
|
220
|
+
fn while_in_use<T>(_: &Path, mut operation: impl FnMut() -> io::Result<T>) -> io::Result<T> {
|
|
221
|
+
operation()
|
|
222
|
+
}
|
|
223
|
+
|
|
142
224
|
fn copy_exclusively(partial: &Path, path: &Path) -> io::Result<()> {
|
|
143
225
|
if path.symlink_metadata().is_ok() {
|
|
144
226
|
return Err(io::Error::from(io::ErrorKind::AlreadyExists));
|
|
@@ -165,7 +247,12 @@ fn copy_exclusively(partial: &Path, path: &Path) -> io::Result<()> {
|
|
|
165
247
|
// operation is supported, placement fails with the destination named and keeps the source. Ordinary rename is
|
|
166
248
|
// never used because it can replace a foreign file between a check and publication.
|
|
167
249
|
fn rename_without_replacing(from: &Path, to: &Path) -> io::Result<()> {
|
|
168
|
-
rename_using(
|
|
250
|
+
rename_using(
|
|
251
|
+
from,
|
|
252
|
+
to,
|
|
253
|
+
|from, to| while_in_use(from, || exclusive_rename(from, to)),
|
|
254
|
+
|| {},
|
|
255
|
+
)
|
|
169
256
|
}
|
|
170
257
|
|
|
171
258
|
fn rename_using(
|
|
@@ -178,7 +265,7 @@ fn rename_using(
|
|
|
178
265
|
Err(error)
|
|
179
266
|
if matches!(
|
|
180
267
|
error.raw_os_error(),
|
|
181
|
-
Some(code) if
|
|
268
|
+
Some(code) if RENAME_UNSUPPORTED.contains(&code)
|
|
182
269
|
) =>
|
|
183
270
|
{
|
|
184
271
|
if to.symlink_metadata().is_ok() {
|
|
@@ -233,7 +320,34 @@ fn exclusive_rename(from: &Path, to: &Path) -> io::Result<()> {
|
|
|
233
320
|
}
|
|
234
321
|
}
|
|
235
322
|
|
|
236
|
-
|
|
323
|
+
// MoveFileExW without MOVEFILE_REPLACE_EXISTING fails when anything is at the destination, the same refusal as
|
|
324
|
+
// RENAME_EXCL and RENAME_NOREPLACE.
|
|
325
|
+
#[cfg(windows)]
|
|
326
|
+
fn exclusive_rename(from: &Path, to: &Path) -> io::Result<()> {
|
|
327
|
+
let (from, to) = (wide_path(from)?, wide_path(to)?);
|
|
328
|
+
// SAFETY: MoveFileExW takes two NUL-terminated UTF-16 paths that live across the call, and no flags.
|
|
329
|
+
let moved = unsafe {
|
|
330
|
+
windows_sys::Win32::Storage::FileSystem::MoveFileExW(from.as_ptr(), to.as_ptr(), 0)
|
|
331
|
+
};
|
|
332
|
+
if moved != 0 {
|
|
333
|
+
Ok(())
|
|
334
|
+
} else {
|
|
335
|
+
Err(io::Error::last_os_error())
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
#[cfg(windows)]
|
|
340
|
+
fn wide_path(path: &Path) -> io::Result<Vec<u16>> {
|
|
341
|
+
use std::os::windows::ffi::OsStrExt;
|
|
342
|
+
let mut wide: Vec<u16> = path.as_os_str().encode_wide().collect();
|
|
343
|
+
if wide.contains(&0) {
|
|
344
|
+
return Err(io::Error::from(io::ErrorKind::InvalidInput));
|
|
345
|
+
}
|
|
346
|
+
wide.push(0);
|
|
347
|
+
Ok(wide)
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
#[cfg(not(any(target_os = "macos", target_os = "linux", windows)))]
|
|
237
351
|
fn exclusive_rename(_: &Path, _: &Path) -> io::Result<()> {
|
|
238
352
|
Err(io::Error::from_raw_os_error(libc::ENOTSUP))
|
|
239
353
|
}
|
|
@@ -259,8 +373,9 @@ fn copy_in_chunks(source: &mut File, target: &mut File) -> io::Result<()> {
|
|
|
259
373
|
}
|
|
260
374
|
}
|
|
261
375
|
|
|
262
|
-
// A test sets this to prove what a process killed in the middle of a copy leaves behind. Release builds have no hook
|
|
263
|
-
|
|
376
|
+
// A test sets this to prove what a process killed in the middle of a copy leaves behind. Release builds have no hook,
|
|
377
|
+
// and neither has Windows, where no test asks.
|
|
378
|
+
#[cfg(all(debug_assertions, unix))]
|
|
264
379
|
fn killed_if_a_test_asks() {
|
|
265
380
|
if std::env::var_os("RETEST_MEDIA_TEST_KILL_DURING_COPY").is_some() {
|
|
266
381
|
// SAFETY: kill(2) on this process's own pid; SIGKILL leaves exactly what a kill from outside would.
|
|
@@ -270,7 +385,7 @@ fn killed_if_a_test_asks() {
|
|
|
270
385
|
}
|
|
271
386
|
}
|
|
272
387
|
|
|
273
|
-
#[cfg(not(debug_assertions))]
|
|
388
|
+
#[cfg(not(all(debug_assertions, unix)))]
|
|
274
389
|
fn killed_if_a_test_asks() {}
|
|
275
390
|
|
|
276
391
|
#[cfg(test)]
|
|
@@ -349,7 +464,7 @@ mod tests {
|
|
|
349
464
|
let result = rename_using(
|
|
350
465
|
©ing,
|
|
351
466
|
&path,
|
|
352
|
-
|_, _| Err(io::Error::from_raw_os_error(
|
|
467
|
+
|_, _| Err(io::Error::from_raw_os_error(NOT_SUPPORTED)),
|
|
353
468
|
|| {
|
|
354
469
|
barrier.wait();
|
|
355
470
|
barrier.wait();
|
|
@@ -391,6 +506,7 @@ mod tests {
|
|
|
391
506
|
let _ = fs::remove_dir_all(folder);
|
|
392
507
|
}
|
|
393
508
|
|
|
509
|
+
#[cfg(unix)]
|
|
394
510
|
#[test]
|
|
395
511
|
fn only_link_refusals_choose_the_copy() {
|
|
396
512
|
for code in [libc::EPERM, libc::ENOTSUP, libc::EXDEV, libc::EMLINK] {
|
|
@@ -406,4 +522,107 @@ mod tests {
|
|
|
406
522
|
);
|
|
407
523
|
}
|
|
408
524
|
}
|
|
525
|
+
|
|
526
|
+
#[cfg(windows)]
|
|
527
|
+
#[test]
|
|
528
|
+
fn only_link_refusals_choose_the_copy_on_windows() {
|
|
529
|
+
use windows_sys::Win32::Foundation::{
|
|
530
|
+
ERROR_ACCESS_DENIED, ERROR_ALREADY_EXISTS, ERROR_DISK_FULL, ERROR_FILE_NOT_FOUND,
|
|
531
|
+
ERROR_INVALID_FUNCTION, ERROR_NOT_SAME_DEVICE, ERROR_NOT_SUPPORTED,
|
|
532
|
+
ERROR_TOO_MANY_LINKS,
|
|
533
|
+
};
|
|
534
|
+
for code in [
|
|
535
|
+
ERROR_INVALID_FUNCTION,
|
|
536
|
+
ERROR_NOT_SUPPORTED,
|
|
537
|
+
ERROR_NOT_SAME_DEVICE,
|
|
538
|
+
ERROR_TOO_MANY_LINKS,
|
|
539
|
+
] {
|
|
540
|
+
assert!(
|
|
541
|
+
links_unavailable(&io::Error::from_raw_os_error(code as i32)),
|
|
542
|
+
"{code}"
|
|
543
|
+
);
|
|
544
|
+
}
|
|
545
|
+
for code in [
|
|
546
|
+
ERROR_ALREADY_EXISTS,
|
|
547
|
+
ERROR_FILE_NOT_FOUND,
|
|
548
|
+
ERROR_ACCESS_DENIED,
|
|
549
|
+
ERROR_DISK_FULL,
|
|
550
|
+
] {
|
|
551
|
+
assert!(
|
|
552
|
+
!links_unavailable(&io::Error::from_raw_os_error(code as i32)),
|
|
553
|
+
"{code}"
|
|
554
|
+
);
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
// Opens `path` as a program that lets others read it but neither rename nor remove it, and lets go after `hold`.
|
|
559
|
+
#[cfg(windows)]
|
|
560
|
+
fn held_open(path: &Path, hold: std::time::Duration) -> std::thread::JoinHandle<()> {
|
|
561
|
+
use std::os::windows::fs::OpenOptionsExt;
|
|
562
|
+
let file = OpenOptions::new()
|
|
563
|
+
.read(true)
|
|
564
|
+
.share_mode(windows_sys::Win32::Storage::FileSystem::FILE_SHARE_READ)
|
|
565
|
+
.open(path)
|
|
566
|
+
.expect("the other program opens the file");
|
|
567
|
+
std::thread::spawn(move || {
|
|
568
|
+
std::thread::sleep(hold);
|
|
569
|
+
drop(file);
|
|
570
|
+
})
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
#[cfg(windows)]
|
|
574
|
+
#[test]
|
|
575
|
+
fn a_partial_another_program_holds_open_is_placed_once_it_is_let_go() {
|
|
576
|
+
let folder = folder("held-briefly");
|
|
577
|
+
let path = folder.join("video.mp4");
|
|
578
|
+
let partial = partial_path_of(&path);
|
|
579
|
+
fs::write(&partial, b"video").expect("written");
|
|
580
|
+
let other = held_open(&partial, std::time::Duration::from_millis(400));
|
|
581
|
+
let placed = finalize_file(&partial, &path);
|
|
582
|
+
other.join().expect("the other program lets go");
|
|
583
|
+
assert!(
|
|
584
|
+
matches!(placed, Finalization::Placed(Placed::Link)),
|
|
585
|
+
"{placed:?}"
|
|
586
|
+
);
|
|
587
|
+
assert_eq!(fs::read(&path).expect("there"), b"video");
|
|
588
|
+
assert!(!partial.exists());
|
|
589
|
+
let _ = fs::remove_dir_all(folder);
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
#[cfg(windows)]
|
|
593
|
+
#[test]
|
|
594
|
+
fn a_partial_held_open_past_the_wait_is_named_as_open_in_another_program() {
|
|
595
|
+
let folder = folder("held-long");
|
|
596
|
+
let path = folder.join("video.mp4");
|
|
597
|
+
let partial = partial_path_of(&path);
|
|
598
|
+
fs::write(&partial, b"video").expect("written");
|
|
599
|
+
let other = held_open(&partial, IN_USE_WAIT + std::time::Duration::from_secs(1));
|
|
600
|
+
let placed = finalize_file(&partial, &path);
|
|
601
|
+
other.join().expect("the other program lets go");
|
|
602
|
+
let Finalization::Failed(message) = placed else {
|
|
603
|
+
panic!("a source still held open is not removed: {placed:?}");
|
|
604
|
+
};
|
|
605
|
+
assert!(message.contains("is open in another program"), "{message}");
|
|
606
|
+
assert!(
|
|
607
|
+
message.contains(&partial.display().to_string()),
|
|
608
|
+
"{message}"
|
|
609
|
+
);
|
|
610
|
+
assert_eq!(fs::read(&path).expect("the video is in place"), b"video");
|
|
611
|
+
let _ = fs::remove_dir_all(folder);
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
#[cfg(windows)]
|
|
615
|
+
#[test]
|
|
616
|
+
fn a_copy_another_program_holds_open_is_renamed_once_it_is_let_go() {
|
|
617
|
+
let folder = folder("copy-held");
|
|
618
|
+
let path = folder.join("video.mp4");
|
|
619
|
+
let copying = copying_path_of(&path);
|
|
620
|
+
fs::write(©ing, b"whole").expect("written");
|
|
621
|
+
let other = held_open(©ing, std::time::Duration::from_millis(400));
|
|
622
|
+
rename_without_replacing(©ing, &path).expect("renamed once let go");
|
|
623
|
+
other.join().expect("the other program lets go");
|
|
624
|
+
assert_eq!(fs::read(&path).expect("there"), b"whole");
|
|
625
|
+
assert!(!copying.exists());
|
|
626
|
+
let _ = fs::remove_dir_all(folder);
|
|
627
|
+
}
|
|
409
628
|
}
|