wsb 0.258.6441

Workspace tools for every project under $PRJ: git across submodules, template stamping, GitHub, and the workbench
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
//! What the operating systems disagree about, named once.
//!
//! Every function here replaced a `use std::os::unix::…` at a call site. There
//! were sixty-one of them, and each one made the whole binary refuse to build
//! for Windows — which is how a workspace that declares a Windows test target
//! ended up with a bootstrap that could never finish: the guest allocates, the
//! toolchain installs, cargo starts, and the build stops on `cannot find unix
//! in os` before a single test runs.
//!
//! The rule here is that every platform gets a REAL implementation, not a
//! no-op standing in for one. Windows has no permission bits, so "make this
//! executable" is genuinely nothing there — that is a fact about Windows and
//! it is written down as one. But "restrict this to its owner" is a thing
//! Windows can do, so it does it, with `icacls`, and fails loudly when it
//! cannot; a silent success on a private key would be worse than no support at
//! all.
//!
//! Where a concept has no counterpart at all — a signal that killed a process,
//! on a system without signals — the function says so in its return type
//! rather than inventing an answer.

use std::path::Path;
use std::process::ExitStatus;

// ---------------------------------------------------------------------------
// The executable bit
// ---------------------------------------------------------------------------

/// Can this path be run?
///
/// Unix asks the permission bits. Windows has none, and decides by extension —
/// the same rule the shell uses to find a command, so a file this says yes to
/// is a file `CreateProcess` will start.

/// Where a process that listens on a local socket keeps its directory.
///
/// gpg-agent creates Unix sockets below its home, and a socket path is capped
/// by `sockaddr_un.sun_path`: under macOS's long `TMPDIR` a suffixed socket
/// overflowed it and key generation failed before an agent could start. `/tmp`
/// is the Unix short-path namespace for exactly this.
#[cfg(unix)]
pub fn short_ipc_dir() -> std::path::PathBuf {
    std::path::PathBuf::from("/tmp")
}

/// Windows has no `/tmp` — a gpg home asked for there failed with "the system
/// cannot find the path specified" — and no `sun_path` limit to keep under:
/// the system temporary directory is the answer.
#[cfg(windows)]
pub fn short_ipc_dir() -> std::path::PathBuf {
    std::env::temp_dir()
}

/// A path as this workspace NAMES one: its components joined with `/`, on
/// every platform.
///
/// For names, not for opening files. A workspace-relative path in a report, a
/// recording or scenario someone types back, a key in a declaration — each is
/// an identifier, and `Path::display` spells it with the platform's separator:
/// `fabricloom\floom-engine\Cargo.toml` on Windows, which matches nothing
/// declared and is not what anyone types. A leading root stays a leading `/`.
pub fn slash_path(path: &Path) -> String {
    path.components()
        .map(|component| match component {
            std::path::Component::RootDir => String::new(),
            other => other.as_os_str().to_string_lossy().into_owned(),
        })
        .collect::<Vec<_>>()
        .join("/")
}

/// A path made absolute and resolved — in the form paths are joined onto.
///
/// Through this, never `std::fs::canonicalize` directly, for any path
/// something is later appended to. On Windows the standard call answers in
/// the extended-length VERBATIM form, `\\?\C:\…`, and a verbatim path gets no
/// separator translation: `root.join("capdag/capdag-js/node_modules")` names
/// ONE component with slashes in it, which exists nowhere. `wsb clear` read
/// every declared build tree that way and reported nothing to remove while
/// they sat on disk; `wsb stubs` reported a script missing that was there.
///
/// Stripped rather than kept: the prefix exists to exceed MAX_PATH, and a
/// workspace root that long has worse problems than this.
pub fn canonicalize(path: impl AsRef<Path>) -> std::io::Result<std::path::PathBuf> {
    std::fs::canonicalize(path).map(|resolved| without_verbatim_prefix(&resolved))
}

/// `\\?\C:\x` as `C:\x`, and `\\?\UNC\server\share` as `\\server\share`. Any
/// other path, on any platform, unchanged.
pub fn without_verbatim_prefix(path: &Path) -> std::path::PathBuf {
    let shown = path.to_string_lossy();
    match shown.strip_prefix(r"\\?\UNC\") {
        Some(rest) => std::path::PathBuf::from(format!(r"\\{rest}")),
        None => match shown.strip_prefix(r"\\?\") {
            Some(rest) => std::path::PathBuf::from(rest),
            None => path.to_path_buf(),
        },
    }
}

#[cfg(unix)]
pub fn is_executable(path: &Path) -> bool {
    use std::os::unix::fs::PermissionsExt;
    std::fs::metadata(path)
        .map(|m| m.is_file() && m.permissions().mode() & 0o111 != 0)
        .unwrap_or(false)
}

#[cfg(windows)]
pub fn is_executable(path: &Path) -> bool {
    if !std::fs::metadata(path).map(|m| m.is_file()).unwrap_or(false) {
        return false;
    }
    let Some(extension) = path.extension().and_then(|e| e.to_str()) else {
        return false;
    };
    let extension = extension.to_ascii_lowercase();
    ["exe", "com", "bat", "cmd", "ps1"].contains(&extension.as_str())
}

/// Keep a disposable directory out of the machine's backups.
///
/// Build output, snapshots and caches are rebuilt from source, and backing
/// them up filled a Mac's disk with local Time Machine snapshots of
/// gigabytes nobody would restore. On macOS the exclusion is sticky (`tmutil
/// addexclusion`): stored on the folder itself, so it is lost when the folder
/// is removed and made again — which is why wsb marks a directory when it
/// creates one, not once by hand. Elsewhere there is no backup this knows of,
/// and nothing to do.
#[cfg(target_os = "macos")]
pub fn exclude_from_backups(path: &Path) -> Result<(), String> {
    if excluded_from_backups(path)? {
        return Ok(());
    }
    let marked = crate::git_backend::command_without_repo_scope("tmutil")
        .arg("addexclusion")
        .arg(path)
        .output()
        .map_err(|e| format!("running tmutil addexclusion {}: {e}", path.display()))?;
    if marked.status.success() {
        Ok(())
    } else {
        Err(format!(
            "tmutil could not exclude {} from backups: {}",
            path.display(),
            String::from_utf8_lossy(&marked.stderr).trim()
        ))
    }
}

#[cfg(not(target_os = "macos"))]
pub fn exclude_from_backups(_path: &Path) -> Result<(), String> {
    Ok(())
}

/// Whether the machine's backups leave this directory out. True where there
/// is no backup this knows of.
#[cfg(target_os = "macos")]
pub fn excluded_from_backups(path: &Path) -> Result<bool, String> {
    let asked = crate::git_backend::command_without_repo_scope("tmutil")
        .arg("isexcluded")
        .arg(path)
        .output()
        .map_err(|e| format!("asking tmutil about {}: {e}", path.display()))?;
    Ok(String::from_utf8_lossy(&asked.stdout).contains("[Excluded]"))
}

#[cfg(not(target_os = "macos"))]
pub fn excluded_from_backups(_path: &Path) -> Result<bool, String> {
    Ok(true)
}

/// Make this path runnable.
///
/// On Windows this is nothing, and nothing is the correct answer: there is no
/// bit to set. What makes a file runnable there is its extension, which
/// whoever wrote the file already chose.
#[cfg(unix)]
pub fn make_executable(path: &Path) -> std::io::Result<()> {
    use std::os::unix::fs::PermissionsExt;
    std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o755))
}

#[cfg(windows)]
pub fn make_executable(path: &Path) -> std::io::Result<()> {
    // The file still has to BE there: this is called after staging a binary,
    // and a missing one is the failure that call is checking for. Returning
    // the io error unchanged is what lets the callers that check for
    // `NotFound` keep checking for it.
    std::fs::metadata(path).map(|_| ())
}

/// The file names a bare command could be, in the order to try them.
///
/// Just the name on Unix. On Windows a command is a FILE with an extension —
/// `cargo` is `cargo.exe`, `rustup` may be `rustup.exe`, and a `npm` is a
/// `npm.cmd` — so a search for the bare name finds nothing and reports every
/// tool on the machine missing.
#[cfg(unix)]
pub fn command_file_names(name: &str) -> Vec<String> {
    vec![name.to_string()]
}

#[cfg(windows)]
pub fn command_file_names(name: &str) -> Vec<String> {
    // An extension already given is respected: `which("python.exe")` is asking
    // for that file and not for `python.exe.exe`.
    if std::path::Path::new(name).extension().is_some() {
        return vec![name.to_string()];
    }
    ["exe", "cmd", "bat", "com", "ps1"]
        .iter()
        .map(|extension| format!("{name}.{extension}"))
        .collect()
}

