feat(remote): let a remote workspace target a Windows host over SSH (#1041)

A remote workspace refused any machine that was not Linux or macOS. This
teaches the installer and the link to reach a Windows OpenSSH host:

- Detection: `uname -sm` is still asked first, unchanged. When it fails
  (cmd.exe / PowerShell) or answers from Git for Windows, MSYS2 or Cygwin,
  a PowerShell probe reads PROCESSOR_ARCHITECTURE. The detected platform
  must match the SFTP home's shape, so a WSL DefaultShell over Windows
  SFTP is refused instead of installing a Linux server at /C:/...
- Assets: tty7-server-windows-{x86_64,aarch64}.exe, verified against
  checksums.txt like every other server; built by new server-windows
  jobs in release.yml and nightly.yml (ARM64 leg non-blocking), and the
  CI vcruntime guard now covers tty7-server.exe.
- Install: %USERPROFILE%\AppData\Local\tty7\bin\tty7-server-cXpY.exe via
  SFTP (/C:/... spelling). No mode bits are required or set. A running
  image is renamed aside to free its name and swept on the next install.
- Commands: every Windows command is a PowerShell script sent as
  -EncodedCommand, which survives cmd.exe, PowerShell and bash as the
  DefaultShell. The daemon is launched through Win32_Process.Create so
  it outlives the SSH session's job object, with this session's
  environment handed over; restarts use a new `tty7-server --stop`.
- Link: a Windows server is always reached by session exec with a plain
  `"C:\...\tty7-server-cXpY.exe" --stdio` (no env probe, no
  stream-local forward).
- Server: `--stdio` (control and --pane) now bridges on Windows to the
  daemon's loopback TCP endpoints instead of refusing.

Tested against a fake Windows host in install/windows_tests.rs; not yet
run against a real Windows machine.
This commit is contained in:
l0ng-ai
2026-09-30 16:36:04 +08:00
committed by GitHub
parent 8efea021e3
commit 53f661e1a1
12 changed files with 2051 additions and 110 deletions
+2 -1
View File
@@ -180,7 +180,8 @@ jobs:
run: |
& ./.github/scripts/assert-no-vcruntime.ps1 `
"target/${{ matrix.target }}/debug/tty7-app.exe" `
"target/${{ matrix.target }}/debug/tty7.exe"
"target/${{ matrix.target }}/debug/tty7.exe" `
"target/${{ matrix.target }}/debug/tty7-server.exe"
# `cargo test` has no timeout of its own, so one hung test is
# indistinguishable from a slow suite until the job hits GitHub's six-hour
+62 -1
View File
@@ -422,11 +422,72 @@ jobs:
path: tty7/dist/${{ matrix.asset }}
if-no-files-found: error
# Mirrors release.yml's server-windows job; keep the two in sync when editing.
# No RUSTFLAGS, for the reason spelled out there: it would drop `+crt-static`.
server-windows:
needs: plan
if: needs.plan.outputs.build == 'true'
strategy:
fail-fast: false
matrix:
include:
- target: x86_64-pc-windows-msvc
asset: tty7-server-windows-x86_64.exe
experimental: false
- target: aarch64-pc-windows-msvc
asset: tty7-server-windows-aarch64.exe
experimental: true
runs-on: windows-latest
continue-on-error: ${{ matrix.experimental }}
env:
CARGO_PROFILE_RELEASE_STRIP: symbols
steps:
- name: Checkout tty7
uses: actions/checkout@v4
with:
path: tty7
- name: Stamp nightly version
working-directory: tty7
shell: bash
run: bash .github/scripts/stamp-version.sh "${{ needs.plan.outputs.version }}"
- uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.target }}
- uses: Swatinem/rust-cache@v2
with:
workspaces: tty7
key: ${{ matrix.target }}
- name: Build tty7-server
working-directory: tty7
run: cargo build --release -p tty7-server --target ${{ matrix.target }}
- name: Assert the binary needs no VC++ redistributable
working-directory: tty7
shell: pwsh
run: '& ./.github/scripts/assert-no-vcruntime.ps1 "target/${{ matrix.target }}/release/tty7-server.exe"'
- name: Stage the asset
working-directory: tty7
shell: pwsh
run: |
New-Item -ItemType Directory -Force dist | Out-Null
Copy-Item "target/${{ matrix.target }}/release/tty7-server.exe" "dist/${{ matrix.asset }}"
- uses: actions/upload-artifact@v7
with:
name: nightly-${{ matrix.asset }}
path: tty7/dist/${{ matrix.asset }}
if-no-files-found: error
# Single publish step after all platforms succeed, so the rolling release is
# always complete — a failed platform means tonight's nightly is skipped
# entirely and users keep yesterday's, never a partial asset set.
publish:
needs: [plan, build, server-musl, server-macos]
needs: [plan, build, server-musl, server-macos, server-windows]
runs-on: ubuntu-latest
env:
GH_TOKEN: ${{ github.token }}
+67 -1
View File
@@ -377,6 +377,72 @@ jobs:
path: tty7/dist/${{ matrix.asset }}
if-no-files-found: error
# The server for remote Windows hosts (Windows OpenSSH), under the same
# contract: flat, version-free names that `install::asset` derives from the
# host's `PROCESSOR_ARCHITECTURE`. They keep `.exe` because the installer
# writes the file under that name, and the checksum line is looked up by it.
#
# No RUSTFLAGS here, unlike the other server jobs: setting it would replace
# `.cargo/config.toml`'s `+crt-static` wholesale, and a server that imports
# VCRUNTIME140.dll will not start on a Windows host without the Visual C++
# redistributable — which a headless server is the likeliest machine to lack.
# Symbols are stripped through the profile instead.
#
# ARM64 is new ground for this repository's Windows builds (the app itself
# ships x64 only), so its leg may fail without holding up the release; an
# ARM64 host then gets a clear "could not download" instead of a server.
server-windows:
strategy:
fail-fast: false
matrix:
include:
- target: x86_64-pc-windows-msvc
asset: tty7-server-windows-x86_64.exe
experimental: false
- target: aarch64-pc-windows-msvc
asset: tty7-server-windows-aarch64.exe
experimental: true
runs-on: windows-latest
continue-on-error: ${{ matrix.experimental }}
env:
CARGO_PROFILE_RELEASE_STRIP: symbols
steps:
- name: Checkout tty7
uses: actions/checkout@v4
with:
path: tty7
- uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.target }}
- uses: Swatinem/rust-cache@v2
with:
workspaces: tty7
key: ${{ matrix.target }}
- name: Build tty7-server
working-directory: tty7
run: cargo build --release --locked -p tty7-server --target ${{ matrix.target }}
- name: Assert the binary needs no VC++ redistributable
working-directory: tty7
shell: pwsh
run: '& ./.github/scripts/assert-no-vcruntime.ps1 "target/${{ matrix.target }}/release/tty7-server.exe"'
- name: Stage the asset
working-directory: tty7
shell: pwsh
run: |
New-Item -ItemType Directory -Force dist | Out-Null
Copy-Item "target/${{ matrix.target }}/release/tty7-server.exe" "dist/${{ matrix.asset }}"
- uses: actions/upload-artifact@v7
with:
name: release-${{ matrix.asset }}
path: tty7/dist/${{ matrix.asset }}
if-no-files-found: error
# Single assembly step, after every GUI platform and every server slice
# succeeds. The release object is
# created as a **draft** and left that way: a draft is invisible to both
@@ -385,7 +451,7 @@ jobs:
# empty. Publishing is the release skill's job — it verifies the platform assets and
# writes the body first, then flips the draft. See .claude/skills/release/SKILL.md.
draft-release:
needs: [build, server-musl, server-macos]
needs: [build, server-musl, server-macos, server-windows]
if: startsWith(github.ref, 'refs/tags/')
runs-on: ubuntu-latest
env: