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 |