// ---------------------------------------------------------------------------
// Owner-only access
// ---------------------------------------------------------------------------

/// Let nobody but this account read or write this path.
///
/// This is for private keys and the directories that hold them, so a platform
/// that cannot do it must say so rather than return success. Windows can:
/// `icacls` drops inheritance and grants the current account alone, which is
/// the same statement 0600 makes.
#[cfg(unix)]
pub fn restrict_to_owner(path: &Path) -> std::io::Result<()> {
    use std::os::unix::fs::PermissionsExt;
    let mode = if path.is_dir() { 0o700 } else { 0o600 };
    std::fs::set_permissions(path, std::fs::Permissions::from_mode(mode))
}

#[cfg(windows)]
pub fn restrict_to_owner(path: &Path) -> std::io::Result<()> {
    let account = std::env::var("USERNAME").map_err(|_| {
        std::io::Error::other("USERNAME is not set, so there is no account to grant")
    })?;
    // Through this crate's own process type, like every other child: its
    // output is packaged with the rest of the run rather than written to
    // whatever terminal the process happens to have. `icacls` refusing is
    // something an operator has to be able to read afterwards.
    // A directory's grant is INHERITABLE — object and container inherit — so
    // what is created inside it is owner-only from the moment it exists. On
    // Windows that is the whole of 0700's meaning that survives: a folder
    // nobody else can list does not stop them opening a file in it by name,
    // because traversal is not checked, so a child that inherited the
    // folder's old entries was readable however private the folder was.
    let grant = if path.is_dir() {
        format!("{account}:(OI)(CI)(F)")
    } else {
        format!("{account}:(F)")
    };
    let output = crate::output::Command::new("icacls")
        .arg(path)
        .arg("/inheritance:r")
        .arg("/grant:r")
        .arg(grant)
        .output()?;
    if !output.status.success() {
        return Err(std::io::Error::other(format!(
            "restricting {} to its owner failed: {}",
            path.display(),
            String::from_utf8_lossy(&output.stderr).trim()
        )));
    }
    Ok(())
}

/// Set a file's mode, for the callers that carry one.
///
/// A mode is a Unix statement, and most of the ones here say one of two
/// things: "this is a program" or "this is private". Windows keeps whichever
/// of those it can — a mode with nothing granted to group or other is an
/// owner-only file, and that is an access rule Windows has — and lets the
/// execute bits go, because it has no such bit and decides by extension.
///
/// The file must exist either way. A mode set on a path with nothing at it is
/// a caller that staged nothing, and that is the failure it was checking for.
#[cfg(unix)]
pub fn set_mode(path: &Path, mode: u32) -> std::io::Result<()> {
    use std::os::unix::fs::PermissionsExt;
    std::fs::set_permissions(path, std::fs::Permissions::from_mode(mode))
}

#[cfg(windows)]
pub fn set_mode(path: &Path, mode: u32) -> std::io::Result<()> {
    std::fs::metadata(path)?;
    if mode & 0o077 == 0 {
        return restrict_to_owner(path);
    }
    Ok(())
}

// ---------------------------------------------------------------------------
// Links
// ---------------------------------------------------------------------------

/// A second name for an existing file.
///
/// A symbolic link on Unix. On Windows a symbolic link needs a privilege an
/// ordinary account does not have, so this makes a HARD link instead: a second
/// name for the same file, which is what the callers here want — a stable path
/// whose content is the current run's.
#[cfg(unix)]
pub fn link_to(target: &Path, link: &Path) -> std::io::Result<()> {
    std::os::unix::fs::symlink(target, link)
}

#[cfg(windows)]
pub fn link_to(target: &Path, link: &Path) -> std::io::Result<()> {
    std::fs::hard_link(target, link)
}

/// A symbolic link at `link` naming `target` exactly as given, relative or
/// not: a copy of a link a working tree holds. On Windows that needs the
/// privilege to make links, and its absence is the error returned.
#[cfg(unix)]
pub fn symlink_file(target: &Path, link: &Path) -> std::io::Result<()> {
    std::os::unix::fs::symlink(target, link)
}

#[cfg(windows)]
pub fn symlink_file(target: &Path, link: &Path) -> std::io::Result<()> {
    std::os::windows::fs::symlink_file(target, link)
}

/// Is this path a socket somebody bound?
///
/// Windows has no filesystem sockets, so nothing there ever is one. The sweep
/// that asks this is looking for leftovers of a Unix-domain listener, and on
/// Windows there are none to find.
#[cfg(unix)]
pub fn is_a_socket(path: &Path) -> bool {
    use std::os::unix::fs::FileTypeExt;
    std::fs::metadata(path)
        .map(|m| m.file_type().is_socket())
        .unwrap_or(false)
}

#[cfg(windows)]
pub fn is_a_socket(_path: &Path) -> bool {
    false
}

// ---------------------------------------------------------------------------
// Processes
// ---------------------------------------------------------------------------

/// Every process on this machine, as (pid, command line).
///
/// One pass over the whole table rather than one per pattern, because the
/// caller matches a union.
///
/// `ps` on Unix — not `pgrep`, which on macOS has no flag to print argv beside
/// the pid — and CIM on Windows, because `tasklist` cannot print a command
/// line and the command line is the entire question.
#[cfg(unix)]
pub fn processes() -> Result<Vec<(i32, String)>, String> {
    let output = crate::git_backend::command_without_repo_scope("ps")
        .args(["-axww", "-o", "pid=,command="])
        .output()
        .map_err(|e| format!("cannot enumerate processes via ps: {e}"))?;
    if !output.status.success() {
        return Err("ps failed to enumerate processes".to_string());
    }
    Ok(parse_table(&String::from_utf8_lossy(&output.stdout)))
}

#[cfg(windows)]
pub fn processes() -> Result<Vec<(i32, String)>, String> {
    let output = crate::git_backend::command_without_repo_scope("powershell")
        .args([
            "-NoProfile",
            "-NonInteractive",
            "-Command",
            "Get-CimInstance Win32_Process | ForEach-Object { \
             \"$($_.ProcessId) $($_.CommandLine)\" }",
        ])
        .output()
        .map_err(|e| format!("cannot enumerate processes: {e}"))?;
    if !output.status.success() {
        return Err(format!(
            "enumerating processes failed: {}",
            String::from_utf8_lossy(&output.stderr).trim()
        ));
    }
    Ok(parse_table(&String::from_utf8_lossy(&output.stdout)))
}

/// One `<pid> <command line>` per line, in either dialect.
///
/// A line with no pid is skipped rather than guessed at: `ps` prints a header
/// on some systems, and CIM prints a blank line for a process whose command
/// line this account cannot read.
///
/// So is a line with a pid and NOTHING else, which is the same case: Windows
/// returns the row for a protected process with an empty `CommandLine`, and a
/// process with no command line is one nothing here can identify — the callers
/// match on the command line, and handing them a process they cannot recognise
/// is handing them a pid to guess about.
fn parse_table(table: &str) -> Vec<(i32, String)> {
    let mut out = Vec::new();
    for line in table.lines() {
        let line = line.trim_start();
        let Some((pid, argv)) = line.split_once(' ') else {
            continue;
        };
        let Ok(pid) = pid.trim().parse::<i32>() else {
            continue;
        };
        let argv = argv.trim();
        if argv.is_empty() {
            continue;
        }
        out.push((pid, argv.to_string()));
    }
    out
}

/// The process that started this one, when the platform will say.
#[cfg(unix)]
pub fn parent_process_id() -> Option<i32> {
    // SAFETY: getppid(2) takes no arguments and cannot fail.
    Some(unsafe { libc::getppid() })
}

#[cfg(windows)]
pub fn parent_process_id() -> Option<i32> {
    let output = crate::git_backend::command_without_repo_scope("powershell")
        .args([
            "-NoProfile",
            "-NonInteractive",
            "-Command",
            &format!(
                "(Get-CimInstance Win32_Process -Filter \"ProcessId={}\").ParentProcessId",
                std::process::id()
            ),
        ])
        .output()
        .ok()?;
    String::from_utf8_lossy(&output.stdout).trim().parse().ok()
}

