OXYGENHUB

oh works in PowerShell: ohub.exe and install.ps1, which respects the execution policy (closes #319); the images page tells peer-held images apart (refs #342); a queued job's reason names CPUs 5d66f27cebe2

Author
Andy McCall @andymccall <[email protected]>
via
agent agent:claude@andymccall <[email protected]> · keyless, under your grant for changes,refs,seal · key 0caf71edf284
When
· 2026-10-27 06:04 UTC
Signature
◈ sealed by agent:claude@andymccall
Hash
5d66f27cebe2e2bdcfb22950ada5ebc843691d1a1c98bc854533907d1045723f
Depends on
0468394734e9 42fa95540468 58cbf42447bc 6ca0b971ff07 757195ad7a5d 99ca93d60bca a7e6e8e6094c b5668108487f b6b7e4cf6b24 d23464c0ac60 f2c9757605c1
Depended on by
7c572789349b 84f09ad45851
In bookmarks
main
In tags
v0.9.11 v0.9.15 v0.9.16 v0.9.19 v0.9.20 v0.9.21 v0.9.22 v0.9.23 v0.9.24 v0.9.9
Edits
16 atoms, 2 tree operations, 8 files

Browse the tree at this change

added packaging/windows/install.ps1 +54 −0
1 +
# Install oh for this Windows user (oxygenhub#319). Run it from the
2 +
# unpacked zip:   powershell -ExecutionPolicy Bypass -File .\install.ps1
3 +
#
4 +
# - Copies oh.exe and ohub.exe to %LOCALAPPDATA%\Programs\oh and puts that
5 +
#   folder on this user's PATH.
6 +
# - PowerShell has a built-in alias `oh` (Out-Host) that hides oh.exe. Where
7 +
#   PowerShell runs scripts, this removes the alias in your profile; under
8 +
#   Windows' default policy (Restricted) a profile cannot run at all, so it
9 +
#   leaves that alone and you use `ohub`, which always works. It never
10 +
#   changes your execution policy.
11 +
# Nothing here needs administrator rights; run it again to update.
12 +
$ErrorActionPreference = 'Stop'
13 +
$here = Split-Path -Parent $MyInvocation.MyCommand.Path
14 +
$dest = Join-Path $env:LOCALAPPDATA 'Programs\oh'
15 +
New-Item -ItemType Directory -Force -Path $dest | Out-Null
16 +
foreach ($exe in 'oh.exe', 'ohub.exe') {
17 +
    Copy-Item -Force (Join-Path $here $exe) (Join-Path $dest $exe)
18 +
}
19 +
20 +
$path = [Environment]::GetEnvironmentVariable('Path', 'User')
21 +
$parts = @($path -split ';' | Where-Object { $_ -ne '' })
22 +
if ($parts -notcontains $dest) {
23 +
    [Environment]::SetEnvironmentVariable('Path', (($parts + $dest) -join ';'), 'User')
24 +
    Write-Host "Added $dest to your PATH (new terminals see it)."
25 +
}
26 +
27 +
$marker = '# oh: PowerShell''s alias `oh` (Out-Host) hides oh.exe (oxygenhub#319)'
28 +
$line = 'Remove-Item Alias:oh -Force -ErrorAction SilentlyContinue'
29 +
$profilePath = $PROFILE.CurrentUserAllHosts
30 +
# A profile runs only where scripts may: under Restricted (Windows' default)
31 +
# or AllSigned (an unsigned profile) it would print an error in every new
32 +
# PowerShell instead, and the alias would stay.
33 +
# The policy a new PowerShell gets, not this one's (run with -ExecutionPolicy
34 +
# Bypass, this process may run anything): the first scope that sets one,
35 +
# else Restricted, the client default.
36 +
$policy = 'Restricted'
37 +
foreach ($scope in 'MachinePolicy', 'UserPolicy', 'CurrentUser', 'LocalMachine') {
38 +
    $p = Get-ExecutionPolicy -Scope $scope
39 +
    if ("$p" -ne 'Undefined') { $policy = "$p"; break }
40 +
}
41 +
if ($policy -in 'RemoteSigned', 'Unrestricted', 'Bypass') {
42 +
    if (-not (Test-Path $profilePath)) {
43 +
        New-Item -ItemType File -Force -Path $profilePath | Out-Null
44 +
    }
45 +
    if (-not (Select-String -Path $profilePath -SimpleMatch -Quiet -Pattern $line)) {
46 +
        Add-Content -Path $profilePath -Value "`r`n$marker`r`n$line"
47 +
        Write-Host "Your PowerShell profile ($profilePath) now removes the oh alias."
48 +
    }
49 +
    Remove-Item Alias:oh -Force -ErrorAction SilentlyContinue
50 +
    Write-Host "oh is installed: open a new terminal and run 'oh --version' (or 'ohub --version')."
51 +
} else {
52 +
    Write-Host "oh is installed: open a new terminal and run 'ohub --version'."
53 +
    Write-Host "PowerShell runs no scripts here (execution policy $policy), so its alias 'oh' (Out-Host) stays: use 'ohub', or 'oh.exe'. Command Prompt runs 'oh' as it is."
54 +
}
modified CHANGELOG.md +3 −0
…
3 unchanged lines
4 4
5 5
## Unreleased
6 6
7 +
- **`oh` works in PowerShell (#319).** PowerShell has a built-in alias `oh` (for `Out-Host`), so `oh --version` ran that and failed. The Windows zip now carries `ohub.exe`, the same program under a name nothing hides, and `install.ps1`, which installs both for the current user and puts them on `PATH`. Where PowerShell runs scripts it also removes the alias in the profile; under Windows' default policy, where a profile cannot run and would only print an error, it says to use `ohub` and never changes the policy. Tried on the Windows VM under both policies.
8 +
- **Admin › Runners › Images tells a peer-held image from the hub's own (#342).** An image only runners hold shows as "manifest" (sent by which runner) rather than "hub", and one no runner holds now is marked nowhere, as an image the hub lacks is.
9 +
- **A queued job's reason names CPUs too.** A runner with a free slot but not the room for a job was said to lack "the memory or disk"; earth-1's limit is often its CPUs (5, with a 4-vCPU job running), so it says "the memory, CPUs or disk".
7 10
- **The Content Security Policy is enforced (#227).** The pages carry no inline scripts or handlers, and a week of reports showed it would block only Cloudflare's Web Analytics beacon, which the policy now allows (`static.cloudflareinsights.com`, reports to `cloudflareinsights.com`). Violations are still reported and logged.
8 11
- **Where a job's time went (oxygenflow#35).** A runner's finish report carries its phases (image, prepare, run, finish); the run page shows them under each job, and `/metrics` has `oxyhub_flow_job_phase_seconds` by phase. Migration 0099 (`flow_jobs.phases`).
9 12
…
363 unchanged lines
modified README.md +1 −1
…
42 unchanged lines
43 43
- **Debian, Ubuntu and other Linux with apt** (x86_64 or arm64): download `oh_VERSION_amd64.deb` (or `_arm64.deb`) and run `sudo apt install ./oh_VERSION_amd64.deb`. It needs glibc 2.28 or later (Debian 10, Ubuntu 20.04 and newer).
44 44
- **Other Linux:** unpack `oh-VERSION-linux-x86_64.tar.gz` (or `-linux-arm64`) and put `oh` on your `PATH`.
45 45
- **macOS** (Apple Silicon or Intel): unpack `oh-VERSION-macos-arm64.tar.gz` (or `-macos-x86_64`), then `xattr -d com.apple.quarantine oh`, since the build is not notarised, and put it on your `PATH`.
46 -
- **Windows:** unzip `oh-VERSION-windows-x86_64.zip` and put the folder on your `PATH`. SmartScreen may warn on first run, since the build is not signed.
46 +
- **Windows:** unzip `oh-VERSION-windows-x86_64.zip` and run `powershell -ExecutionPolicy Bypass -File .\install.ps1` in the folder: it installs for you alone, puts `oh` on your `PATH`, and removes PowerShell's built-in alias `oh` (for `Out-Host`) in your profile, which would otherwise run instead. `ohub` is the same program under a name nothing hides. SmartScreen may warn on first run, since the build is not signed.
47 47
48 48
Sign in with `oh auth login --with-hub https://oxygenhub.io`; the commands are in `docs/oh.md`. `oh` works alongside `oxy`, from [Oxygen's releases](https://oxygenhub.io/andymccall/oxygen/releases).
49 49
…
46 unchanged lines
modified crates/hub/src/flow.rs +1 −1
…
1713 unchanged lines
1714 1714
        if n >= slots {
1715 1715
            busy.push(format!("{} busy ({n} of {slots})", r.name));
1716 1716
        } else {
1717 -
            busy.push(format!("{} has a free slot ({n} of {slots}) but not the memory or disk for it now", r.name));
1717 +
            busy.push(format!("{} has a free slot ({n} of {slots}) but not the memory, CPUs or disk for it now", r.name));
1718 1718
        }
1719 1719
    }
1720 1720
    let mut parts = Vec::new();
…
1338 unchanged lines
modified crates/hub/src/web/runners.rs +14 −2
…
569 unchanged lines
570 570
571 571
impl ImageRow {
572 572
    /// Nowhere a job could get it: not on the hub, on no runner.
573 +
    /// Nowhere a job could get it: nobody holds it, and the hub has at most
574 +
    /// its manifest (oxygenflow#94).
573 575
    pub fn stranded(&self) -> bool {
574 -
        self.places.is_empty()
576 +
        self.places.iter().all(|p| p.kind == "peer")
575 577
    }
576 578
}
577 579
…
70 unchanged lines
648 650
            r.arch = k.arch.clone();
649 651
        }
650 652
        r.sent = Some((k.uploader.clone(), k.created_ms));
651 -
        r.places.push(Place { name: "hub".into(), kind: "hub", title: format!("{} chunks kept by the hub", k.chunks) });
653 +
        // An image only runners hold (oxygenflow#94): the hub keeps its
654 +
        // manifest, and the runners holding it serve its chunks.
655 +
        r.places.push(if k.source == "peer" {
656 +
            Place {
657 +
                name: "manifest".into(),
658 +
                kind: "peer",
659 +
                title: format!("The hub keeps only its manifest, sent by {}; runners holding it serve its {} chunks", k.holder, k.chunks),
660 +
            }
661 +
        } else {
662 +
            Place { name: "hub".into(), kind: "hub", title: format!("{} chunks kept by the hub", k.chunks) }
663 +
        });
652 664
    }
653 665
    let mut swarm = Vec::new();
654 666
    for runner in &list {
…
39 unchanged lines
modified crates/hub/tests/flow.rs +6 −1
…
3047 unchanged lines
3048 3048
    sqlx::query("UPDATE flow_runners SET max_jobs = 2 WHERE id = $1").bind(r.id).execute(&h.app.db).await.unwrap();
3049 3049
    oxygenhub::flow::explain_waits(&h.app).await.unwrap();
3050 3050
    let build = db::flow::jobs(&h.app.db, run.id).await.unwrap().into_iter().find(|j| j.name == "build").unwrap();
3051 -
    assert!(build.waiting.contains("earth-9 has a free slot (1 of 2) but not the memory or disk for it now"), "{}", build.waiting);
3051 +
    assert!(build.waiting.contains("earth-9 has a free slot (1 of 2) but not the memory, CPUs or disk for it now"), "{}", build.waiting);
3052 3052
}
3053 3053
3054 3054
/// #291: expediting a run places the queue again at once, and a job it
…
724 unchanged lines
3779 3779
    let cookie = res.headers().get(header::SET_COOKIE).unwrap().to_str().unwrap().split(';').next().unwrap().to_string();
3780 3780
    let carol = db::accounts::account_by_name(&h.app.db, "carol").await.unwrap().unwrap();
3781 3781
    db::accounts::set_admin(&h.app.db, carol.id, true).await.unwrap();
3782 +
    // An image only runners held (oxygenflow#94), and none holds it now.
3783 +
    let lost = jet::images::scan(&[7u8; 4096][..], Some("x86_64".into()), |_, _| Ok(())).unwrap();
3784 +
    oxygenhub::images::record_peer(&h.app.db, &lost, "mars-1").await.unwrap();
3782 3785
    let res = oxygenhub::router(h.app.clone())
3783 3786
        .oneshot(Request::get("/admin/runners/images").header(header::COOKIE, cookie).body(Body::empty()).unwrap())
3784 3787
        .await
…
11 unchanged lines
3796 3799
    assert!(on_runner.contains("img-holds") && on_runner.contains("earth-1") && !on_runner.contains("nowhere"), "{on_runner}");
3797 3800
    let on_hub = row(&kept);
3798 3801
    assert!(on_hub.contains("img-hub") && on_hub.contains("1.0 MiB") && on_hub.contains("x86_64"), "{on_hub}");
3802 +
    let peer_only = row(&lost.hash);
3803 +
    assert!(peer_only.contains("img-peer") && peer_only.contains("sent by mars-1") && peer_only.contains("nowhere"), "{peer_only}");
3799 3804
    assert!(page.contains("earth-1 (home)"), "the swarm is named");
3800 3805
    // The swarm backdrop (docs/backgrounds.md §2): the hub and the runners.
3801 3806
    assert!(page.contains(r#"{"images":"#) && page.contains(r#""scene":"swarm""#) && page.contains(r#""earth-1""#), "the swarm's data");
…
3183 unchanged lines
modified scripts/dist.sh +5 −0
…
65 unchanged lines
66 66
  cp "$built" "$stage/"
67 67
  cp "${DOCS[@]}" "$stage/"
68 68
  if [[ "$triple" == *windows* ]]; then
69 +
    # PowerShell's built-in alias `oh` (Out-Host) hides oh.exe (#319):
70 +
    # ohub.exe is the same program under a name nothing hides, and
71 +
    # install.ps1 puts both on PATH and removes the alias in the profile.
72 +
    cp "$built" "$stage/ohub.exe"
73 +
    cp packaging/windows/install.ps1 "$stage/"
69 74
    (cd "$OUT/stage" && zip -qr "../$name.zip" "$name")
70 75
  else
71 76
    tar -C "$OUT/stage" -czf "$OUT/$name.tar.gz" "$name"
…
61 unchanged lines
modified templates/style.css +1 −0
…
1148 unchanged lines
1149 1149
.lbl.img-hub { color: var(--acc); border-color: var(--acc); }
1150 1150
.lbl.img-holds { color: var(--ok); border-color: var(--ok); }
1151 1151
.lbl.img-fetching { color: var(--warn); border-color: var(--warn); }
1152 +
.lbl.img-peer { color: var(--acc); border-color: var(--acc); border-style: dashed; }
1152 1153
1153 1154
/* Diffusion (#150): each consumer's status at a glance. */
1154 1155
.df-chip { display: inline-block; font-size: 12px; padding: 1px 8px; border-radius: 10px; border: 1px solid; white-space: nowrap; }
…
315 unchanged lines