/// Ask a process to stop, giving it the chance to flush.
///
/// Windows has no SIGTERM. `taskkill` without `/F` posts WM_CLOSE and asks a
/// console process to end, which is the closest thing it has to the request
/// this is; what it is NOT is the hammer, and the caller sends that separately
/// to whatever survived.
#[cfg(unix)]
pub fn end_politely(pid: i32) {
    // SAFETY: kill(2) on a pid read from the process table; ESRCH for a
    // process that already exited is not an error worth acting on.
    unsafe {
        libc::kill(pid, libc::SIGTERM);
    }
}

/// Windows has no SIGTERM. It has two polite requests, for the two kinds of
/// process there are, and both are made:
///
/// - Ctrl-Break to the process group the pid leads: what a console process —
///   a suite, a fixture's daemon, anything started `in_its_own_group` beside
///   this one's console — receives as Ctrl-Break, which [`stop_flag`] turns
///   into an orderly stop.
/// - `taskkill` without `/F`: a request to close the process's WINDOWS, which
///   is what a windowed program (an Electron app) understands.
///
/// This used to make only the second. A console or background process has no
/// window, so on a Windows guest nothing wsb asked politely ever stopped: the
/// fixtures' daemons "would not stop, and are still holding a port" in every
/// test that started one (run-e1c18d9651284802, 19 Oct 2026). A process that
/// neither reaches — a detached one, which has no console — is stopped through
/// its own channel or by [`end_hard`], as every caller here escalates to.
#[cfg(windows)]
pub fn end_politely(pid: i32) {
    #[link(name = "kernel32")]
    extern "system" {
        fn GenerateConsoleCtrlEvent(dwCtrlEvent: u32, dwProcessGroupId: u32) -> i32;
    }
    const CTRL_BREAK_EVENT: u32 = 1;
    // SAFETY: takes two integers and touches no memory of this process. It
    // fails, harmlessly, for a pid that leads no group on this console.
    unsafe {
        GenerateConsoleCtrlEvent(CTRL_BREAK_EVENT, pid as u32);
    }
    let _ = crate::git_backend::command_without_repo_scope("taskkill")
        .args(["/PID", &pid.to_string()])
        .stdout(std::process::Stdio::null())
        .stderr(std::process::Stdio::null())
        .status();
}

/// End a process now, with nothing it can do about it.
#[cfg(unix)]
pub fn end_hard(pid: i32) {
    // SAFETY: as above.
    unsafe {
        libc::kill(pid, libc::SIGKILL);
    }
}

#[cfg(windows)]
pub fn end_hard(pid: i32) {
    let _ = crate::git_backend::command_without_repo_scope("taskkill")
        .args(["/F", "/PID", &pid.to_string()])
        .stdout(std::process::Stdio::null())
        .stderr(std::process::Stdio::null())
        .status();
}

/// End a process AND everything it started.
///
/// A test suite is a tree — cargo, rustc, the linker, and whatever they forked
/// — and killing the one at the top leaves the rest running, holding the
/// target directory and the port the next run wants.
///
/// Unix reaches them through the process group, which is why the child was put
/// in one of its own; Windows has no such group and reaches them through the
/// tree, which `taskkill /T` walks. `polite` gets the chance to flush.
#[cfg(unix)]
pub fn end_group(pid: u32, polite: bool) {
    // Each child called setsid, so its pid IS its process group id and the
    // negative form reaches every descendant.
    // SAFETY: kill(2) with a pid we spawned; an already-dead group returns
    // ESRCH, which is not an error worth acting on here.
    unsafe {
        libc::kill(
            -(pid as i32),
            if polite { libc::SIGTERM } else { libc::SIGKILL },
        );
    }
}

#[cfg(windows)]
pub fn end_group(pid: u32, polite: bool) {
    let mut command = crate::git_backend::command_without_repo_scope("taskkill");
    if !polite {
        command.arg("/F");
    }
    let _ = command
        .args(["/T", "/PID", &pid.to_string()])
        .stdout(std::process::Stdio::null())
        .stderr(std::process::Stdio::null())
        .status();
}

/// Keep this process's own standard handles out of a child it detaches.
///
/// A detached child — the daemon — outlives whatever started it, so anything
/// it inherits by accident it holds for its whole life. On Windows a child
/// inherits EVERY inheritable handle of its parent, not only the three it is
/// given for stdio, and a process's standard handles are inheritable: they are
/// how it got them. So a daemon started from a git hook held the hook's
/// stdout — the pipe back to git — for as long as it ran, the pipe never
/// reached end-of-file, and whoever was reading git's output waited for ever.
/// `git commit` from a test hung until the suite was killed
/// (`test11544_the_second_commit_still_works`, on a Windows guest).
///
/// Clearing the flag changes nothing for any other child: std duplicates a
/// standard handle as inheritable whenever a child is told to inherit it.
///
/// Unix needs nothing. A child's standard descriptors are the ones it is
/// given, and std opens everything else close-on-exec.
#[cfg(unix)]
pub fn withhold_standard_handles() -> std::io::Result<()> {
    Ok(())
}

#[cfg(windows)]
pub fn withhold_standard_handles() -> std::io::Result<()> {
    use std::ffi::c_void;
    const STD_INPUT_HANDLE: u32 = -10i32 as u32;
    const STD_OUTPUT_HANDLE: u32 = -11i32 as u32;
    const STD_ERROR_HANDLE: u32 = -12i32 as u32;
    const HANDLE_FLAG_INHERIT: u32 = 0x0000_0001;
    const INVALID_HANDLE_VALUE: isize = -1;
    #[link(name = "kernel32")]
    extern "system" {
        fn GetStdHandle(which: u32) -> *mut c_void;
        fn SetHandleInformation(handle: *mut c_void, mask: u32, flags: u32) -> i32;
    }
    for which in [STD_INPUT_HANDLE, STD_OUTPUT_HANDLE, STD_ERROR_HANDLE] {
        // SAFETY: GetStdHandle takes a constant and returns this process's own
        // handle, or null / INVALID_HANDLE_VALUE when there is none.
        // SetHandleInformation changes only that handle's inherit flag.
        unsafe {
            let handle = GetStdHandle(which);
            if handle.is_null() || handle as isize == INVALID_HANDLE_VALUE {
                continue;
            }
            if SetHandleInformation(handle, HANDLE_FLAG_INHERIT, 0) == 0 {
                return Err(std::io::Error::last_os_error());
            }
        }
    }
    Ok(())
}

/// Is this pid still a process?
#[cfg(unix)]
pub fn alive(pid: i32) -> bool {
    // Signal 0 performs the permission and existence checks without delivering
    // anything, which is exactly the question.
    // SAFETY: as above.
    unsafe { libc::kill(pid, 0) == 0 }
}

#[cfg(windows)]
pub fn alive(pid: i32) -> bool {
    crate::git_backend::command_without_repo_scope("tasklist")
        .args(["/FI", &format!("PID eq {pid}"), "/NH", "/FO", "CSV"])
        .output()
        .map(|out| String::from_utf8_lossy(&out.stdout).contains(&format!("\"{pid}\"")))
        .unwrap_or(false)
}

/// The flag that is set when the operator asks this process to stop.
///
/// Ctrl-C and a `kill` on Unix, Ctrl-C and Ctrl-Break on Windows. The handler
/// does nothing but store into an AtomicBool, which is what makes it legal in
/// a signal handler and what makes the two platforms able to mean the same
/// thing by it — the run notices on its next loop and stops what it started.
///
/// ONE flag for the process, handed to everyone who asks. It took a flag from
/// its caller and kept the first: a second caller's flag was never set by
/// anything, and it waited on a stop it could not be told about.
///
/// Asking for the flag is what takes the signals away from their default,
/// which ends the process. Whoever asks must therefore see that the process
/// does end: see [`crate::ops::process::end_when_stopped`].
#[cfg(unix)]
pub fn stop_requested() -> std::sync::Arc<std::sync::atomic::AtomicBool> {
    use std::sync::OnceLock;
    static FLAG: OnceLock<std::sync::Arc<std::sync::atomic::AtomicBool>> = OnceLock::new();
    extern "C" fn on_signal(_: i32) {
        if let Some(flag) = FLAG.get() {
            flag.store(true, std::sync::atomic::Ordering::SeqCst);
        }
    }
    let mut installed_now = false;
    let flag = FLAG.get_or_init(|| {
        installed_now = true;
        std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false))
    });
    if installed_now {
        // SAFETY: the handler does nothing but store into an AtomicBool, which
        // is async-signal-safe. Anything richer — printing, allocating,
        // unwinding — would be undefined behaviour in a handler.
        unsafe {
            libc::signal(libc::SIGINT, on_signal as *const () as libc::sighandler_t);
            libc::signal(libc::SIGTERM, on_signal as *const () as libc::sighandler_t);
        }
    }
    std::sync::Arc::clone(flag)
}

#[cfg(windows)]
pub fn stop_requested() -> std::sync::Arc<std::sync::atomic::AtomicBool> {
    use std::sync::OnceLock;
    static FLAG: OnceLock<std::sync::Arc<std::sync::atomic::AtomicBool>> = OnceLock::new();
    let mut installed_now = false;
    let flag = FLAG.get_or_init(|| {
        installed_now = true;
        std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false))
    });
    if !installed_now {
        return std::sync::Arc::clone(flag);
    }
    // The console control handler is Windows' Ctrl-C, and it runs on a thread
    // of its own rather than on an interrupted one — so the only rule it has
    // to keep is the same one: touch nothing but the flag.
    unsafe extern "system" fn on_control(_event: u32) -> i32 {
        if let Some(flag) = FLAG.get() {
            flag.store(true, std::sync::atomic::Ordering::SeqCst);
        }
        // Handled. Returning false here would let the default handler end the
        // process outright, which is the thing this exists to prevent: a run
        // that is killed mid-write leaves a log nobody can diagnose.
        1
    }
    unsafe extern "system" {
        fn SetConsoleCtrlHandler(
            handler: Option<unsafe extern "system" fn(u32) -> i32>,
            add: i32,
        ) -> i32;
    }
    // SAFETY: a plain FFI call registering a handler that only stores into an
    // AtomicBool. A failure here means the console has no handler list — a
    // service with no console — and the run simply does not get told.
    unsafe {
        SetConsoleCtrlHandler(Some(on_control), 1);
    }
    std::sync::Arc::clone(flag)
}

/// Which signal ended this, if a signal did.
///
/// Windows has no signals, so the answer there is always that none did — which
/// is the truth, not a stand-in for one.
#[cfg(unix)]
pub fn killed_by_signal(status: &ExitStatus) -> Option<i32> {
    use std::os::unix::process::ExitStatusExt;
    status.signal()
}

#[cfg(windows)]
pub fn killed_by_signal(_status: &ExitStatus) -> Option<i32> {
    None
}

/// The account this process runs as, as a string for substitution.
///
/// A numeric uid on Unix, because that is what the paths being built expect.
/// Windows has no uid; it has a name, which is what its paths are built from.
#[cfg(unix)]
pub fn account() -> String {
    // SAFETY: getuid cannot fail and has no side effects.
    unsafe { libc::getuid() }.to_string()
}

#[cfg(windows)]
pub fn account() -> String {
    std::env::var("USERNAME").unwrap_or_default()
}

#[cfg(test)]
mod tests {
    use super::*;

    /// TEST11435: the process table is read the same way on either platform.
    ///
    /// `ps` and CIM print the same shape — a pid, a space, the rest of the
    /// line — and the parser is shared so that a change to one cannot quietly
    /// mean something different in the other. A line with no pid is skipped
    /// rather than guessed at: `ps` prints a header on some systems and CIM
    /// prints blank lines for processes whose command line is not readable.
    #[test]
    fn test11435_the_process_table_parses_the_same_either_way() {
        let table = "  1234 /usr/bin/cargo test --release\n\
                     5678 C:\\Program Files\\thing.exe --flag\n\
                     \n\
                     PID COMMAND\n\
                     91011 \n";
        let parsed = parse_table(table);
        assert_eq!(
            parsed,
            vec![
                (1234, "/usr/bin/cargo test --release".to_string()),
                (5678, "C:\\Program Files\\thing.exe --flag".to_string()),
            ],
            "a header line and a process with no command line are not processes"
        );
    }

    /// TEST11436: a file this says is runnable is one the platform will run.
    ///
    /// The two platforms answer from different evidence — permission bits and
    /// an extension — and the thing that must not happen is either of them
    /// saying yes to a file that cannot be started. A directory is the case
    /// that used to slip through: on Unix it has the execute bit set, and it
    /// is not a program.
    #[test]
    fn test11436_only_a_runnable_file_is_executable() {
        let dir = tempfile::tempdir().expect("a temporary directory");
        assert!(
            !is_executable(dir.path()),
            "a directory carries the execute bit and is not a program"
        );

        let plain = dir.path().join("notes.txt");
        std::fs::write(&plain, b"text").expect("the file");
        assert!(!is_executable(&plain), "a text file is not a program");

        let program = dir.path().join(if cfg!(windows) { "tool.exe" } else { "tool" });
        std::fs::write(&program, b"#!/bin/sh\n").expect("the file");
        #[cfg(unix)]
        assert!(
            !is_executable(&program),
            "on unix the bit is what decides, and it is not set yet"
        );
        make_executable(&program).expect("making it executable");
        assert!(is_executable(&program), "it is a program now");

        assert!(
            !is_executable(&dir.path().join("absent")),
            "a path with nothing at it is not a program"
        );
    }

    /// TEST12163: a resolved path is one things can be joined onto.
    ///
    /// The verbatim prefix `canonicalize` answers with on Windows turns off
    /// separator translation, so a workspace root in that form made every
    /// `/`-joined declared path — `capdag/capdag-js/node_modules` — name
    /// nothing, and `wsb clear` listed no build tree at all on a Windows guest.
    #[test]
    fn test12163_a_resolved_path_carries_no_verbatim_prefix() {
        use std::path::{Path, PathBuf};
        assert_eq!(
            without_verbatim_prefix(Path::new(r"\\?\C:\mg-test\workspace")),
            PathBuf::from(r"C:\mg-test\workspace")
        );
        assert_eq!(
            without_verbatim_prefix(Path::new(r"\\?\UNC\server\share\ws")),
            PathBuf::from(r"\\server\share\ws"),
            "a UNC share keeps its double backslash"
        );
        assert_eq!(
            without_verbatim_prefix(Path::new("/Users/me/ws")),
            PathBuf::from("/Users/me/ws"),
            "any other path is unchanged"
        );
        // And the resolving call itself answers in that form, here and now.
        let dir = tempfile::tempdir().expect("a temporary directory");
        let resolved = canonicalize(dir.path()).expect("resolving it");
        assert!(!resolved.to_string_lossy().starts_with(r"\\?\"), "{}", resolved.display());
        assert!(resolved.is_absolute());
    }

    /// TEST12912: nothing resolves a path except through `canonicalize` here.
    ///
    /// The rule above was written down and not kept: sixty-three calls went to
    /// the standard one directly, in wsb, the daemon and their tests. On a
    /// Windows guest their `\\?\` paths reached git, which cannot use them
    /// (`git worktree add \\?\C:\…` failed in every snapshot test), and were
    /// compared with plain ones (`Some("C:\…")` against `Some("\\?\C:\…")`).
    /// So it is a property of the source, checked here: a call that bypasses
    /// this module fails on the Mac, before a guest ever runs it.
    #[test]
    fn test12912_no_path_is_resolved_around_this_module() {
        fn walk(dir: &Path, out: &mut Vec<std::path::PathBuf>) {
            let entries =
                std::fs::read_dir(dir).unwrap_or_else(|e| panic!("cannot read {}: {e}", dir.display()));
            for entry in entries {
                let path = entry.expect("a directory entry").path();
                if path.is_dir() {
                    walk(&path, out);
                } else if path.extension().is_some_and(|e| e == "rs") {
                    out.push(path);
                }
            }
        }
        let crate_root = Path::new(env!("CARGO_MANIFEST_DIR"));
        let mut files = Vec::new();
        for tree in ["src", "tests", "daemon/src"] {
            walk(&crate_root.join(tree), &mut files);
        }
        assert!(files.len() > 100, "the sources should have been walked, found {}", files.len());

        let this = crate_root.join("src").join("platform.rs");
        let mut offences = Vec::new();
        for file in files.iter().filter(|f| **f != this) {
            let text = std::fs::read_to_string(file).expect("a source file must be readable");
            for (number, line) in text.lines().enumerate() {
                let code = line.split("//").next().unwrap_or_default();
                let bypasses = code.contains(".canonicalize()")
                    || code.match_indices("fs::canonicalize(").any(|(at, _)| !code[..at].ends_with("platform::"));
                if bypasses {
                    offences.push(format!("{}:{}: {}", file.display(), number + 1, line.trim()));
                }
            }
        }
        assert!(
            offences.is_empty(),
            "resolve paths with crate::platform::canonicalize (wsb::platform::canonicalize outside \
             the crate), which never answers in the verbatim form:\n{}",
            offences.join("\n")
        );
    }
}