Compare commits

...

11 Commits

Author SHA1 Message Date
Matt Hill
58e0b166cb move comment to safe place 2026-02-02 21:09:19 -07:00
Matt Hill
2a678bb017 fix warning and skip raspberrypi builds for now 2026-02-02 20:16:41 -07:00
Matt Hill
5664456b77 fix for buildjet 2026-02-02 18:51:11 -07:00
Matt Hill
3685b7e57e fix workflows 2026-02-02 18:37:13 -07:00
Matt Hill
989d5f73b1 fix --arch flag to fall back to emulation when native image unavailab… (#3108)
* fix --arch flag to fall back to emulation when native image unavailable, always infer hardware requirement for arch

* better handling of arch filter

* dont cancel in-progress commit workflows and abstract common setup

* cli improvements

fix group handling

* fix cli publish

* alpha.19

---------

Co-authored-by: Aiden McClelland <me@drbonez.dev>
2026-02-03 00:56:59 +00:00
Matt Hill
4f84073cb5 actually eliminate duplicate workflows 2026-01-30 11:28:18 -07:00
Matt Hill
c190295c34 cleaner, also eliminate duplicate workflows 2026-01-30 11:23:40 -07:00
Matt Hill
60875644a1 better i18n checks, better action disabled, fix cert download for ios 2026-01-30 10:59:27 -07:00
Matt Hill
113b09ad01 fix cert download issue in index html 2026-01-29 16:57:12 -07:00
Alex Inkin
2605d0e671 chore: make column shorter (#3107) 2026-01-29 09:54:42 -07:00
Aiden McClelland
d232b91d31 update ota script, rbind for dependency mounts, cli list-ingredients fix, and formatting 2026-01-28 16:09:37 -07:00
78 changed files with 1254 additions and 676 deletions

81
.github/actions/setup-build/action.yml vendored Normal file
View File

@@ -0,0 +1,81 @@
name: Setup Build Environment
description: Common build environment setup steps
inputs:
nodejs-version:
description: Node.js version
required: true
setup-python:
description: Set up Python
required: false
default: "false"
setup-docker:
description: Set up Docker QEMU and Buildx
required: false
default: "true"
setup-sccache:
description: Configure sccache for GitHub Actions
required: false
default: "true"
free-space:
description: Remove unnecessary packages to free disk space
required: false
default: "true"
runs:
using: composite
steps:
- name: Free disk space
if: inputs.free-space == 'true'
shell: bash
run: |
sudo apt-get remove --purge -y azure-cli || true
sudo apt-get remove --purge -y firefox || true
sudo apt-get remove --purge -y ghc-* || true
sudo apt-get remove --purge -y google-cloud-sdk || true
sudo apt-get remove --purge -y google-chrome-stable || true
sudo apt-get remove --purge -y powershell || true
sudo apt-get remove --purge -y php* || true
sudo apt-get remove --purge -y ruby* || true
sudo apt-get remove --purge -y mono-* || true
sudo apt-get autoremove -y
sudo apt-get clean
sudo rm -rf /usr/lib/jvm
sudo rm -rf /usr/local/.ghcup
sudo rm -rf /usr/local/lib/android
sudo rm -rf /usr/share/dotnet
sudo rm -rf /usr/share/swift
sudo rm -rf "$AGENT_TOOLSDIRECTORY"
# BuildJet runners lack /opt/hostedtoolcache, which setup-python and setup-qemu expect
- name: Ensure hostedtoolcache exists
shell: bash
run: sudo mkdir -p /opt/hostedtoolcache && sudo chown $USER:$USER /opt/hostedtoolcache
- name: Set up Python
if: inputs.setup-python == 'true'
uses: actions/setup-python@v5
with:
python-version: "3.x"
- uses: actions/setup-node@v4
with:
node-version: ${{ inputs.nodejs-version }}
cache: npm
cache-dependency-path: "**/package-lock.json"
- name: Set up Docker QEMU
if: inputs.setup-docker == 'true'
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
if: inputs.setup-docker == 'true'
uses: docker/setup-buildx-action@v3
- name: Configure sccache
if: inputs.setup-sccache == 'true'
uses: actions/github-script@v7
with:
script: |
core.exportVariable('ACTIONS_RESULTS_URL', process.env.ACTIONS_RESULTS_URL || '');
core.exportVariable('ACTIONS_RUNTIME_TOKEN', process.env.ACTIONS_RUNTIME_TOKEN || '');

View File

@@ -37,6 +37,10 @@ on:
- master - master
- next/* - next/*
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.ref }}
cancel-in-progress: true
env: env:
NODEJS_VERSION: "24.11.0" NODEJS_VERSION: "24.11.0"
ENVIRONMENT: '${{ fromJson(format(''["{0}", ""]'', github.event.inputs.environment || ''dev''))[github.event.inputs.environment == ''NONE''] }}' ENVIRONMENT: '${{ fromJson(format(''["{0}", ""]'', github.event.inputs.environment || ''dev''))[github.event.inputs.environment == ''NONE''] }}'
@@ -44,6 +48,7 @@ env:
jobs: jobs:
compile: compile:
name: Build Debian Package name: Build Debian Package
if: github.event.pull_request.draft != true
strategy: strategy:
fail-fast: true fail-fast: true
matrix: matrix:
@@ -60,50 +65,15 @@ jobs:
}} }}
runs-on: ${{ fromJson('["ubuntu-latest", "buildjet-32vcpu-ubuntu-2204"]')[github.event.inputs.runner == 'fast'] }} runs-on: ${{ fromJson('["ubuntu-latest", "buildjet-32vcpu-ubuntu-2204"]')[github.event.inputs.runner == 'fast'] }}
steps: steps:
- name: Cleaning up unnecessary files - name: Mount tmpfs
run: |
sudo apt-get remove --purge -y mono-* \
ghc* cabal-install* \
dotnet* \
php* \
ruby* \
mysql-* \
postgresql-* \
azure-cli \
powershell \
google-cloud-sdk \
msodbcsql* mssql-tools* \
imagemagick* \
libgl1-mesa-dri \
google-chrome-stable \
firefox
sudo apt-get autoremove -y
sudo apt-get clean
- run: |
sudo mount -t tmpfs tmpfs .
if: ${{ github.event.inputs.runner == 'fast' }} if: ${{ github.event.inputs.runner == 'fast' }}
run: sudo mount -t tmpfs tmpfs .
- uses: actions/checkout@v4 - uses: actions/checkout@v4
with: with:
submodules: recursive submodules: recursive
- uses: ./.github/actions/setup-build
- uses: actions/setup-node@v4
with: with:
node-version: ${{ env.NODEJS_VERSION }} nodejs-version: ${{ env.NODEJS_VERSION }}
- name: Set up docker QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Configure sccache
uses: actions/github-script@v7
with:
script: |
core.exportVariable('ACTIONS_RESULTS_URL', process.env.ACTIONS_RESULTS_URL || '');
core.exportVariable('ACTIONS_RUNTIME_TOKEN', process.env.ACTIONS_RUNTIME_TOKEN || '');
- name: Make - name: Make
run: TARGET=${{ matrix.triple }} make cli run: TARGET=${{ matrix.triple }} make cli

View File

@@ -1,4 +1,4 @@
name: Start-Registry name: start-registry
on: on:
workflow_call: workflow_call:
@@ -35,6 +35,10 @@ on:
- master - master
- next/* - next/*
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.ref }}
cancel-in-progress: true
env: env:
NODEJS_VERSION: "24.11.0" NODEJS_VERSION: "24.11.0"
ENVIRONMENT: '${{ fromJson(format(''["{0}", ""]'', github.event.inputs.environment || ''dev''))[github.event.inputs.environment == ''NONE''] }}' ENVIRONMENT: '${{ fromJson(format(''["{0}", ""]'', github.event.inputs.environment || ''dev''))[github.event.inputs.environment == ''NONE''] }}'
@@ -42,6 +46,7 @@ env:
jobs: jobs:
compile: compile:
name: Build Debian Package name: Build Debian Package
if: github.event.pull_request.draft != true
strategy: strategy:
fail-fast: true fail-fast: true
matrix: matrix:
@@ -56,50 +61,15 @@ jobs:
}} }}
runs-on: ${{ fromJson('["ubuntu-latest", "buildjet-32vcpu-ubuntu-2204"]')[github.event.inputs.runner == 'fast'] }} runs-on: ${{ fromJson('["ubuntu-latest", "buildjet-32vcpu-ubuntu-2204"]')[github.event.inputs.runner == 'fast'] }}
steps: steps:
- name: Cleaning up unnecessary files - name: Mount tmpfs
run: |
sudo apt-get remove --purge -y mono-* \
ghc* cabal-install* \
dotnet* \
php* \
ruby* \
mysql-* \
postgresql-* \
azure-cli \
powershell \
google-cloud-sdk \
msodbcsql* mssql-tools* \
imagemagick* \
libgl1-mesa-dri \
google-chrome-stable \
firefox
sudo apt-get autoremove -y
sudo apt-get clean
- run: |
sudo mount -t tmpfs tmpfs .
if: ${{ github.event.inputs.runner == 'fast' }} if: ${{ github.event.inputs.runner == 'fast' }}
run: sudo mount -t tmpfs tmpfs .
- uses: actions/checkout@v4 - uses: actions/checkout@v4
with: with:
submodules: recursive submodules: recursive
- uses: ./.github/actions/setup-build
- uses: actions/setup-node@v4
with: with:
node-version: ${{ env.NODEJS_VERSION }} nodejs-version: ${{ env.NODEJS_VERSION }}
- name: Set up docker QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Configure sccache
uses: actions/github-script@v7
with:
script: |
core.exportVariable('ACTIONS_RESULTS_URL', process.env.ACTIONS_RESULTS_URL || '');
core.exportVariable('ACTIONS_RUNTIME_TOKEN', process.env.ACTIONS_RUNTIME_TOKEN || '');
- name: Make - name: Make
run: make registry-deb run: make registry-deb

View File

@@ -1,4 +1,4 @@
name: Start-Tunnel name: start-tunnel
on: on:
workflow_call: workflow_call:
@@ -35,6 +35,10 @@ on:
- master - master
- next/* - next/*
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.ref }}
cancel-in-progress: true
env: env:
NODEJS_VERSION: "24.11.0" NODEJS_VERSION: "24.11.0"
ENVIRONMENT: '${{ fromJson(format(''["{0}", ""]'', github.event.inputs.environment || ''dev''))[github.event.inputs.environment == ''NONE''] }}' ENVIRONMENT: '${{ fromJson(format(''["{0}", ""]'', github.event.inputs.environment || ''dev''))[github.event.inputs.environment == ''NONE''] }}'
@@ -42,6 +46,7 @@ env:
jobs: jobs:
compile: compile:
name: Build Debian Package name: Build Debian Package
if: github.event.pull_request.draft != true
strategy: strategy:
fail-fast: true fail-fast: true
matrix: matrix:
@@ -56,50 +61,15 @@ jobs:
}} }}
runs-on: ${{ fromJson('["ubuntu-latest", "buildjet-32vcpu-ubuntu-2204"]')[github.event.inputs.runner == 'fast'] }} runs-on: ${{ fromJson('["ubuntu-latest", "buildjet-32vcpu-ubuntu-2204"]')[github.event.inputs.runner == 'fast'] }}
steps: steps:
- name: Cleaning up unnecessary files - name: Mount tmpfs
run: |
sudo apt-get remove --purge -y mono-* \
ghc* cabal-install* \
dotnet* \
php* \
ruby* \
mysql-* \
postgresql-* \
azure-cli \
powershell \
google-cloud-sdk \
msodbcsql* mssql-tools* \
imagemagick* \
libgl1-mesa-dri \
google-chrome-stable \
firefox
sudo apt-get autoremove -y
sudo apt-get clean
- run: |
sudo mount -t tmpfs tmpfs .
if: ${{ github.event.inputs.runner == 'fast' }} if: ${{ github.event.inputs.runner == 'fast' }}
run: sudo mount -t tmpfs tmpfs .
- uses: actions/checkout@v4 - uses: actions/checkout@v4
with: with:
submodules: recursive submodules: recursive
- uses: ./.github/actions/setup-build
- uses: actions/setup-node@v4
with: with:
node-version: ${{ env.NODEJS_VERSION }} nodejs-version: ${{ env.NODEJS_VERSION }}
- name: Set up docker QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Configure sccache
uses: actions/github-script@v7
with:
script: |
core.exportVariable('ACTIONS_RESULTS_URL', process.env.ACTIONS_RESULTS_URL || '');
core.exportVariable('ACTIONS_RUNTIME_TOKEN', process.env.ACTIONS_RUNTIME_TOKEN || '');
- name: Make - name: Make
run: make tunnel-deb run: make tunnel-deb

View File

@@ -27,7 +27,7 @@ on:
- x86_64-nonfree - x86_64-nonfree
- aarch64 - aarch64
- aarch64-nonfree - aarch64-nonfree
- raspberrypi # - raspberrypi
- riscv64 - riscv64
deploy: deploy:
type: choice type: choice
@@ -45,6 +45,10 @@ on:
- master - master
- next/* - next/*
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.ref }}
cancel-in-progress: true
env: env:
NODEJS_VERSION: "24.11.0" NODEJS_VERSION: "24.11.0"
ENVIRONMENT: '${{ fromJson(format(''["{0}", ""]'', github.event.inputs.environment || ''dev''))[github.event.inputs.environment == ''NONE''] }}' ENVIRONMENT: '${{ fromJson(format(''["{0}", ""]'', github.event.inputs.environment || ''dev''))[github.event.inputs.environment == ''NONE''] }}'
@@ -52,6 +56,7 @@ env:
jobs: jobs:
compile: compile:
name: Compile Base Binaries name: Compile Base Binaries
if: github.event.pull_request.draft != true
strategy: strategy:
fail-fast: true fail-fast: true
matrix: matrix:
@@ -86,54 +91,16 @@ jobs:
)[github.event.inputs.runner == 'fast'] )[github.event.inputs.runner == 'fast']
}} }}
steps: steps:
- name: Cleaning up unnecessary files - name: Mount tmpfs
run: |
sudo apt-get remove --purge -y azure-cli || true
sudo apt-get remove --purge -y firefox || true
sudo apt-get remove --purge -y ghc-* || true
sudo apt-get remove --purge -y google-cloud-sdk || true
sudo apt-get remove --purge -y google-chrome-stable || true
sudo apt-get remove --purge -y powershell || true
sudo apt-get remove --purge -y php* || true
sudo apt-get remove --purge -y ruby* || true
sudo apt-get remove --purge -y mono-* || true
sudo apt-get autoremove -y
sudo apt-get clean
sudo rm -rf /usr/lib/jvm # All JDKs
sudo rm -rf /usr/local/.ghcup # Haskell toolchain
sudo rm -rf /usr/local/lib/android # Android SDK/NDK, emulator
sudo rm -rf /usr/share/dotnet # .NET SDKs
sudo rm -rf /usr/share/swift # Swift toolchain (if present)
sudo rm -rf "$AGENT_TOOLSDIRECTORY" # Pre-cached tool cache (Go, Node, etc.)
- run: |
sudo mount -t tmpfs tmpfs .
if: ${{ github.event.inputs.runner == 'fast' }} if: ${{ github.event.inputs.runner == 'fast' }}
run: sudo mount -t tmpfs tmpfs .
- uses: actions/checkout@v4 - uses: actions/checkout@v4
with: with:
submodules: recursive submodules: recursive
- uses: ./.github/actions/setup-build
- name: Set up Python
uses: actions/setup-python@v5
with: with:
python-version: "3.x" nodejs-version: ${{ env.NODEJS_VERSION }}
setup-python: "true"
- uses: actions/setup-node@v4
with:
node-version: ${{ env.NODEJS_VERSION }}
- name: Set up docker QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Configure sccache
uses: actions/github-script@v7
with:
script: |
core.exportVariable('ACTIONS_RESULTS_URL', process.env.ACTIONS_RESULTS_URL || '');
core.exportVariable('ACTIONS_RUNTIME_TOKEN', process.env.ACTIONS_RUNTIME_TOKEN || '');
- name: Make - name: Make
run: make ARCH=${{ matrix.arch }} compiled-${{ matrix.arch }}.tar run: make ARCH=${{ matrix.arch }} compiled-${{ matrix.arch }}.tar
@@ -151,13 +118,14 @@ jobs:
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
# TODO: re-add "raspberrypi" to the platform list below
platform: >- platform: >-
${{ ${{
fromJson( fromJson(
format( format(
'[ '[
["{0}"], ["{0}"],
["x86_64", "x86_64-nonfree", "aarch64", "aarch64-nonfree", "riscv64", "raspberrypi"] ["x86_64", "x86_64-nonfree", "aarch64", "aarch64-nonfree", "riscv64"]
]', ]',
github.event.inputs.platform || 'ALL' github.event.inputs.platform || 'ALL'
) )
@@ -221,6 +189,10 @@ jobs:
sudo rm -rf "$AGENT_TOOLSDIRECTORY" # Pre-cached tool cache (Go, Node, etc.) sudo rm -rf "$AGENT_TOOLSDIRECTORY" # Pre-cached tool cache (Go, Node, etc.)
if: ${{ github.event.inputs.runner != 'fast' }} if: ${{ github.event.inputs.runner != 'fast' }}
# BuildJet runners lack /opt/hostedtoolcache, which setup-qemu expects
- name: Ensure hostedtoolcache exists
run: sudo mkdir -p /opt/hostedtoolcache && sudo chown $USER:$USER /opt/hostedtoolcache
- name: Set up docker QEMU - name: Set up docker QEMU
uses: docker/setup-qemu-action@v3 uses: docker/setup-qemu-action@v3

View File

@@ -10,6 +10,10 @@ on:
- master - master
- next/* - next/*
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.ref }}
cancel-in-progress: true
env: env:
NODEJS_VERSION: "24.11.0" NODEJS_VERSION: "24.11.0"
ENVIRONMENT: dev-unstable ENVIRONMENT: dev-unstable
@@ -17,15 +21,18 @@ env:
jobs: jobs:
test: test:
name: Run Automated Tests name: Run Automated Tests
if: github.event.pull_request.draft != true
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
with: with:
submodules: recursive submodules: recursive
- uses: ./.github/actions/setup-build
- uses: actions/setup-node@v4
with: with:
node-version: ${{ env.NODEJS_VERSION }} nodejs-version: ${{ env.NODEJS_VERSION }}
free-space: "false"
setup-docker: "false"
setup-sccache: "false"
- name: Build And Run Tests - name: Build And Run Tests
run: make test run: make test

View File

@@ -324,15 +324,19 @@ web/.angular/.updated: patch-db/client/dist/index.js sdk/baseDist/package.json w
mkdir -p web/.angular mkdir -p web/.angular
touch web/.angular/.updated touch web/.angular/.updated
web/dist/raw/ui/index.html: $(WEB_UI_SRC) $(WEB_SHARED_SRC) web/.angular/.updated web/.i18n-checked: $(WEB_SHARED_SRC) $(WEB_UI_SRC) $(WEB_SETUP_WIZARD_SRC) $(WEB_START_TUNNEL_SRC)
npm --prefix web run check:i18n
touch web/.i18n-checked
web/dist/raw/ui/index.html: $(WEB_UI_SRC) $(WEB_SHARED_SRC) web/.angular/.updated web/.i18n-checked
npm --prefix web run build:ui npm --prefix web run build:ui
touch web/dist/raw/ui/index.html touch web/dist/raw/ui/index.html
web/dist/raw/setup-wizard/index.html: $(WEB_SETUP_WIZARD_SRC) $(WEB_SHARED_SRC) web/.angular/.updated web/dist/raw/setup-wizard/index.html: $(WEB_SETUP_WIZARD_SRC) $(WEB_SHARED_SRC) web/.angular/.updated web/.i18n-checked
npm --prefix web run build:setup npm --prefix web run build:setup
touch web/dist/raw/setup-wizard/index.html touch web/dist/raw/setup-wizard/index.html
web/dist/raw/start-tunnel/index.html: $(WEB_START_TUNNEL_SRC) $(WEB_SHARED_SRC) web/.angular/.updated web/dist/raw/start-tunnel/index.html: $(WEB_START_TUNNEL_SRC) $(WEB_SHARED_SRC) web/.angular/.updated web/.i18n-checked
npm --prefix web run build:tunnel npm --prefix web run build:tunnel
touch web/dist/raw/start-tunnel/index.html touch web/dist/raw/start-tunnel/index.html

View File

@@ -46,6 +46,7 @@ openssh-server
podman podman
psmisc psmisc
qemu-guest-agent qemu-guest-agent
qemu-user-static
rfkill rfkill
rsync rsync
samba-common-bin samba-common-bin

View File

@@ -15,13 +15,12 @@ if [ "$SKIP_DL" != "1" ]; then
fi fi
if [ -n "$RUN_ID" ]; then if [ -n "$RUN_ID" ]; then
for arch in aarch64 aarch64-nonfree riscv64 riscv64-nonfree x86_64 x86_64-nonfree raspberrypi; do for arch in aarch64 aarch64-nonfree riscv64 x86_64 x86_64-nonfree; do
while ! gh run download -R Start9Labs/start-os $RUN_ID -n $arch.squashfs -D $(pwd); do sleep 1; done while ! gh run download -R Start9Labs/start-os $RUN_ID -n $arch.squashfs -D $(pwd); do sleep 1; done
done done
for arch in aarch64 aarch64-nonfree riscv64 riscv64-nonfree x86_64 x86_64-nonfree; do for arch in aarch64 aarch64-nonfree riscv64 x86_64 x86_64-nonfree; do
while ! gh run download -R Start9Labs/start-os $RUN_ID -n $arch.iso -D $(pwd); do sleep 1; done while ! gh run download -R Start9Labs/start-os $RUN_ID -n $arch.iso -D $(pwd); do sleep 1; done
done done
while ! gh run download -R Start9Labs/start-os $RUN_ID -n raspberrypi.img -D $(pwd); do sleep 1; done
fi fi
if [ -n "$ST_RUN_ID" ]; then if [ -n "$ST_RUN_ID" ]; then
@@ -57,31 +56,23 @@ start-cli --registry=https://alpha-registry-x.start9.com registry os version add
if [ "$SKIP_UL" = "2" ]; then if [ "$SKIP_UL" = "2" ]; then
exit 2 exit 2
elif [ "$SKIP_UL" != "1" ]; then elif [ "$SKIP_UL" != "1" ]; then
for file in *.squashfs *.iso *.deb start-cli_*; do for file in *.deb start-cli_*; do
gh release upload -R Start9Labs/start-os v$VERSION $file gh release upload -R Start9Labs/start-os v$VERSION $file
done done
for file in *.img; do for file in *.iso *.squashfs; do
if ! [ -f $file.gz ]; then s3cmd put -P $file s3://startos-images/v$VERSION/$file
cat $file | pigz > $file.gz
fi
gh release upload -R Start9Labs/start-os v$VERSION $file.gz
done done
fi fi
if [ "$SKIP_INDEX" != "1" ]; then if [ "$SKIP_INDEX" != "1" ]; then
for arch in aarch64 aarch64-nonfree riscv64 riscv64-nonfree x86_64 x86_64-nonfree; do for arch in aarch64 aarch64-nonfree riscv64 x86_64 x86_64-nonfree; do
for file in *_$arch.squashfs *_$arch.iso; do for file in *_$arch.squashfs *_$arch.iso; do
start-cli --registry=https://alpha-registry-x.start9.com registry os asset add --platform=$arch --version=$VERSION $file https://github.com/Start9Labs/start-os/releases/download/v$VERSION/$(echo -n "$file" | sed 's/~/./g') start-cli --registry=https://alpha-registry-x.start9.com registry os asset add --platform=$arch --version=$VERSION $file https://startos-images.nyc3.cdn.digitaloceanspaces.com/v$VERSION/$file
done
done
for arch in raspberrypi; do
for file in *_$arch.squashfs; do
start-cli --registry=https://alpha-registry-x.start9.com registry os asset add --platform=$arch --version=$VERSION $file https://github.com/Start9Labs/start-os/releases/download/v$VERSION/$(echo -n "$file" | sed 's/~/./g')
done done
done done
fi fi
for file in *.iso *.img *.img.gz *.squashfs *.deb start-cli_*; do for file in *.iso *.squashfs *.deb start-cli_*; do
gpg -u 7CFFDA41CA66056A --detach-sign --armor -o "${file}.asc" "$file" gpg -u 7CFFDA41CA66056A --detach-sign --armor -o "${file}.asc" "$file"
done done
@@ -90,20 +81,30 @@ tar -czvf signatures.tar.gz *.asc
gh release upload -R Start9Labs/start-os v$VERSION signatures.tar.gz gh release upload -R Start9Labs/start-os v$VERSION signatures.tar.gz
cat << EOF
# ISO Downloads
- [x86_64/AMD64](https://startos-images.nyc3.cdn.digitaloceanspaces.com/v$VERSION/$(ls *_x86_64-nonfree.iso))
- [x86_64/AMD64-slim (FOSS-only)](https://startos-images.nyc3.cdn.digitaloceanspaces.com/v$VERSION/$(ls *_x86_64.iso) "Without proprietary software or drivers")
- [aarch64/ARM64](https://startos-images.nyc3.cdn.digitaloceanspaces.com/v$VERSION/$(ls *_aarch64-nonfree.iso))
- [aarch64/ARM64-slim (FOSS-Only)](https://startos-images.nyc3.cdn.digitaloceanspaces.com/v$VERSION/$(ls *_aarch64.iso) "Without proprietary software or drivers")
- [RISCV64 (RVA23)](https://startos-images.nyc3.cdn.digitaloceanspaces.com/v$VERSION/$(ls *_riscv64.iso))
EOF
cat << 'EOF' cat << 'EOF'
# StartOS Checksums # StartOS Checksums
## SHA-256 ## SHA-256
``` ```
EOF EOF
sha256sum *.iso *.img *img.gz *.squashfs sha256sum *.iso *.squashfs
cat << 'EOF' cat << 'EOF'
``` ```
## BLAKE-3 ## BLAKE-3
``` ```
EOF EOF
b3sum *.iso *.img *.img.gz *.squashfs b3sum *.iso *.squashfs
cat << 'EOF' cat << 'EOF'
``` ```
@@ -138,5 +139,4 @@ EOF
b3sum start-cli_* b3sum start-cli_*
cat << 'EOF' cat << 'EOF'
``` ```
EOF EOF

2
core/Cargo.lock generated
View File

@@ -7817,7 +7817,7 @@ dependencies = [
[[package]] [[package]]
name = "start-os" name = "start-os"
version = "0.4.0-alpha.18" version = "0.4.0-alpha.19"
dependencies = [ dependencies = [
"aes 0.7.5", "aes 0.7.5",
"arti-client", "arti-client",

View File

@@ -15,7 +15,7 @@ license = "MIT"
name = "start-os" name = "start-os"
readme = "README.md" readme = "README.md"
repository = "https://github.com/Start9Labs/start-os" repository = "https://github.com/Start9Labs/start-os"
version = "0.4.0-alpha.18" # VERSION_BUMP version = "0.4.0-alpha.19" # VERSION_BUMP
[lib] [lib]
name = "startos" name = "startos"
@@ -176,6 +176,7 @@ mio = "1"
new_mime_guess = "4" new_mime_guess = "4"
nix = { version = "0.30.1", features = [ nix = { version = "0.30.1", features = [
"fs", "fs",
"hostname",
"mount", "mount",
"net", "net",
"process", "process",

View File

@@ -1843,18 +1843,18 @@ service.mod.failed-to-parse-package-data-entry:
pl_PL: "Nie udało się przeanalizować PackageDataEntry, znaleziono: %{error}" pl_PL: "Nie udało się przeanalizować PackageDataEntry, znaleziono: %{error}"
service.mod.no-matching-subcontainers: service.mod.no-matching-subcontainers:
en_US: "no matching subcontainers are running for %{id}; some possible choices are:\n%{subcontainers}" en_US: "no matching subcontainers are running for %{id}; some possible choices are:"
de_DE: "keine passenden Subcontainer laufen für %{id}; einige mögliche Optionen sind:\n%{subcontainers}" de_DE: "keine passenden Subcontainer laufen für %{id}; einige mögliche Optionen sind:"
es_ES: "no hay subcontenedores coincidentes ejecutándose para %{id}; algunas opciones posibles son:\n%{subcontainers}" es_ES: "no hay subcontenedores coincidentes ejecutándose para %{id}; algunas opciones posibles son:"
fr_FR: "aucun sous-conteneur correspondant n'est en cours d'exécution pour %{id} ; voici quelques choix possibles :\n%{subcontainers}" fr_FR: "aucun sous-conteneur correspondant n'est en cours d'exécution pour %{id} ; voici quelques choix possibles :"
pl_PL: "nie działają pasujące podkontenery dla %{id}; niektóre możliwe wybory to:\n%{subcontainers}" pl_PL: "nie działają pasujące podkontenery dla %{id}; niektóre możliwe wybory to:"
service.mod.multiple-subcontainers-found: service.mod.multiple-subcontainers-found:
en_US: "multiple subcontainers found for %{id}: \n%{subcontainer_ids}" en_US: "multiple subcontainers found for %{id}"
de_DE: "mehrere Subcontainer für %{id} gefunden: \n%{subcontainer_ids}" de_DE: "mehrere Subcontainer für %{id} gefunden"
es_ES: "se encontraron múltiples subcontenedores para %{id}: \n%{subcontainer_ids}" es_ES: "se encontraron múltiples subcontenedores para %{id}"
fr_FR: "plusieurs sous-conteneurs trouvés pour %{id} : \n%{subcontainer_ids}" fr_FR: "plusieurs sous-conteneurs trouvés pour %{id}"
pl_PL: "znaleziono wiele podkontenerów dla %{id}: \n%{subcontainer_ids}" pl_PL: "znaleziono wiele podkontenerów dla %{id}"
service.mod.invalid-byte-length-for-signal: service.mod.invalid-byte-length-for-signal:
en_US: "invalid byte length for signal: %{length}" en_US: "invalid byte length for signal: %{length}"
@@ -3703,6 +3703,20 @@ help.arg.wireguard-config:
fr_FR: "Configuration WireGuard" fr_FR: "Configuration WireGuard"
pl_PL: "Konfiguracja WireGuard" pl_PL: "Konfiguracja WireGuard"
help.s9pk-s3base:
en_US: "Base URL for publishing s9pks"
de_DE: "Basis-URL für die Veröffentlichung von s9pks"
es_ES: "URL base para publicar s9pks"
fr_FR: "URL de base pour publier les s9pks"
pl_PL: "Bazowy URL do publikowania s9pks"
help.s9pk-s3bucket:
en_US: "S3 bucket to publish s9pks to (should correspond to s3base)"
de_DE: "S3-Bucket zum Veröffentlichen von s9pks (sollte mit s3base übereinstimmen)"
es_ES: "Bucket S3 para publicar s9pks (debe corresponder con s3base)"
fr_FR: "Bucket S3 pour publier les s9pks (doit correspondre à s3base)"
pl_PL: "Bucket S3 do publikowania s9pks (powinien odpowiadać s3base)"
# CLI command descriptions (about.*) # CLI command descriptions (about.*)
about.add-address-to-host: about.add-address-to-host:
en_US: "Add an address to this host" en_US: "Add an address to this host"
@@ -4866,6 +4880,13 @@ about.persist-new-notification:
fr_FR: "Persister une nouvelle notification" fr_FR: "Persister une nouvelle notification"
pl_PL: "Utrwal nowe powiadomienie" pl_PL: "Utrwal nowe powiadomienie"
about.publish-s9pk:
en_US: "Publish s9pk to S3 bucket and index on registry"
de_DE: "S9pk in S3-Bucket veröffentlichen und in Registry indizieren"
es_ES: "Publicar s9pk en bucket S3 e indexar en el registro"
fr_FR: "Publier s9pk dans le bucket S3 et indexer dans le registre"
pl_PL: "Opublikuj s9pk do bucketu S3 i zindeksuj w rejestrze"
about.rebuild-service-container: about.rebuild-service-container:
en_US: "Rebuild service container" en_US: "Rebuild service container"
de_DE: "Dienst-Container neu erstellen" de_DE: "Dienst-Container neu erstellen"

View File

@@ -180,7 +180,13 @@ pub async fn update(
.as_idx_mut(&id) .as_idx_mut(&id)
.ok_or_else(|| { .ok_or_else(|| {
Error::new( Error::new(
eyre!("{}", t!("backup.target.cifs.target-not-found", id = BackupTargetId::Cifs { id })), eyre!(
"{}",
t!(
"backup.target.cifs.target-not-found",
id = BackupTargetId::Cifs { id }
)
),
ErrorKind::NotFound, ErrorKind::NotFound,
) )
})? })?

View File

@@ -1,10 +1,7 @@
use rust_i18n::t; use rust_i18n::t;
pub fn renamed(old: &str, new: &str) -> ! { pub fn renamed(old: &str, new: &str) -> ! {
eprintln!( eprintln!("{}", t!("bins.deprecated.renamed", old = old, new = new));
"{}",
t!("bins.deprecated.renamed", old = old, new = new)
);
std::process::exit(1) std::process::exit(1)
} }

View File

@@ -4,8 +4,8 @@ use std::time::Duration;
use clap::Parser; use clap::Parser;
use color_eyre::eyre::eyre; use color_eyre::eyre::eyre;
use rust_i18n::t;
use futures::{FutureExt, TryFutureExt}; use futures::{FutureExt, TryFutureExt};
use rust_i18n::t;
use tokio::signal::unix::signal; use tokio::signal::unix::signal;
use tracing::instrument; use tracing::instrument;

View File

@@ -38,6 +38,8 @@ pub struct CliContextSeed {
pub registry_url: Option<Url>, pub registry_url: Option<Url>,
pub registry_hostname: Vec<InternedString>, pub registry_hostname: Vec<InternedString>,
pub registry_listen: Option<SocketAddr>, pub registry_listen: Option<SocketAddr>,
pub s9pk_s3base: Option<Url>,
pub s9pk_s3bucket: Option<InternedString>,
pub tunnel_addr: Option<SocketAddr>, pub tunnel_addr: Option<SocketAddr>,
pub tunnel_listen: Option<SocketAddr>, pub tunnel_listen: Option<SocketAddr>,
pub client: Client, pub client: Client,
@@ -129,6 +131,8 @@ impl CliContext {
.transpose()?, .transpose()?,
registry_hostname: config.registry_hostname.unwrap_or_default(), registry_hostname: config.registry_hostname.unwrap_or_default(),
registry_listen: config.registry_listen, registry_listen: config.registry_listen,
s9pk_s3base: config.s9pk_s3base,
s9pk_s3bucket: config.s9pk_s3bucket,
tunnel_addr: config.tunnel, tunnel_addr: config.tunnel,
tunnel_listen: config.tunnel_listen, tunnel_listen: config.tunnel_listen,
client: { client: {
@@ -160,21 +164,23 @@ impl CliContext {
if !path.exists() { if !path.exists() {
continue; continue;
} }
let pair = <ed25519::KeypairBytes as ed25519::pkcs8::DecodePrivateKey>::from_pkcs8_pem( let pair =
&std::fs::read_to_string(path)?, <ed25519::KeypairBytes as ed25519::pkcs8::DecodePrivateKey>::from_pkcs8_pem(
) &std::fs::read_to_string(path)?,
.with_kind(crate::ErrorKind::Pem)?;
let secret = ed25519_dalek::SecretKey::try_from(&pair.secret_key[..]).map_err(|_| {
Error::new(
eyre!("{}", t!("context.cli.pkcs8-key-incorrect-length")),
ErrorKind::OpenSsl,
) )
})?; .with_kind(crate::ErrorKind::Pem)?;
return Ok(secret.into()) let secret =
ed25519_dalek::SecretKey::try_from(&pair.secret_key[..]).map_err(|_| {
Error::new(
eyre!("{}", t!("context.cli.pkcs8-key-incorrect-length")),
ErrorKind::OpenSsl,
)
})?;
return Ok(secret.into());
} }
Err(Error::new( Err(Error::new(
eyre!("{}", t!("context.cli.developer-key-does-not-exist")), eyre!("{}", t!("context.cli.developer-key-does-not-exist")),
crate::ErrorKind::Uninitialized crate::ErrorKind::Uninitialized,
)) ))
}) })
} }
@@ -195,8 +201,12 @@ impl CliContext {
.into()); .into());
} }
}; };
url.set_scheme(ws_scheme) url.set_scheme(ws_scheme).map_err(|_| {
.map_err(|_| Error::new(eyre!("{}", t!("context.cli.cannot-set-url-scheme")), crate::ErrorKind::ParseUrl))?; Error::new(
eyre!("{}", t!("context.cli.cannot-set-url-scheme")),
crate::ErrorKind::ParseUrl,
)
})?;
url.path_segments_mut() url.path_segments_mut()
.map_err(|_| eyre!("Url cannot be base")) .map_err(|_| eyre!("Url cannot be base"))
.with_kind(crate::ErrorKind::ParseUrl)? .with_kind(crate::ErrorKind::ParseUrl)?

View File

@@ -68,6 +68,10 @@ pub struct ClientConfig {
pub registry_hostname: Option<Vec<InternedString>>, pub registry_hostname: Option<Vec<InternedString>>,
#[arg(skip)] #[arg(skip)]
pub registry_listen: Option<SocketAddr>, pub registry_listen: Option<SocketAddr>,
#[arg(long, help = "help.s9pk-s3base")]
pub s9pk_s3base: Option<Url>,
#[arg(long, help = "help.s9pk-s3bucket")]
pub s9pk_s3bucket: Option<InternedString>,
#[arg(short = 't', long, help = "help.arg.tunnel-address")] #[arg(short = 't', long, help = "help.arg.tunnel-address")]
pub tunnel: Option<SocketAddr>, pub tunnel: Option<SocketAddr>,
#[arg(skip)] #[arg(skip)]
@@ -89,8 +93,13 @@ impl ContextConfig for ClientConfig {
self.host = self.host.take().or(other.host); self.host = self.host.take().or(other.host);
self.registry = self.registry.take().or(other.registry); self.registry = self.registry.take().or(other.registry);
self.registry_hostname = self.registry_hostname.take().or(other.registry_hostname); self.registry_hostname = self.registry_hostname.take().or(other.registry_hostname);
self.registry_listen = self.registry_listen.take().or(other.registry_listen);
self.s9pk_s3base = self.s9pk_s3base.take().or(other.s9pk_s3base);
self.s9pk_s3bucket = self.s9pk_s3bucket.take().or(other.s9pk_s3bucket);
self.tunnel = self.tunnel.take().or(other.tunnel); self.tunnel = self.tunnel.take().or(other.tunnel);
self.tunnel_listen = self.tunnel_listen.take().or(other.tunnel_listen);
self.proxy = self.proxy.take().or(other.proxy); self.proxy = self.proxy.take().or(other.proxy);
self.socks_listen = self.socks_listen.take().or(other.socks_listen);
self.cookie_path = self.cookie_path.take().or(other.cookie_path); self.cookie_path = self.cookie_path.take().or(other.cookie_path);
self.developer_key_path = self.developer_key_path.take().or(other.developer_key_path); self.developer_key_path = self.developer_key_path.take().or(other.developer_key_path);
} }

View File

@@ -27,7 +27,10 @@ impl DiagnosticContext {
disk_guid: Option<InternedString>, disk_guid: Option<InternedString>,
error: Error, error: Error,
) -> Result<Self, Error> { ) -> Result<Self, Error> {
tracing::error!("{}", t!("context.diagnostic.starting-diagnostic-ui", error = error)); tracing::error!(
"{}",
t!("context.diagnostic.starting-diagnostic-ui", error = error)
);
tracing::debug!("{:?}", error); tracing::debug!("{:?}", error);
let (shutdown, _) = tokio::sync::broadcast::channel(1); let (shutdown, _) = tokio::sync::broadcast::channel(1);

View File

@@ -463,7 +463,10 @@ impl RpcContext {
.await .await
.result .result
{ {
tracing::error!("{}", t!("context.rpc.error-in-session-cleanup-cron", error = e)); tracing::error!(
"{}",
t!("context.rpc.error-in-session-cleanup-cron", error = e)
);
tracing::debug!("{e:?}"); tracing::debug!("{e:?}");
} }
} }
@@ -576,6 +579,7 @@ impl RpcContext {
pub async fn call_remote<RemoteContext>( pub async fn call_remote<RemoteContext>(
&self, &self,
method: &str, method: &str,
metadata: OrdMap<&'static str, Value>,
params: Value, params: Value,
) -> Result<Value, RpcError> ) -> Result<Value, RpcError>
where where
@@ -584,7 +588,7 @@ impl RpcContext {
<Self as CallRemote<RemoteContext, Empty>>::call_remote( <Self as CallRemote<RemoteContext, Empty>>::call_remote(
&self, &self,
method, method,
OrdMap::new(), metadata,
params, params,
Empty {}, Empty {},
) )
@@ -593,20 +597,15 @@ impl RpcContext {
pub async fn call_remote_with<RemoteContext, T>( pub async fn call_remote_with<RemoteContext, T>(
&self, &self,
method: &str, method: &str,
metadata: OrdMap<&'static str, Value>,
params: Value, params: Value,
extra: T, extra: T,
) -> Result<Value, RpcError> ) -> Result<Value, RpcError>
where where
Self: CallRemote<RemoteContext, T>, Self: CallRemote<RemoteContext, T>,
{ {
<Self as CallRemote<RemoteContext, T>>::call_remote( <Self as CallRemote<RemoteContext, T>>::call_remote(&self, method, metadata, params, extra)
&self, .await
method,
OrdMap::new(),
params,
extra,
)
.await
} }
} }
impl AsRef<Client> for RpcContext { impl AsRef<Client> for RpcContext {

View File

@@ -87,7 +87,11 @@ pub enum RevisionsRes {
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
#[command(rename_all = "kebab-case")] #[command(rename_all = "kebab-case")]
pub struct CliDumpParams { pub struct CliDumpParams {
#[arg(long = "include-private", short = 'p', help = "help.arg.include-private-data")] #[arg(
long = "include-private",
short = 'p',
help = "help.arg.include-private-data"
)]
#[serde(default)] #[serde(default)]
include_private: bool, include_private: bool,
#[arg(help = "help.arg.db-path")] #[arg(help = "help.arg.db-path")]

View File

@@ -70,12 +70,20 @@ async fn e2fsck_runner(
if code & 4 != 0 { if code & 4 != 0 {
tracing::error!( tracing::error!(
"{}", "{}",
t!("disk.fsck.errors-not-corrected", device = logicalname.as_ref().display(), stderr = e2fsck_stderr), t!(
"disk.fsck.errors-not-corrected",
device = logicalname.as_ref().display(),
stderr = e2fsck_stderr
),
); );
} else if code & 1 != 0 { } else if code & 1 != 0 {
tracing::warn!( tracing::warn!(
"{}", "{}",
t!("disk.fsck.errors-corrected", device = logicalname.as_ref().display(), stderr = e2fsck_stderr), t!(
"disk.fsck.errors-corrected",
device = logicalname.as_ref().display(),
stderr = e2fsck_stderr
),
); );
} }
if code < 8 { if code < 8 {

View File

@@ -29,25 +29,31 @@ impl Default for FileType {
pub struct Bind<Src: AsRef<Path>> { pub struct Bind<Src: AsRef<Path>> {
src: Src, src: Src,
filetype: FileType, filetype: FileType,
recursive: bool,
} }
impl<Src: AsRef<Path>> Bind<Src> { impl<Src: AsRef<Path>> Bind<Src> {
pub fn new(src: Src) -> Self { pub fn new(src: Src) -> Self {
Self { Self {
src, src,
filetype: FileType::Directory, filetype: FileType::Directory,
recursive: false,
} }
} }
pub fn with_type(mut self, filetype: FileType) -> Self { pub fn with_type(mut self, filetype: FileType) -> Self {
self.filetype = filetype; self.filetype = filetype;
self self
} }
pub fn recursive(mut self, recursive: bool) -> Self {
self.recursive = recursive;
self
}
} }
impl<Src: AsRef<Path> + Send + Sync> FileSystem for Bind<Src> { impl<Src: AsRef<Path> + Send + Sync> FileSystem for Bind<Src> {
async fn source(&self) -> Result<Option<impl AsRef<Path>>, Error> { async fn source(&self) -> Result<Option<impl AsRef<Path>>, Error> {
Ok(Some(&self.src)) Ok(Some(&self.src))
} }
fn extra_args(&self) -> impl IntoIterator<Item = impl AsRef<std::ffi::OsStr>> { fn extra_args(&self) -> impl IntoIterator<Item = impl AsRef<std::ffi::OsStr>> {
["--bind"] [if self.recursive { "--rbind" } else { "--bind" }]
} }
async fn pre_mount(&self, mountpoint: &Path, mount_type: MountType) -> Result<(), Error> { async fn pre_mount(&self, mountpoint: &Path, mount_type: MountType) -> Result<(), Error> {
let from_meta = tokio::fs::metadata(&self.src).await.ok(); let from_meta = tokio::fs::metadata(&self.src).await.ok();

View File

@@ -24,7 +24,11 @@ pub async fn bind<P0: AsRef<Path>, P1: AsRef<Path>>(
) -> Result<(), Error> { ) -> Result<(), Error> {
tracing::info!( tracing::info!(
"{}", "{}",
t!("disk.mount.binding", src = src.as_ref().display(), dst = dst.as_ref().display()) t!(
"disk.mount.binding",
src = src.as_ref().display(),
dst = dst.as_ref().display()
)
); );
if is_mountpoint(&dst).await? { if is_mountpoint(&dst).await? {
unmount(dst.as_ref(), true).await?; unmount(dst.as_ref(), true).await?;

View File

@@ -4,7 +4,7 @@ use axum::http::StatusCode;
use axum::http::uri::InvalidUri; use axum::http::uri::InvalidUri;
use color_eyre::eyre::eyre; use color_eyre::eyre::eyre;
use num_enum::TryFromPrimitive; use num_enum::TryFromPrimitive;
use patch_db::Revision; use patch_db::Value;
use rpc_toolkit::reqwest; use rpc_toolkit::reqwest;
use rpc_toolkit::yajrc::{ use rpc_toolkit::yajrc::{
INVALID_PARAMS_ERROR, INVALID_REQUEST_ERROR, METHOD_NOT_FOUND_ERROR, PARSE_ERROR, RpcError, INVALID_PARAMS_ERROR, INVALID_REQUEST_ERROR, METHOD_NOT_FOUND_ERROR, PARSE_ERROR, RpcError,
@@ -16,6 +16,7 @@ use tokio_rustls::rustls;
use ts_rs::TS; use ts_rs::TS;
use crate::InvalidId; use crate::InvalidId;
use crate::prelude::to_value;
#[derive(Debug, Clone, Copy, PartialEq, Eq, TryFromPrimitive)] #[derive(Debug, Clone, Copy, PartialEq, Eq, TryFromPrimitive)]
#[repr(i32)] #[repr(i32)]
@@ -183,7 +184,8 @@ impl ErrorKind {
UpdateFailed => t!("error.update-failed"), UpdateFailed => t!("error.update-failed"),
Smtp => t!("error.smtp"), Smtp => t!("error.smtp"),
SetSysInfo => t!("error.set-sys-info"), SetSysInfo => t!("error.set-sys-info"),
}.to_string() }
.to_string()
} }
} }
impl Display for ErrorKind { impl Display for ErrorKind {
@@ -196,7 +198,7 @@ pub struct Error {
pub source: color_eyre::eyre::Error, pub source: color_eyre::eyre::Error,
pub debug: Option<color_eyre::eyre::Error>, pub debug: Option<color_eyre::eyre::Error>,
pub kind: ErrorKind, pub kind: ErrorKind,
pub revision: Option<Revision>, pub info: Value,
pub task: Option<JoinHandle<()>>, pub task: Option<JoinHandle<()>>,
} }
@@ -227,7 +229,7 @@ impl Error {
source: source.into(), source: source.into(),
debug, debug,
kind, kind,
revision: None, info: Value::Null,
task: None, task: None,
} }
} }
@@ -236,7 +238,7 @@ impl Error {
source: eyre!("{}", self.source), source: eyre!("{}", self.source),
debug: self.debug.as_ref().map(|e| eyre!("{e}")), debug: self.debug.as_ref().map(|e| eyre!("{e}")),
kind: self.kind, kind: self.kind,
revision: self.revision.clone(), info: self.info.clone(),
task: None, task: None,
} }
} }
@@ -244,6 +246,10 @@ impl Error {
self.task = Some(task); self.task = Some(task);
self self
} }
pub fn with_info(mut self, info: Value) -> Self {
self.info = info;
self
}
pub async fn wait(mut self) -> Self { pub async fn wait(mut self) -> Self {
if let Some(task) = &mut self.task { if let Some(task) = &mut self.task {
task.await.log_err(); task.await.log_err();
@@ -422,6 +428,8 @@ impl From<patch_db::value::Error> for Error {
pub struct ErrorData { pub struct ErrorData {
pub details: String, pub details: String,
pub debug: String, pub debug: String,
#[serde(default)]
pub info: Value,
} }
impl Display for ErrorData { impl Display for ErrorData {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
@@ -439,6 +447,7 @@ impl From<Error> for ErrorData {
Self { Self {
details: value.to_string(), details: value.to_string(),
debug: format!("{:?}", value), debug: format!("{:?}", value),
info: value.info,
} }
} }
} }
@@ -469,40 +478,31 @@ impl From<&RpcError> for ErrorData {
.or_else(|| d.as_str().map(|s| s.to_owned())) .or_else(|| d.as_str().map(|s| s.to_owned()))
}) })
.unwrap_or_else(|| value.message.clone().into_owned()), .unwrap_or_else(|| value.message.clone().into_owned()),
info: to_value(
&value
.data
.as_ref()
.and_then(|d| d.as_object().and_then(|d| d.get("info"))),
)
.unwrap_or_default(),
} }
} }
} }
impl From<Error> for RpcError { impl From<Error> for RpcError {
fn from(e: Error) -> Self { fn from(e: Error) -> Self {
let mut data_object = serde_json::Map::with_capacity(3); let kind = e.kind;
data_object.insert("details".to_owned(), format!("{}", e.source).into()); let data = ErrorData::from(e);
data_object.insert("debug".to_owned(), format!("{:?}", e.source).into()); RpcError {
data_object.insert( code: kind as i32,
"revision".to_owned(), message: kind.as_str().into(),
match serde_json::to_value(&e.revision) { data: Some(match serde_json::to_value(&data) {
Ok(a) => a, Ok(a) => a,
Err(e) => { Err(e) => {
tracing::warn!("Error serializing revision for Error object: {}", e); tracing::warn!("Error serializing ErrorData object: {}", e);
serde_json::Value::Null serde_json::Value::Null
} }
}, }),
);
RpcError {
code: e.kind as i32,
message: e.kind.as_str().into(),
data: Some(
match serde_json::to_value(&ErrorData {
details: format!("{}", e.source),
debug: format!("{:?}", e.source),
}) {
Ok(a) => a,
Err(e) => {
tracing::warn!("Error serializing revision for Error object: {}", e);
serde_json::Value::Null
}
},
),
} }
} }
} }
@@ -605,7 +605,7 @@ where
kind, kind,
source, source,
debug, debug,
revision: None, info: Value::Null,
task: None, task: None,
} }
}) })

View File

@@ -131,6 +131,9 @@ pub async fn install(
let package: GetPackageResponse = from_value( let package: GetPackageResponse = from_value(
ctx.call_remote_with::<RegistryContext, _>( ctx.call_remote_with::<RegistryContext, _>(
"package.get", "package.get",
[("get_device_info", Value::Bool(true))]
.into_iter()
.collect(),
json!({ json!({
"id": id, "id": id,
"targetVersion": VersionRange::exactly(version.deref().clone()), "targetVersion": VersionRange::exactly(version.deref().clone()),

View File

@@ -540,7 +540,10 @@ pub fn package<C: Context>() -> ParentHandler<C> {
.with_about("about.execute-commands-container") .with_about("about.execute-commands-container")
.no_cli(), .no_cli(),
) )
.subcommand("attach", from_fn_async(service::cli_attach).no_display()) .subcommand(
"attach",
from_fn_async_local(service::cli_attach).no_display(),
)
.subcommand( .subcommand(
"host", "host",
net::host::host_api::<C>().with_about("about.manage-network-hosts-package"), net::host::host_api::<C>().with_about("about.manage-network-hosts-package"),

View File

@@ -6,7 +6,6 @@ use std::str::FromStr;
use std::time::{Duration, UNIX_EPOCH}; use std::time::{Duration, UNIX_EPOCH};
use axum::extract::ws; use axum::extract::ws;
use crate::util::net::WebSocket;
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use clap::builder::ValueParserFactory; use clap::builder::ValueParserFactory;
use clap::{Args, FromArgMatches, Parser}; use clap::{Args, FromArgMatches, Parser};
@@ -31,6 +30,7 @@ use crate::context::{CliContext, RpcContext};
use crate::error::ResultExt; use crate::error::ResultExt;
use crate::prelude::*; use crate::prelude::*;
use crate::rpc_continuations::{Guid, RpcContinuation, RpcContinuations}; use crate::rpc_continuations::{Guid, RpcContinuation, RpcContinuations};
use crate::util::net::WebSocket;
use crate::util::serde::Reversible; use crate::util::serde::Reversible;
use crate::util::{FromStrParser, Invoke}; use crate::util::{FromStrParser, Invoke};
@@ -330,12 +330,22 @@ pub struct LogsParams<Extra: FromArgMatches + Args = Empty> {
extra: Extra, extra: Extra,
#[arg(short = 'l', long = "limit", help = "help.arg.log-limit")] #[arg(short = 'l', long = "limit", help = "help.arg.log-limit")]
limit: Option<usize>, limit: Option<usize>,
#[arg(short = 'c', long = "cursor", conflicts_with = "follow", help = "help.arg.log-cursor")] #[arg(
short = 'c',
long = "cursor",
conflicts_with = "follow",
help = "help.arg.log-cursor"
)]
cursor: Option<String>, cursor: Option<String>,
#[arg(short = 'b', long = "boot", help = "help.arg.log-boot")] #[arg(short = 'b', long = "boot", help = "help.arg.log-boot")]
#[serde(default)] #[serde(default)]
boot: Option<BootIdentifier>, boot: Option<BootIdentifier>,
#[arg(short = 'B', long = "before", conflicts_with = "follow", help = "help.arg.log-before")] #[arg(
short = 'B',
long = "before",
conflicts_with = "follow",
help = "help.arg.log-before"
)]
#[serde(default)] #[serde(default)]
before: bool, before: bool,
} }
@@ -553,10 +563,12 @@ pub async fn journalctl(
follow_cmd.arg("--lines=0"); follow_cmd.arg("--lines=0");
} }
let mut child = follow_cmd.stdout(Stdio::piped()).spawn()?; let mut child = follow_cmd.stdout(Stdio::piped()).spawn()?;
let out = let out = BufReader::new(child.stdout.take().ok_or_else(|| {
BufReader::new(child.stdout.take().ok_or_else(|| { Error::new(
Error::new(eyre!("{}", t!("logs.no-stdout-available")), crate::ErrorKind::Journald) eyre!("{}", t!("logs.no-stdout-available")),
})?); crate::ErrorKind::Journald,
)
})?);
let journalctl_entries = LinesStream::new(out.lines()); let journalctl_entries = LinesStream::new(out.lines());
@@ -701,7 +713,10 @@ pub async fn follow_logs<Context: AsRef<RpcContinuations>>(
RpcContinuation::ws( RpcContinuation::ws(
move |socket| async move { move |socket| async move {
if let Err(e) = ws_handler(first_entry, stream, socket).await { if let Err(e) = ws_handler(first_entry, stream, socket).await {
tracing::error!("{}", t!("logs.error-in-log-stream", error = e.to_string())); tracing::error!(
"{}",
t!("logs.error-in-log-stream", error = e.to_string())
);
} }
}, },
Duration::from_secs(30), Duration::from_secs(30),

View File

@@ -40,7 +40,10 @@ impl LocalAuthContext for RpcContext {
} }
fn unauthorized() -> Error { fn unauthorized() -> Error {
Error::new(eyre!("{}", t!("middleware.auth.unauthorized")), crate::ErrorKind::Authorization) Error::new(
eyre!("{}", t!("middleware.auth.unauthorized")),
crate::ErrorKind::Authorization,
)
} }
async fn check_from_header<C: LocalAuthContext>(header: Option<&HeaderValue>) -> Result<(), Error> { async fn check_from_header<C: LocalAuthContext>(header: Option<&HeaderValue>) -> Result<(), Error> {

View File

@@ -244,7 +244,10 @@ impl ValidSessionToken {
C::access_sessions(db) C::access_sessions(db)
.as_idx_mut(session_hash) .as_idx_mut(session_hash)
.ok_or_else(|| { .ok_or_else(|| {
Error::new(eyre!("{}", t!("middleware.auth.unauthorized")), crate::ErrorKind::Authorization) Error::new(
eyre!("{}", t!("middleware.auth.unauthorized")),
crate::ErrorKind::Authorization,
)
})? })?
.mutate(|s| { .mutate(|s| {
s.last_active = Utc::now(); s.last_active = Utc::now();

View File

@@ -347,6 +347,10 @@ pub async fn call_remote<Ctx: SigningContext + AsRef<Client>>(
.with_kind(ErrorKind::Deserialization)? .with_kind(ErrorKind::Deserialization)?
.result .result
} }
_ => Err(Error::new(eyre!("{}", t!("middleware.auth.unknown-content-type")), ErrorKind::Network).into()), _ => Err(Error::new(
eyre!("{}", t!("middleware.auth.unknown-content-type")),
ErrorKind::Network,
)
.into()),
} }
} }

View File

@@ -47,7 +47,13 @@ impl Middleware<RpcContext> for SyncDb {
} }
.await .await
{ {
tracing::error!("{}", t!("middleware.db.error-writing-patch-sequence-header", error = e)); tracing::error!(
"{}",
t!(
"middleware.db.error-writing-patch-sequence-header",
error = e
)
);
tracing::debug!("{e:?}"); tracing::debug!("{e:?}");
} }
} }

View File

@@ -240,7 +240,13 @@ impl PortForwardController {
} }
.await .await
{ {
tracing::error!("{}", t!("net.forward.error-initializing-controller", error = format!("{e:#}"))); tracing::error!(
"{}",
t!(
"net.forward.error-initializing-controller",
error = format!("{e:#}")
)
);
tracing::debug!("{e:?}"); tracing::debug!("{e:?}");
tokio::time::sleep(Duration::from_secs(5)).await; tokio::time::sleep(Duration::from_secs(5)).await;
} }

View File

@@ -171,16 +171,13 @@ where
let mut tls_handler = self.tls_handler.clone(); let mut tls_handler = self.tls_handler.clone();
let mut fut = async move { let mut fut = async move {
let res = async { let res = async {
let mut acceptor = LazyConfigAcceptor::new( let mut acceptor =
Acceptor::default(), LazyConfigAcceptor::new(Acceptor::default(), BackTrackingIO::new(stream));
BackTrackingIO::new(stream),
);
let mut mid: tokio_rustls::StartHandshake<BackTrackingIO<AcceptStream>> = let mut mid: tokio_rustls::StartHandshake<BackTrackingIO<AcceptStream>> =
match (&mut acceptor).await { match (&mut acceptor).await {
Ok(a) => a, Ok(a) => a,
Err(e) => { Err(e) => {
let mut stream = let mut stream = acceptor.take_io().or_not_found("acceptor io")?;
acceptor.take_io().or_not_found("acceptor io")?;
let (_, buf) = stream.rewind(); let (_, buf) = stream.rewind();
if std::str::from_utf8(buf) if std::str::from_utf8(buf)
.ok() .ok()

View File

@@ -324,7 +324,12 @@ pub async fn list_keys(ctx: RpcContext) -> Result<BTreeSet<OnionAddress>, Error>
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
#[command(rename_all = "kebab-case")] #[command(rename_all = "kebab-case")]
pub struct ResetParams { pub struct ResetParams {
#[arg(name = "wipe-state", short = 'w', long = "wipe-state", help = "help.arg.wipe-tor-state")] #[arg(
name = "wipe-state",
short = 'w',
long = "wipe-state",
help = "help.arg.wipe-tor-state"
)]
wipe_state: bool, wipe_state: bool,
} }

View File

@@ -351,7 +351,12 @@ pub async fn list_keys(ctx: RpcContext) -> Result<BTreeSet<OnionAddress>, Error>
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
#[command(rename_all = "kebab-case")] #[command(rename_all = "kebab-case")]
pub struct ResetParams { pub struct ResetParams {
#[arg(name = "wipe-state", short = 'w', long = "wipe-state", help = "help.arg.wipe-tor-state")] #[arg(
name = "wipe-state",
short = 'w',
long = "wipe-state",
help = "help.arg.wipe-tor-state"
)]
wipe_state: bool, wipe_state: bool,
#[arg(help = "help.arg.reset-reason")] #[arg(help = "help.arg.reset-reason")]
reason: String, reason: String,

View File

@@ -94,7 +94,12 @@ impl Model<BTreeMap<Guid, SignerInfo>> {
.next() .next()
.transpose()? .transpose()?
.map(|(a, _)| a) .map(|(a, _)| a)
.ok_or_else(|| Error::new(eyre!("{}", t!("registry.admin.unknown-signer")), ErrorKind::Authorization)) .ok_or_else(|| {
Error::new(
eyre!("{}", t!("registry.admin.unknown-signer")),
ErrorKind::Authorization,
)
})
} }
pub fn get_signer_info(&self, key: &AnyVerifyingKey) -> Result<(Guid, SignerInfo), Error> { pub fn get_signer_info(&self, key: &AnyVerifyingKey) -> Result<(Guid, SignerInfo), Error> {
@@ -104,7 +109,12 @@ impl Model<BTreeMap<Guid, SignerInfo>> {
.filter_ok(|(_, s)| s.keys.contains(key)) .filter_ok(|(_, s)| s.keys.contains(key))
.next() .next()
.transpose()? .transpose()?
.ok_or_else(|| Error::new(eyre!("{}", t!("registry.admin.unknown-signer")), ErrorKind::Authorization)) .ok_or_else(|| {
Error::new(
eyre!("{}", t!("registry.admin.unknown-signer")),
ErrorKind::Authorization,
)
})
} }
pub fn add_signer(&mut self, signer: &SignerInfo) -> Result<Guid, Error> { pub fn add_signer(&mut self, signer: &SignerInfo) -> Result<Guid, Error> {
@@ -119,7 +129,11 @@ impl Model<BTreeMap<Guid, SignerInfo>> {
return Err(Error::new( return Err(Error::new(
eyre!( eyre!(
"{}", "{}",
t!("registry.admin.signer-already-exists", guid = guid, name = s.name) t!(
"registry.admin.signer-already-exists",
guid = guid,
name = s.name
)
), ),
ErrorKind::InvalidRequest, ErrorKind::InvalidRequest,
)); ));

View File

@@ -44,7 +44,11 @@ const DEFAULT_REGISTRY_LISTEN: SocketAddr =
pub struct RegistryConfig { pub struct RegistryConfig {
#[arg(short = 'c', long = "config", help = "help.arg.config-file-path")] #[arg(short = 'c', long = "config", help = "help.arg.config-file-path")]
pub config: Option<PathBuf>, pub config: Option<PathBuf>,
#[arg(short = 'l', long = "listen", help = "help.arg.registry-listen-address")] #[arg(
short = 'l',
long = "listen",
help = "help.arg.registry-listen-address"
)]
pub registry_listen: Option<SocketAddr>, pub registry_listen: Option<SocketAddr>,
#[arg(short = 'H', long = "hostname", help = "help.arg.registry-hostname")] #[arg(short = 'H', long = "hostname", help = "help.arg.registry-hostname")]
pub registry_hostname: Vec<InternedString>, pub registry_hostname: Vec<InternedString>,
@@ -52,7 +56,11 @@ pub struct RegistryConfig {
pub tor_proxy: Option<Url>, pub tor_proxy: Option<Url>,
#[arg(short = 'd', long = "datadir", help = "help.arg.data-directory")] #[arg(short = 'd', long = "datadir", help = "help.arg.data-directory")]
pub datadir: Option<PathBuf>, pub datadir: Option<PathBuf>,
#[arg(short = 'u', long = "pg-connection-url", help = "help.arg.postgres-connection-url")] #[arg(
short = 'u',
long = "pg-connection-url",
help = "help.arg.postgres-connection-url"
)]
pub pg_connection_url: Option<String>, pub pg_connection_url: Option<String>,
} }
impl ContextConfig for RegistryConfig { impl ContextConfig for RegistryConfig {
@@ -195,9 +203,11 @@ impl CallRemote<RegistryContext> for CliContext {
.push("v0"); .push("v0");
url url
} else { } else {
return Err( return Err(Error::new(
Error::new(eyre!("{}", t!("registry.context.registry-required")), ErrorKind::InvalidRequest).into(), eyre!("{}", t!("registry.context.registry-required")),
); ErrorKind::InvalidRequest,
)
.into());
}; };
if let Ok(local) = cookie { if let Ok(local) = cookie {
@@ -331,7 +341,10 @@ impl SignatureAuthContext for RegistryContext {
} }
} }
Err(Error::new(eyre!("{}", t!("registry.context.unauthorized")), ErrorKind::Authorization)) Err(Error::new(
eyre!("{}", t!("registry.context.unauthorized")),
ErrorKind::Authorization,
))
} }
async fn post_auth_hook( async fn post_auth_hook(
&self, &self,

View File

@@ -154,7 +154,10 @@ async fn add_asset(
})?; })?;
Ok(()) Ok(())
} else { } else {
Err(Error::new(eyre!("{}", t!("registry.os.asset.unauthorized")), ErrorKind::Authorization)) Err(Error::new(
eyre!("{}", t!("registry.os.asset.unauthorized")),
ErrorKind::Authorization,
))
} }
}) })
.await .await
@@ -231,10 +234,12 @@ pub async fn cli_add_asset(
sign_phase.start(); sign_phase.start();
let blake3 = file.blake3_mmap().await?; let blake3 = file.blake3_mmap().await?;
let size = file let size = file.size().await.ok_or_else(|| {
.size() Error::new(
.await eyre!("{}", t!("registry.os.asset.failed-read-metadata")),
.ok_or_else(|| Error::new(eyre!("{}", t!("registry.os.asset.failed-read-metadata")), ErrorKind::Filesystem))?; ErrorKind::Filesystem,
)
})?;
let commitment = Blake3Commitment { let commitment = Blake3Commitment {
hash: Base64(*blake3.as_bytes()), hash: Base64(*blake3.as_bytes()),
size, size,
@@ -336,7 +341,10 @@ async fn remove_asset(
.remove(&platform)?; .remove(&platform)?;
Ok(()) Ok(())
} else { } else {
Err(Error::new(eyre!("{}", t!("registry.os.asset.unauthorized")), ErrorKind::Authorization)) Err(Error::new(
eyre!("{}", t!("registry.os.asset.unauthorized")),
ErrorKind::Authorization,
))
} }
}) })
.await .await

View File

@@ -125,17 +125,9 @@ pub struct CliGetOsAssetParams {
pub version: Version, pub version: Version,
#[arg(help = "help.arg.platform")] #[arg(help = "help.arg.platform")]
pub platform: InternedString, pub platform: InternedString,
#[arg( #[arg(long = "download", short = 'd', help = "help.arg.download-directory")]
long = "download",
short = 'd',
help = "help.arg.download-directory"
)]
pub download: Option<PathBuf>, pub download: Option<PathBuf>,
#[arg( #[arg(long = "reverify", short = 'r', help = "help.arg.reverify-hash")]
long = "reverify",
short = 'r',
help = "help.arg.reverify-hash"
)]
pub reverify: bool, pub reverify: bool,
} }

View File

@@ -89,7 +89,10 @@ async fn sign_asset(
.contains(&guid) .contains(&guid)
{ {
return Err(Error::new( return Err(Error::new(
eyre!("{}", t!("registry.os.asset.signer-not-authorized", guid = guid)), eyre!(
"{}",
t!("registry.os.asset.signer-not-authorized", guid = guid)
),
ErrorKind::Authorization, ErrorKind::Authorization,
)); ));
} }
@@ -184,10 +187,12 @@ pub async fn cli_sign_asset(
sign_phase.start(); sign_phase.start();
let blake3 = file.blake3_mmap().await?; let blake3 = file.blake3_mmap().await?;
let size = file let size = file.size().await.ok_or_else(|| {
.size() Error::new(
.await eyre!("{}", t!("registry.os.asset.failed-read-metadata")),
.ok_or_else(|| Error::new(eyre!("{}", t!("registry.os.asset.failed-read-metadata")), ErrorKind::Filesystem))?; ErrorKind::Filesystem,
)
})?;
let commitment = Blake3Commitment { let commitment = Blake3Commitment {
hash: Base64(*blake3.as_bytes()), hash: Base64(*blake3.as_bytes()),
size, size,

View File

@@ -26,7 +26,6 @@ pub fn os_api<C: Context>() -> ParentHandler<C> {
) )
.subcommand( .subcommand(
"version", "version",
version::version_api::<C>() version::version_api::<C>().with_about("about.commands-add-remove-list-versions"),
.with_about("about.commands-add-remove-list-versions"),
) )
} }

View File

@@ -95,7 +95,14 @@ pub async fn remove_version_signer(
.mutate(|s| Ok(s.remove(&signer)))? .mutate(|s| Ok(s.remove(&signer)))?
{ {
return Err(Error::new( return Err(Error::new(
eyre!("{}", t!("registry.os.version.signer-not-authorized", signer = signer, version = version)), eyre!(
"{}",
t!(
"registry.os.version.signer-not-authorized",
signer = signer,
version = version
)
),
ErrorKind::NotFound, ErrorKind::NotFound,
)); ));
} }

View File

@@ -112,7 +112,10 @@ pub async fn add_package(
Ok(()) Ok(())
} else { } else {
Err(Error::new(eyre!("{}", t!("registry.package.add.unauthorized")), ErrorKind::Authorization)) Err(Error::new(
eyre!("{}", t!("registry.package.add.unauthorized")),
ErrorKind::Authorization,
))
} }
}) })
.await .await
@@ -132,20 +135,24 @@ pub struct CliAddPackageParams {
} }
pub async fn cli_add_package( pub async fn cli_add_package(
HandlerArgs { ctx: CliContext,
context: ctx, CliAddPackageParams {
parent_method, file,
method, url,
params: no_verify,
CliAddPackageParams { }: CliAddPackageParams,
file,
url,
no_verify,
},
..
}: HandlerArgs<CliContext, CliAddPackageParams>,
) -> Result<(), Error> { ) -> Result<(), Error> {
let s9pk = S9pk::open(&file, None).await?; let s9pk = S9pk::open(&file, None).await?;
cli_add_package_impl(ctx, s9pk, url, no_verify).await
}
pub async fn cli_add_package_impl(
ctx: CliContext,
s9pk: S9pk,
url: Vec<Url>,
no_verify: bool,
) -> Result<(), Error> {
let manifest = s9pk.as_manifest();
let progress = FullProgressTracker::new(); let progress = FullProgressTracker::new();
let mut sign_phase = progress.add_phase(InternedString::intern("Signing File"), Some(1)); let mut sign_phase = progress.add_phase(InternedString::intern("Signing File"), Some(1));
@@ -167,8 +174,16 @@ pub async fn cli_add_package(
Some(1), Some(1),
); );
let progress_task = let progress_task = progress.progress_bar_task(&format!(
progress.progress_bar_task(&format!("Adding {} to registry...", file.display())); "Adding {}@{}{} to registry...",
manifest.id,
manifest.version,
manifest
.hardware_requirements
.arch
.as_ref()
.map_or(String::new(), |a| format!(" ({})", a.iter().join("/")))
));
sign_phase.start(); sign_phase.start();
let commitment = s9pk.as_archive().commitment().await?; let commitment = s9pk.as_archive().commitment().await?;
@@ -185,7 +200,7 @@ pub async fn cli_add_package(
index_phase.start(); index_phase.start();
ctx.call_remote::<RegistryContext>( ctx.call_remote::<RegistryContext>(
&parent_method.into_iter().chain(method).join("."), "package.add",
imbl_value::json!({ imbl_value::json!({
"urls": &url, "urls": &url,
"signature": AnySignature::Ed25519(signature), "signature": AnySignature::Ed25519(signature),
@@ -228,8 +243,12 @@ pub async fn remove_package(
}: RemovePackageParams, }: RemovePackageParams,
) -> Result<bool, Error> { ) -> Result<bool, Error> {
let peek = ctx.db.peek().await; let peek = ctx.db.peek().await;
let signer = let signer = signer.ok_or_else(|| {
signer.ok_or_else(|| Error::new(eyre!("{}", t!("registry.package.missing-signer")), ErrorKind::InvalidRequest))?; Error::new(
eyre!("{}", t!("registry.package.missing-signer")),
ErrorKind::InvalidRequest,
)
})?;
let signer_guid = peek.as_index().as_signers().get_signer(&signer)?; let signer_guid = peek.as_index().as_signers().get_signer(&signer)?;
let rev = ctx let rev = ctx
@@ -270,7 +289,10 @@ pub async fn remove_package(
} }
Ok(()) Ok(())
} else { } else {
Err(Error::new(eyre!("{}", t!("registry.package.unauthorized")), ErrorKind::Authorization)) Err(Error::new(
eyre!("{}", t!("registry.package.unauthorized")),
ErrorKind::Authorization,
))
} }
}) })
.await; .await;
@@ -345,7 +367,10 @@ pub async fn add_mirror(
Ok(()) Ok(())
} else { } else {
Err(Error::new(eyre!("{}", t!("registry.package.add-mirror.unauthorized")), ErrorKind::Authorization)) Err(Error::new(
eyre!("{}", t!("registry.package.add-mirror.unauthorized")),
ErrorKind::Authorization,
))
} }
}) })
.await .await
@@ -461,8 +486,12 @@ pub async fn remove_mirror(
}: RemoveMirrorParams, }: RemoveMirrorParams,
) -> Result<(), Error> { ) -> Result<(), Error> {
let peek = ctx.db.peek().await; let peek = ctx.db.peek().await;
let signer = let signer = signer.ok_or_else(|| {
signer.ok_or_else(|| Error::new(eyre!("{}", t!("registry.package.missing-signer")), ErrorKind::InvalidRequest))?; Error::new(
eyre!("{}", t!("registry.package.missing-signer")),
ErrorKind::InvalidRequest,
)
})?;
let signer_guid = peek.as_index().as_signers().get_signer(&signer)?; let signer_guid = peek.as_index().as_signers().get_signer(&signer)?;
ctx.db ctx.db
@@ -501,7 +530,10 @@ pub async fn remove_mirror(
} }
Ok(()) Ok(())
} else { } else {
Err(Error::new(eyre!("{}", t!("registry.package.remove-mirror.unauthorized")), ErrorKind::Authorization)) Err(Error::new(
eyre!("{}", t!("registry.package.remove-mirror.unauthorized")),
ErrorKind::Authorization,
))
} }
}) })
.await .await

View File

@@ -52,10 +52,14 @@ pub fn package_api<C: Context>() -> ParentHandler<C> {
if !changed { if !changed {
tracing::warn!( tracing::warn!(
"{}", "{}",
t!("registry.package.remove-not-exist", t!(
"registry.package.remove-not-exist",
id = args.params.id, id = args.params.id,
version = args.params.version, version = args.params.version,
sighash = args.params.sighash.map_or(String::new(), |h| format!("#{h}")) sighash = args
.params
.sighash
.map_or(String::new(), |h| format!("#{h}"))
) )
); );
} }
@@ -96,7 +100,6 @@ pub fn package_api<C: Context>() -> ParentHandler<C> {
) )
.subcommand( .subcommand(
"category", "category",
category::category_api::<C>() category::category_api::<C>().with_about("about.update-categories-registry"),
.with_about("about.update-categories-registry"),
) )
} }

View File

@@ -118,7 +118,14 @@ pub async fn remove_package_signer(
.is_some() .is_some()
{ {
return Err(Error::new( return Err(Error::new(
eyre!("{}", t!("registry.package.signer.not-authorized", signer = signer, id = id)), eyre!(
"{}",
t!(
"registry.package.signer.not-authorized",
signer = signer,
id = id
)
),
ErrorKind::NotFound, ErrorKind::NotFound,
)); ));
} }

View File

@@ -1,10 +1,13 @@
use std::ops::Deref;
use std::path::PathBuf; use std::path::PathBuf;
use std::sync::Arc; use std::sync::Arc;
use clap::Parser; use clap::Parser;
use rpc_toolkit::{Empty, HandlerExt, ParentHandler, from_fn_async}; use rpc_toolkit::{Empty, HandlerExt, ParentHandler, from_fn_async};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use tokio::process::Command;
use ts_rs::TS; use ts_rs::TS;
use url::Url;
use crate::ImageId; use crate::ImageId;
use crate::context::CliContext; use crate::context::CliContext;
@@ -13,9 +16,9 @@ use crate::s9pk::manifest::Manifest;
use crate::s9pk::merkle_archive::source::multi_cursor_file::MultiCursorFile; use crate::s9pk::merkle_archive::source::multi_cursor_file::MultiCursorFile;
use crate::s9pk::v2::SIG_CONTEXT; use crate::s9pk::v2::SIG_CONTEXT;
use crate::s9pk::v2::pack::ImageConfig; use crate::s9pk::v2::pack::ImageConfig;
use crate::util::Apply;
use crate::util::io::{TmpDir, create_file, open_file}; use crate::util::io::{TmpDir, create_file, open_file};
use crate::util::serde::{HandlerExtSerde, apply_expr}; use crate::util::serde::{HandlerExtSerde, apply_expr};
use crate::util::{Apply, Invoke};
pub const SKIP_ENV: &[&str] = &["TERM", "container", "HOME", "HOSTNAME"]; pub const SKIP_ENV: &[&str] = &["TERM", "container", "HOME", "HOSTNAME"];
@@ -61,6 +64,12 @@ pub fn s9pk() -> ParentHandler<CliContext> {
.no_display() .no_display()
.with_about("about.convert-s9pk-v1-to-v2"), .with_about("about.convert-s9pk-v1-to-v2"),
) )
.subcommand(
"publish",
from_fn_async(publish)
.no_display()
.with_about("about.publish-s9pk"),
)
} }
#[derive(Deserialize, Serialize, Parser)] #[derive(Deserialize, Serialize, Parser)]
@@ -256,3 +265,61 @@ async fn convert(ctx: CliContext, S9pkPath { s9pk: s9pk_path }: S9pkPath) -> Res
tokio::fs::rename(tmp_path, s9pk_path).await?; tokio::fs::rename(tmp_path, s9pk_path).await?;
Ok(()) Ok(())
} }
async fn publish(ctx: CliContext, S9pkPath { s9pk: s9pk_path }: S9pkPath) -> Result<(), Error> {
let filename = s9pk_path.file_name().unwrap().to_string_lossy();
let s9pk = super::S9pk::open(&s9pk_path, None).await?;
let manifest = s9pk.as_manifest();
let path = [
manifest.id.deref(),
manifest.version.as_str(),
filename.deref(),
];
let mut s3url = ctx
.s9pk_s3base
.as_ref()
.ok_or_else(|| Error::new(eyre!("--s9pk-s3base required"), ErrorKind::InvalidRequest))?
.clone();
s3url
.path_segments_mut()
.map_err(|_| {
Error::new(
eyre!("s9pk-s3base is invalid (missing protocol?)"),
ErrorKind::ParseUrl,
)
})?
.pop_if_empty()
.extend(path);
let mut s3dest = format!(
"s3://{}",
ctx.s9pk_s3bucket
.as_deref()
.or_else(|| s3url
.host_str()
.and_then(|h| h.split_once(".").map(|h| h.0)))
.ok_or_else(|| {
Error::new(eyre!("--s9pk-s3bucket required"), ErrorKind::InvalidRequest)
})?,
)
.parse::<Url>()?;
s3dest
.path_segments_mut()
.map_err(|_| {
Error::new(
eyre!("s9pk-s3base is invalid (missing protocol?)"),
ErrorKind::ParseUrl,
)
})?
.pop_if_empty()
.extend(path);
Command::new("s3cmd")
.arg("put")
.arg("-P")
.arg(s9pk_path)
.arg(s3dest.as_str())
.capture(false)
.invoke(ErrorKind::Network)
.await?;
crate::registry::package::add::cli_add_package_impl(ctx, s9pk, vec![s3url], false).await
}

View File

@@ -7,6 +7,7 @@ use clap::Parser;
use futures::future::{BoxFuture, ready}; use futures::future::{BoxFuture, ready};
use futures::{FutureExt, TryStreamExt}; use futures::{FutureExt, TryStreamExt};
use imbl_value::InternedString; use imbl_value::InternedString;
use itertools::Itertools;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use tokio::process::Command; use tokio::process::Command;
use tokio::sync::OnceCell; use tokio::sync::OnceCell;
@@ -385,13 +386,17 @@ impl ImageSource {
pub fn ingredients(&self) -> Vec<PathBuf> { pub fn ingredients(&self) -> Vec<PathBuf> {
match self { match self {
Self::Packed => Vec::new(), Self::Packed => Vec::new(),
Self::DockerBuild { dockerfile, .. } => { Self::DockerBuild {
vec![ dockerfile,
dockerfile workdir,
..
} => {
vec![dockerfile.clone().unwrap_or_else(|| {
workdir
.as_deref() .as_deref()
.unwrap_or(Path::new("Dockerfile")) .unwrap_or(Path::new("."))
.to_owned(), .join("Dockerfile")
] })]
} }
Self::DockerTag(_) => Vec::new(), Self::DockerTag(_) => Vec::new(),
} }
@@ -682,7 +687,7 @@ pub async fn pack(ctx: CliContext, params: PackParams) -> Result<(), Error> {
let manifest = s9pk.as_manifest_mut(); let manifest = s9pk.as_manifest_mut();
manifest.git_hash = Some(GitHash::from_path(params.path()).await?); manifest.git_hash = Some(GitHash::from_path(params.path()).await?);
if !params.arch.is_empty() { if !params.arch.is_empty() {
let arches = match manifest.hardware_requirements.arch.take() { let arches: BTreeSet<InternedString> = match manifest.hardware_requirements.arch.take() {
Some(a) => params Some(a) => params
.arch .arch
.iter() .iter()
@@ -691,10 +696,41 @@ pub async fn pack(ctx: CliContext, params: PackParams) -> Result<(), Error> {
.collect(), .collect(),
None => params.arch.iter().cloned().collect(), None => params.arch.iter().cloned().collect(),
}; };
manifest if arches.is_empty() {
.images return Err(Error::new(
.values_mut() eyre!(
.for_each(|c| c.arch = c.arch.intersection(&arches).cloned().collect()); "none of the requested architectures ({:?}) are supported by this package",
params.arch
),
ErrorKind::InvalidRequest,
));
}
manifest.images.iter_mut().for_each(|(id, c)| {
let filtered = c
.arch
.intersection(&arches)
.cloned()
.collect::<BTreeSet<_>>();
if filtered.is_empty() {
if let Some(arch) = &c.emulate_missing_as {
tracing::warn!(
"ImageId {} is not available for {}, emulating as {}",
id,
arches.iter().join("/"),
arch
);
c.arch = [arch.clone()].into_iter().collect();
} else {
tracing::error!(
"ImageId {} is not available for {}",
id,
arches.iter().join("/"),
);
}
} else {
c.arch = filtered;
}
});
manifest.hardware_requirements.arch = Some(arches); manifest.hardware_requirements.arch = Some(arches);
} }

View File

@@ -102,7 +102,13 @@ pub fn update_tasks(
} }
} }
None => { None => {
tracing::error!("{}", t!("service.action.action-request-invalid-state", task = format!("{:?}", v.task))); tracing::error!(
"{}",
t!(
"service.action.action-request-invalid-state",
task = format!("{:?}", v.task)
)
);
} }
}, },
} }
@@ -151,7 +157,10 @@ impl Handler<RunAction> for ServiceActor {
.de()?; .de()?;
if matches!(&action.visibility, ActionVisibility::Disabled(_)) { if matches!(&action.visibility, ActionVisibility::Disabled(_)) {
return Err(Error::new( return Err(Error::new(
eyre!("{}", t!("service.action.action-is-disabled", action_id = action_id)), eyre!(
"{}",
t!("service.action.action-is-disabled", action_id = action_id)
),
ErrorKind::Action, ErrorKind::Action,
)); ));
} }
@@ -162,7 +171,13 @@ impl Handler<RunAction> for ServiceActor {
_ => false, _ => false,
} { } {
return Err(Error::new( return Err(Error::new(
eyre!("{}", t!("service.action.service-not-in-allowed-status", action_id = action_id)), eyre!(
"{}",
t!(
"service.action.service-not-in-allowed-status",
action_id = action_id
)
),
ErrorKind::Action, ErrorKind::Action,
)); ));
} }

View File

@@ -181,7 +181,10 @@ async fn run_action(
if package_id != &context.seed.id { if package_id != &context.seed.id {
return Err(Error::new( return Err(Error::new(
eyre!("{}", t!("service.effects.action.calling-actions-on-other-packages-unsupported")), eyre!(
"{}",
t!("service.effects.action.calling-actions-on-other-packages-unsupported")
),
ErrorKind::InvalidRequest, ErrorKind::InvalidRequest,
)); ));
context context
@@ -226,7 +229,10 @@ async fn create_task(
TaskCondition::InputNotMatches => { TaskCondition::InputNotMatches => {
let Some(input) = task.input.as_ref() else { let Some(input) = task.input.as_ref() else {
return Err(Error::new( return Err(Error::new(
eyre!("{}", t!("service.effects.action.input-not-matches-requires-input")), eyre!(
"{}",
t!("service.effects.action.input-not-matches-requires-input")
),
ErrorKind::InvalidRequest, ErrorKind::InvalidRequest,
)); ));
}; };
@@ -244,7 +250,12 @@ async fn create_task(
else { else {
return Err(Error::new( return Err(Error::new(
eyre!( eyre!(
"{}", t!("service.effects.action.action-has-no-input", action_id = task.action_id, package_id = task.package_id) "{}",
t!(
"service.effects.action.action-has-no-input",
action_id = task.action_id,
package_id = task.package_id
)
), ),
ErrorKind::InvalidRequest, ErrorKind::InvalidRequest,
)); ));

View File

@@ -79,7 +79,7 @@ pub async fn mount(
} }
IdMapped::new( IdMapped::new(
Bind::new(source).with_type(filetype), Bind::new(source).with_type(filetype).recursive(true),
IdMap::stack( IdMap::stack(
vec![IdMap { vec![IdMap {
from_id: 0, from_id: 0,

View File

@@ -10,6 +10,7 @@ use crate::rpc_continuations::Guid;
use crate::service::effects::prelude::*; use crate::service::effects::prelude::*;
use crate::service::persistent_container::Subcontainer; use crate::service::persistent_container::Subcontainer;
use crate::util::Invoke; use crate::util::Invoke;
use crate::util::io::write_file_owned_atomic;
pub const NVIDIA_OVERLAY_PATH: &str = "/var/tmp/startos/nvidia-overlay"; pub const NVIDIA_OVERLAY_PATH: &str = "/var/tmp/startos/nvidia-overlay";
pub const NVIDIA_OVERLAY_DEBIAN: &str = "/var/tmp/startos/nvidia-overlay/debian"; pub const NVIDIA_OVERLAY_DEBIAN: &str = "/var/tmp/startos/nvidia-overlay/debian";
@@ -94,7 +95,7 @@ pub async fn create_subcontainer_fs(
.cloned() .cloned()
{ {
let guid = Guid::new(); let guid = Guid::new();
let rootfs_dir = context let lxc_container = context
.seed .seed
.persistent_container .persistent_container
.lxc_container .lxc_container
@@ -104,8 +105,9 @@ pub async fn create_subcontainer_fs(
eyre!("PersistentContainer has been destroyed"), eyre!("PersistentContainer has been destroyed"),
ErrorKind::Incoherent, ErrorKind::Incoherent,
) )
})? })?;
.rootfs_dir(); let container_guid = &lxc_container.guid;
let rootfs_dir = lxc_container.rootfs_dir();
let mountpoint = rootfs_dir let mountpoint = rootfs_dir
.join("media/startos/subcontainers") .join("media/startos/subcontainers")
.join(guid.as_ref()); .join(guid.as_ref());
@@ -154,6 +156,20 @@ pub async fn create_subcontainer_fs(
.arg(&mountpoint) .arg(&mountpoint)
.invoke(ErrorKind::Filesystem) .invoke(ErrorKind::Filesystem)
.await?; .await?;
write_file_owned_atomic(
mountpoint.join("etc/hostname"),
format!("{container_guid}\n"),
100000,
100000,
)
.await?;
write_file_owned_atomic(
mountpoint.join("etc/hosts"),
format!("127.0.0.1\tlocalhost\n127.0.1.1\t{container_guid}\n::1\tlocalhost ip6-localhost ip6-loopback\n"),
100000,
100000,
)
.await?;
tracing::info!("Mounted overlay {guid} for {image_id}"); tracing::info!("Mounted overlay {guid} for {image_id}");
context context
.seed .seed

View File

@@ -1,7 +1,6 @@
use std::collections::BTreeMap;
use std::ffi::{OsStr, OsString, c_int}; use std::ffi::{OsStr, OsString, c_int};
use std::fs::File; use std::fs::File;
use std::io::{IsTerminal, Read}; use std::io::{BufRead, BufReader, IsTerminal, Read};
use std::os::unix::process::{CommandExt, ExitStatusExt}; use std::os::unix::process::{CommandExt, ExitStatusExt};
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::process::{Command as StdCommand, Stdio}; use std::process::{Command as StdCommand, Stdio};
@@ -146,95 +145,160 @@ impl ExecParams {
let mut cmd = StdCommand::new(command); let mut cmd = StdCommand::new(command);
let passwd = std::fs::read_to_string(chroot.join("etc/passwd")) let mut uid = Err(None);
.with_ctx(|_| (ErrorKind::Filesystem, "read /etc/passwd")) let mut gid = Err(None);
.log_err() let mut needs_home = true;
.unwrap_or_default();
let mut home = None;
if let Some((uid, gid)) = if let Some(user) = user {
if let Some(uid) = user.as_deref().and_then(|u| u.parse::<u32>().ok()) { if let Some((u, g)) = user.split_once(":") {
Some((uid, uid)) uid = Err(Some(u));
} else if let Some((uid, gid)) = user gid = Err(Some(g));
.as_deref()
.and_then(|u| u.split_once(":"))
.and_then(|(u, g)| Some((u.parse::<u32>().ok()?, g.parse::<u32>().ok()?)))
{
Some((uid, gid))
} else if let Some(user) = user {
Some(
if let Some((uid, gid)) = passwd.lines().find_map(|l| {
let l = l.trim();
let mut split = l.split(":");
if user != split.next()? {
return None;
}
split.next(); // throw away x
let uid = split.next()?.parse().ok()?;
let gid = split.next()?.parse().ok()?;
split.next(); // throw away group name
home = split.next();
Some((uid, gid))
// uid gid
}) {
(uid, gid)
} else if user == "root" {
(0, 0)
} else {
None.or_not_found(lazy_format!("{user} in /etc/passwd"))?
},
)
} else { } else {
None uid = Err(Some(user));
} }
{
if home.is_none() {
home = passwd.lines().find_map(|l| {
let l = l.trim();
let mut split = l.split(":");
split.next(); // throw away user name
split.next(); // throw away x
if split.next()?.parse::<u32>().ok()? != uid {
return None;
}
split.next(); // throw away gid
split.next(); // throw away group name
split.next()
})
};
std::os::unix::fs::chown("/proc/self/fd/0", Some(uid), Some(gid)).ok();
std::os::unix::fs::chown("/proc/self/fd/1", Some(uid), Some(gid)).ok();
std::os::unix::fs::chown("/proc/self/fd/2", Some(uid), Some(gid)).ok();
cmd.uid(uid);
cmd.gid(gid);
} else {
home = Some("/root");
} }
cmd.env("HOME", home.unwrap_or("/"));
let env_string = if let Some(env_file) = &env_file { if let Some(u) = uid.err().flatten().and_then(|u| u.parse::<u32>().ok()) {
std::fs::read_to_string(env_file) uid = Ok(u);
.with_ctx(|_| (ErrorKind::Filesystem, lazy_format!("read {env:?}")))? }
} else { if let Some(g) = gid.err().flatten().and_then(|g| g.parse::<u32>().ok()) {
Default::default() gid = Ok(g);
}
let mut update_env = |line: &str| {
if let Some((k, v)) = line.split_once("=") {
needs_home &= k != "HOME";
cmd.env(k, v);
} else {
tracing::warn!("Invalid line in env: {line}");
}
}; };
let env = env_string if let Some(f) = env_file {
.lines() let mut lines = BufReader::new(
.chain(env.iter().map(|l| l.as_str())) File::open(&f).with_ctx(|_| (ErrorKind::Filesystem, format!("open r {f:?}")))?,
.map(|l| l.trim()) )
.filter_map(|l| l.split_once("=")) .lines();
.collect::<BTreeMap<_, _>>(); while let Some(line) = lines.next().transpose()? {
update_env(&line);
}
}
for line in env {
update_env(&line);
}
let needs_gid = Err(None) == gid;
let mut username = InternedString::intern("root");
let mut handle_passwd_line = |line: &str| -> Option<()> {
let l = line.trim();
let mut split = l.split(":");
let user = split.next()?;
match uid {
Err(Some(u)) if u != user => return None,
_ => (),
}
split.next(); // throw away x
let u: u32 = split.next()?.parse().ok()?;
match uid {
Err(Some(_)) => uid = Ok(u),
Err(None) if u == 0 => uid = Ok(u),
Ok(uid) if uid != u => return None,
_ => (),
}
username = user.into();
if !needs_gid && !needs_home {
return Some(());
}
let g = split.next()?;
if needs_gid {
gid = Ok(g.parse().ok()?);
}
if needs_home {
split.next(); // throw away group name
let home = split.next()?;
cmd.env("HOME", home);
}
Some(())
};
let mut lines = BufReader::new(
File::open(chroot.join("etc/passwd"))
.with_ctx(|_| (ErrorKind::Filesystem, format!("open r /etc/passwd")))?,
)
.lines();
while let Some(line) = lines.next().transpose()? {
if handle_passwd_line(&line).is_some() {
break;
}
}
let mut groups = Vec::new();
let mut handle_group_line = |line: &str| -> Option<()> {
let l = line.trim();
let mut split = l.split(":");
let name = split.next()?;
split.next()?; // throw away x
let g = split.next()?.parse::<u32>().ok()?;
match gid {
Err(Some(n)) if n == name => gid = Ok(g),
_ => (),
}
let users = split.next()?;
if users.split(",").any(|u| u == &*username) {
groups.push(nix::unistd::Gid::from_raw(g));
}
Some(())
};
let mut lines = BufReader::new(
File::open(chroot.join("etc/group"))
.with_ctx(|_| (ErrorKind::Filesystem, format!("open r /etc/group")))?,
)
.lines();
while let Some(line) = lines.next().transpose()? {
if handle_group_line(&line).is_none() {
tracing::warn!("Invalid /etc/group line: {line}");
}
}
std::os::unix::fs::chroot(chroot) std::os::unix::fs::chroot(chroot)
.with_ctx(|_| (ErrorKind::Filesystem, lazy_format!("chroot {chroot:?}")))?; .with_ctx(|_| (ErrorKind::Filesystem, lazy_format!("chroot {chroot:?}")))?;
cmd.args(args); if let Ok(uid) = uid {
for (k, v) in env { if uid != 0 {
cmd.env(k, v); std::os::unix::fs::chown("/proc/self/fd/0", Some(uid), gid.ok()).ok();
std::os::unix::fs::chown("/proc/self/fd/1", Some(uid), gid.ok()).ok();
std::os::unix::fs::chown("/proc/self/fd/2", Some(uid), gid.ok()).ok();
}
} }
// Handle credential changes in pre_exec to control the order:
// setgroups must happen before setgid/setuid (requires CAP_SETGID)
{
let set_uid = uid.ok();
let set_gid = gid.ok();
unsafe {
cmd.pre_exec(move || {
if !groups.is_empty() {
nix::unistd::setgroups(&groups)
.map_err(|e| std::io::Error::from_raw_os_error(e as i32))?;
}
if let Some(gid) = set_gid {
nix::unistd::setgid(nix::unistd::Gid::from_raw(gid))
.map_err(|e| std::io::Error::from_raw_os_error(e as i32))?;
}
if let Some(uid) = set_uid {
nix::unistd::setuid(nix::unistd::Uid::from_raw(uid))
.map_err(|e| std::io::Error::from_raw_os_error(e as i32))?;
}
Ok(())
});
}
}
cmd.args(args);
if let Some(workdir) = workdir { if let Some(workdir) = workdir {
cmd.current_dir(workdir); cmd.current_dir(workdir);

View File

@@ -28,7 +28,6 @@ use tokio_tungstenite::tungstenite::protocol::frame::coding::CloseCode;
use ts_rs::TS; use ts_rs::TS;
use url::Url; use url::Url;
use crate::context::{CliContext, RpcContext}; use crate::context::{CliContext, RpcContext};
use crate::db::model::package::{ use crate::db::model::package::{
InstalledState, ManifestPreference, PackageState, PackageStateMatchModelRef, TaskSeverity, InstalledState, ManifestPreference, PackageState, PackageStateMatchModelRef, TaskSeverity,
@@ -51,6 +50,7 @@ use crate::util::io::{AsyncReadStream, AtomicFile, TermSize, delete_file};
use crate::util::net::WebSocket; use crate::util::net::WebSocket;
use crate::util::serde::Pem; use crate::util::serde::Pem;
use crate::util::sync::SyncMutex; use crate::util::sync::SyncMutex;
use crate::util::tui::choose;
use crate::volume::data_dir; use crate::volume::data_dir;
use crate::{ActionId, CAP_1_KiB, DATA_DIR, HostId, ImageId, PackageId}; use crate::{ActionId, CAP_1_KiB, DATA_DIR, HostId, ImageId, PackageId};
@@ -184,7 +184,10 @@ impl ServiceRef {
Arc::try_unwrap(service.seed) Arc::try_unwrap(service.seed)
.map_err(|_| { .map_err(|_| {
Error::new( Error::new(
eyre!("{}", t!("service.mod.service-actor-seed-held-after-shutdown")), eyre!(
"{}",
t!("service.mod.service-actor-seed-held-after-shutdown")
),
ErrorKind::Unknown, ErrorKind::Unknown,
) )
})? })?
@@ -376,12 +379,16 @@ impl Service {
{ {
Ok(PackageState::Installed(InstalledState { manifest })) Ok(PackageState::Installed(InstalledState { manifest }))
} else { } else {
Err(Error::new(eyre!("{}", t!("service.mod.race-condition-detected")), ErrorKind::Database)) Err(Error::new(
eyre!("{}", t!("service.mod.race-condition-detected")),
ErrorKind::Database,
))
} }
}) })
} }
}) })
.await.result?; .await
.result?;
handle_installed(s9pk).await handle_installed(s9pk).await
} }
PackageStateMatchModelRef::Removing(_) | PackageStateMatchModelRef::Restoring(_) => { PackageStateMatchModelRef::Removing(_) | PackageStateMatchModelRef::Restoring(_) => {
@@ -447,7 +454,13 @@ impl Service {
handle_installed(S9pk::open(s9pk_path, Some(id)).await?).await handle_installed(S9pk::open(s9pk_path, Some(id)).await?).await
} }
PackageStateMatchModelRef::Error(e) => Err(Error::new( PackageStateMatchModelRef::Error(e) => Err(Error::new(
eyre!("{}", t!("service.mod.failed-to-parse-package-data-entry", error = format!("{e:?}"))), eyre!(
"{}",
t!(
"service.mod.failed-to-parse-package-data-entry",
error = format!("{e:?}")
)
),
ErrorKind::Deserialization, ErrorKind::Deserialization,
)), )),
} }
@@ -553,7 +566,11 @@ impl Service {
true true
} else { } else {
tracing::warn!( tracing::warn!(
"{}", t!("service.mod.deleting-task-action-no-longer-exists", id = id) "{}",
t!(
"service.mod.deleting-task-action-no-longer-exists",
id = id
)
); );
false false
} }
@@ -693,6 +710,19 @@ pub async fn rebuild(ctx: RpcContext, RebuildParams { id }: RebuildParams) -> Re
Ok(()) Ok(())
} }
#[derive(Debug, Deserialize, Serialize)]
pub struct SubcontainerInfo {
pub id: Guid,
pub name: InternedString,
pub image_id: ImageId,
}
impl std::fmt::Display for SubcontainerInfo {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let SubcontainerInfo { id, name, image_id } = self;
write!(f, "{id} => Name: {name}; Image: {image_id}")
}
}
#[derive(Deserialize, Serialize, TS)] #[derive(Deserialize, Serialize, TS)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct AttachParams { pub struct AttachParams {
@@ -706,7 +736,7 @@ pub struct AttachParams {
#[serde(rename = "__Auth_session")] #[serde(rename = "__Auth_session")]
session: Option<InternedString>, session: Option<InternedString>,
#[ts(type = "string | null")] #[ts(type = "string | null")]
subcontainer: Option<InternedString>, subcontainer: Option<Guid>,
#[ts(type = "string | null")] #[ts(type = "string | null")]
name: Option<InternedString>, name: Option<InternedString>,
#[ts(type = "string | null")] #[ts(type = "string | null")]
@@ -729,7 +759,7 @@ pub async fn attach(
user, user,
}: AttachParams, }: AttachParams,
) -> Result<Guid, Error> { ) -> Result<Guid, Error> {
let (container_id, subcontainer_id, image_id, workdir, root_command) = { let (container_id, subcontainer_id, image_id, user, workdir, root_command) = {
let id = &id; let id = &id;
let service = ctx.services.get(id).await; let service = ctx.services.get(id).await;
@@ -770,13 +800,6 @@ pub async fn attach(
} }
}) })
.collect(); .collect();
let format_subcontainer_pair = |(guid, wrapper): (&Guid, &Subcontainer)| {
format!(
"{guid} imageId: {image_id} name: \"{name}\"",
name = &wrapper.name,
image_id = &wrapper.image_id
)
};
let Some((subcontainer_id, image_id)) = subcontainer_ids let Some((subcontainer_id, image_id)) = subcontainer_ids
.first() .first()
.map::<(Guid, ImageId), _>(|&x| (x.0.clone(), x.1.image_id.clone())) .map::<(Guid, ImageId), _>(|&x| (x.0.clone(), x.1.image_id.clone()))
@@ -787,14 +810,17 @@ pub async fn attach(
.lock() .lock()
.await .await
.iter() .iter()
.map(format_subcontainer_pair) .map(|(g, s)| SubcontainerInfo {
.join("\n"); id: g.clone(),
name: s.name.clone(),
image_id: s.image_id.clone(),
})
.collect::<Vec<_>>();
return Err(Error::new( return Err(Error::new(
eyre!( eyre!("{}", t!("service.mod.no-matching-subcontainers", id = id)),
"{}", t!("service.mod.no-matching-subcontainers", id = id, subcontainers = subcontainers)
),
ErrorKind::NotFound, ErrorKind::NotFound,
)); )
.with_info(to_value(&subcontainers)?));
}; };
let passwd = root_dir let passwd = root_dir
@@ -814,31 +840,39 @@ pub async fn attach(
) )
.with_kind(ErrorKind::Deserialization)?; .with_kind(ErrorKind::Deserialization)?;
let root_command = get_passwd_command( let user = user
passwd, .clone()
user.as_deref() .or_else(|| image_meta["user"].as_str().map(InternedString::intern))
.or_else(|| image_meta["user"].as_str()) .unwrap_or_else(|| InternedString::intern("root"));
.unwrap_or("root"),
) let root_command = get_passwd_command(passwd, &*user).await;
.await;
let workdir = image_meta["workdir"].as_str().map(|s| s.to_owned()); let workdir = image_meta["workdir"].as_str().map(|s| s.to_owned());
if subcontainer_ids.len() > 1 { if subcontainer_ids.len() > 1 {
let subcontainer_ids = subcontainer_ids let subcontainers = subcontainer_ids
.into_iter() .into_iter()
.map(format_subcontainer_pair) .map(|(g, s)| SubcontainerInfo {
.join("\n"); id: g.clone(),
name: s.name.clone(),
image_id: s.image_id.clone(),
})
.collect::<Vec<_>>();
return Err(Error::new( return Err(Error::new(
eyre!("{}", t!("service.mod.multiple-subcontainers-found", id = id, subcontainer_ids = subcontainer_ids)), eyre!(
"{}",
t!("service.mod.multiple-subcontainers-found", id = id,)
),
ErrorKind::InvalidRequest, ErrorKind::InvalidRequest,
)); )
.with_info(to_value(&subcontainers)?));
} }
( (
service_ref.container_id()?, service_ref.container_id()?,
subcontainer_id, subcontainer_id,
image_id, image_id,
user.into(),
workdir, workdir,
root_command, root_command,
) )
@@ -855,7 +889,7 @@ pub async fn attach(
pty_size: Option<TermSize>, pty_size: Option<TermSize>,
image_id: ImageId, image_id: ImageId,
workdir: Option<String>, workdir: Option<String>,
user: Option<InternedString>, user: InternedString,
root_command: &RootCommand, root_command: &RootCommand,
) -> Result<(), Error> { ) -> Result<(), Error> {
use axum::extract::ws::Message; use axum::extract::ws::Message;
@@ -876,11 +910,9 @@ pub async fn attach(
Path::new("/media/startos/images") Path::new("/media/startos/images")
.join(image_id) .join(image_id)
.with_extension("env"), .with_extension("env"),
); )
.arg("--user")
if let Some(user) = user { .arg(&*user);
cmd.arg("--user").arg(&*user);
}
if let Some(workdir) = workdir { if let Some(workdir) = workdir {
cmd.arg("--workdir").arg(workdir); cmd.arg("--workdir").arg(workdir);
@@ -1063,45 +1095,6 @@ pub async fn attach(
Ok(guid) Ok(guid)
} }
#[derive(Deserialize, Serialize, TS)]
#[serde(rename_all = "camelCase")]
pub struct ListSubcontainersParams {
pub id: PackageId,
}
#[derive(Clone, Debug, Serialize, Deserialize, TS)]
#[serde(rename_all = "camelCase")]
pub struct SubcontainerInfo {
pub name: InternedString,
pub image_id: ImageId,
}
pub async fn list_subcontainers(
ctx: RpcContext,
ListSubcontainersParams { id }: ListSubcontainersParams,
) -> Result<BTreeMap<Guid, SubcontainerInfo>, Error> {
let service = ctx.services.get(&id).await;
let service_ref = service.as_ref().or_not_found(&id)?;
let container = &service_ref.seed.persistent_container;
let subcontainers = container.subcontainers.lock().await;
let result: BTreeMap<Guid, SubcontainerInfo> = subcontainers
.iter()
.map(|(guid, subcontainer)| {
(
guid.clone(),
SubcontainerInfo {
name: subcontainer.name.clone(),
image_id: subcontainer.image_id.clone(),
},
)
})
.collect();
Ok(result)
}
async fn get_passwd_command(etc_passwd_path: PathBuf, user: &str) -> RootCommand { async fn get_passwd_command(etc_passwd_path: PathBuf, user: &str) -> RootCommand {
async { async {
let mut file = tokio::fs::File::open(etc_passwd_path).await?; let mut file = tokio::fs::File::open(etc_passwd_path).await?;
@@ -1120,7 +1113,13 @@ async fn get_passwd_command(etc_passwd_path: PathBuf, user: &str) -> RootCommand
} }
} }
Err(Error::new( Err(Error::new(
eyre!("{}", t!("service.mod.could-not-parse-etc-passwd", contents = contents)), eyre!(
"{}",
t!(
"service.mod.could-not-parse-etc-passwd",
contents = contents
)
),
ErrorKind::Filesystem, ErrorKind::Filesystem,
)) ))
} }
@@ -1176,23 +1175,34 @@ pub async fn cli_attach(
None None
}; };
let method = parent_method.into_iter().chain(method).join(".");
let mut params = json!({
"id": params.id,
"command": params.command,
"tty": tty,
"stderrTty": stderr.is_terminal(),
"ptySize": if tty { TermSize::get_current() } else { None },
"subcontainer": params.subcontainer,
"imageId": params.image_id,
"name": params.name,
"user": params.user,
});
let guid: Guid = from_value( let guid: Guid = from_value(
context match context
.call_remote::<RpcContext>( .call_remote::<RpcContext>(&method, params.clone())
&parent_method.into_iter().chain(method).join("."), .await
json!({ {
"id": params.id, Ok(a) => a,
"command": params.command, Err(e) => {
"tty": tty, let prompt = e.to_string();
"stderrTty": stderr.is_terminal(), let options: Vec<SubcontainerInfo> = from_value(e.info)?;
"ptySize": if tty { TermSize::get_current() } else { None }, let choice = choose(&prompt, &options).await?;
"subcontainer": params.subcontainer, params["subcontainer"] = to_value(&choice.id)?;
"imageId": params.image_id, context
"name": params.name, .call_remote::<RpcContext>(&method, params.clone())
"user": params.user, .await?
}), }
) },
.await?,
)?; )?;
let mut ws = context.ws_continuation(guid).await?; let mut ws = context.ws_continuation(guid).await?;

View File

@@ -364,7 +364,14 @@ impl PersistentContainer {
let handle = NonDetachingJoinHandle::from(tokio::spawn(async move { let handle = NonDetachingJoinHandle::from(tokio::spawn(async move {
let chown_status = async { let chown_status = async {
let res = server.run_unix(&path, |err| { let res = server.run_unix(&path, |err| {
tracing::error!("{}", t!("service.persistent-container.error-on-unix-socket", path = path.display(), error = err)) tracing::error!(
"{}",
t!(
"service.persistent-container.error-on-unix-socket",
path = path.display(),
error = err
)
)
})?; })?;
Command::new("chown") Command::new("chown")
.arg("100000:100000") .arg("100000:100000")
@@ -386,7 +393,10 @@ impl PersistentContainer {
})); }));
let shutdown = recv.await.map_err(|_| { let shutdown = recv.await.map_err(|_| {
Error::new( Error::new(
eyre!("{}", t!("service.persistent-container.unix-socket-server-panicked")), eyre!(
"{}",
t!("service.persistent-container.unix-socket-server-panicked")
),
ErrorKind::Unknown, ErrorKind::Unknown,
) )
})??; })??;
@@ -473,7 +483,13 @@ impl PersistentContainer {
if let Some(destroy) = self.destroy(uninit) { if let Some(destroy) = self.destroy(uninit) {
destroy.await?; destroy.await?;
} }
tracing::info!("{}", t!("service.persistent-container.service-exited", id = self.s9pk.as_manifest().id)); tracing::info!(
"{}",
t!(
"service.persistent-container.service-exited",
id = self.s9pk.as_manifest().id
)
);
Ok(()) Ok(())
} }

View File

@@ -47,9 +47,18 @@ impl Actor for ServiceActor {
} }
.await .await
{ {
tracing::error!("{}", t!("service.service-actor.error-synchronizing-state", error = e)); tracing::error!(
"{}",
t!("service.service-actor.error-synchronizing-state", error = e)
);
tracing::debug!("{e:?}"); tracing::debug!("{e:?}");
tracing::error!("{}", t!("service.service-actor.retrying-in-seconds", seconds = SYNC_RETRY_COOLDOWN_SECONDS)); tracing::error!(
"{}",
t!(
"service.service-actor.retrying-in-seconds",
seconds = SYNC_RETRY_COOLDOWN_SECONDS
)
);
tokio::time::timeout( tokio::time::timeout(
Duration::from_secs(SYNC_RETRY_COOLDOWN_SECONDS), Duration::from_secs(SYNC_RETRY_COOLDOWN_SECONDS),
async { async {

View File

@@ -62,7 +62,13 @@ pub async fn cleanup(ctx: &RpcContext, id: &PackageId, soft: bool) -> Result<(),
| PackageState::Removing(InstalledState { manifest }) => manifest, | PackageState::Removing(InstalledState { manifest }) => manifest,
s => { s => {
return Err(Error::new( return Err(Error::new(
eyre!("{}", t!("service.uninstall.invalid-package-state-for-cleanup", state = format!("{s:?}"))), eyre!(
"{}",
t!(
"service.uninstall.invalid-package-state-for-cleanup",
state = format!("{s:?}")
)
),
ErrorKind::InvalidRequest, ErrorKind::InvalidRequest,
)); ));
} }

View File

@@ -1,4 +1,3 @@
use crate::PLATFORM; use crate::PLATFORM;
use crate::context::RpcContext; use crate::context::RpcContext;
use crate::disk::main::export; use crate::disk::main::export;
@@ -36,18 +35,33 @@ impl Shutdown {
.invoke(crate::ErrorKind::Journald) .invoke(crate::ErrorKind::Journald)
.await .await
{ {
tracing::error!("{}", t!("shutdown.error-stopping-journald", error = e.to_string())); tracing::error!(
"{}",
t!("shutdown.error-stopping-journald", error = e.to_string())
);
tracing::debug!("{:?}", e); tracing::debug!("{:?}", e);
} }
if let Some(guid) = &self.disk_guid { if let Some(guid) = &self.disk_guid {
if let Err(e) = export(guid, crate::DATA_DIR).await { if let Err(e) = export(guid, crate::DATA_DIR).await {
tracing::error!("{}", t!("shutdown.error-exporting-volume-group", error = e.to_string())); tracing::error!(
"{}",
t!(
"shutdown.error-exporting-volume-group",
error = e.to_string()
)
);
tracing::debug!("{:?}", e); tracing::debug!("{:?}", e);
} }
} }
if &*PLATFORM != "raspberrypi" || self.restart { if &*PLATFORM != "raspberrypi" || self.restart {
if let Err(e) = SHUTDOWN.play().await { if let Err(e) = SHUTDOWN.play().await {
tracing::error!("{}", t!("shutdown.error-playing-shutdown-song", error = e.to_string())); tracing::error!(
"{}",
t!(
"shutdown.error-playing-shutdown-song",
error = e.to_string()
)
);
tracing::debug!("{:?}", e); tracing::debug!("{:?}", e);
} }
} }

View File

@@ -19,8 +19,7 @@ pub fn tunnel_api<C: Context>() -> ParentHandler<C> {
.subcommand("web", super::web::web_api::<C>()) .subcommand("web", super::web::web_api::<C>())
.subcommand( .subcommand(
"db", "db",
super::db::db_api::<C>() super::db::db_api::<C>().with_about("about.commands-interact-with-db-dump-apply"),
.with_about("about.commands-interact-with-db-dump-apply"),
) )
.subcommand( .subcommand(
"auth", "auth",

View File

@@ -6,6 +6,7 @@ use clap::{ArgAction, Parser};
use color_eyre::eyre::{Result, eyre}; use color_eyre::eyre::{Result, eyre};
use exver::{Version, VersionRange}; use exver::{Version, VersionRange};
use futures::TryStreamExt; use futures::TryStreamExt;
use imbl::OrdMap;
use imbl_value::json; use imbl_value::json;
use itertools::Itertools; use itertools::Itertools;
use patch_db::json_ptr::JsonPointer; use patch_db::json_ptr::JsonPointer;
@@ -179,7 +180,10 @@ pub async fn cli_update_system(
Some(v) => { Some(v) => {
if let Some(progress) = res.progress { if let Some(progress) = res.progress {
let mut ws = context.ws_continuation(progress).await?; let mut ws = context.ws_continuation(progress).await?;
let mut progress = PhasedProgressBar::new(&t!("update.updating-to-version", version = v.to_string())); let mut progress = PhasedProgressBar::new(&t!(
"update.updating-to-version",
version = v.to_string()
));
let mut prev = None; let mut prev = None;
while let Some(msg) = ws.try_next().await.with_kind(ErrorKind::Network)? { while let Some(msg) = ws.try_next().await.with_kind(ErrorKind::Network)? {
if let tokio_tungstenite::tungstenite::Message::Text(msg) = msg { if let tokio_tungstenite::tungstenite::Message::Text(msg) = msg {
@@ -202,7 +206,10 @@ pub async fn cli_update_system(
} }
println!("{}", t!("update.complete-restart-to-apply")) println!("{}", t!("update.complete-restart-to-apply"))
} else { } else {
println!("{}", t!("update.updating-to-version", version = v.to_string())) println!(
"{}",
t!("update.updating-to-version", version = v.to_string())
)
} }
} }
} }
@@ -239,6 +246,7 @@ async fn maybe_do_update(
let mut available = from_value::<BTreeMap<Version, OsVersionInfo>>( let mut available = from_value::<BTreeMap<Version, OsVersionInfo>>(
ctx.call_remote_with::<RegistryContext, _>( ctx.call_remote_with::<RegistryContext, _>(
"os.version.get", "os.version.get",
OrdMap::new(),
json!({ json!({
"source": current_version, "source": current_version,
"target": target, "target": target,

View File

@@ -248,7 +248,7 @@ impl<'a> Invoke<'a> for ExtendedCommand<'a> {
.or(Some(&res.stdout)) .or(Some(&res.stdout))
.filter(|a| !a.is_empty()) .filter(|a| !a.is_empty())
.and_then(|a| std::str::from_utf8(a).ok()) .and_then(|a| std::str::from_utf8(a).ok())
.unwrap_or(&format!("{} exited with code {}", cmd_str, res.status)) .unwrap_or(&format!("{} exited with {}", cmd_str, res.status))
); );
Ok(res.stdout) Ok(res.stdout)
} else { } else {
@@ -309,7 +309,7 @@ impl<'a> Invoke<'a> for ExtendedCommand<'a> {
.filter(|a| !a.is_empty()) .filter(|a| !a.is_empty())
.and_then(|a| std::str::from_utf8(a).ok()) .and_then(|a| std::str::from_utf8(a).ok())
.unwrap_or(&format!( .unwrap_or(&format!(
"{} exited with code {}", "{} exited with {}",
cmd.as_std().get_program().to_string_lossy(), cmd.as_std().get_program().to_string_lossy(),
res.status res.status
)) ))

View File

@@ -97,7 +97,11 @@ impl WebSocket {
if self.ping_state.is_some() { if self.ping_state.is_some() {
self.fused = true; self.fused = true;
break Poll::Ready(Some(Err(axum::Error::new(eyre!( break Poll::Ready(Some(Err(axum::Error::new(eyre!(
"{}", t!("util.net.websocket-ping-timeout", timeout = format!("{PING_TIMEOUT:?}")) "{}",
t!(
"util.net.websocket-ping-timeout",
timeout = format!("{PING_TIMEOUT:?}")
)
))))); )))));
} }
self.ping_state = Some((false, rand::random())); self.ping_state = Some((false, rand::random()));

View File

@@ -1151,7 +1151,13 @@ pub fn apply_expr(input: jaq_core::Val, expr: &str) -> Result<jaq_core::Val, Err
let Some(expr) = expr else { let Some(expr) = expr else {
return Err(Error::new( return Err(Error::new(
eyre!("{}", t!("util.serde.failed-to-parse-expression", errors = format!("{:?}", errs))), eyre!(
"{}",
t!(
"util.serde.failed-to-parse-expression",
errors = format!("{:?}", errs)
)
),
crate::ErrorKind::InvalidRequest, crate::ErrorKind::InvalidRequest,
)); ));
}; };
@@ -1167,7 +1173,13 @@ pub fn apply_expr(input: jaq_core::Val, expr: &str) -> Result<jaq_core::Val, Err
if !errs.is_empty() { if !errs.is_empty() {
return Err(Error::new( return Err(Error::new(
eyre!("{}", t!("util.serde.failed-to-compile-expression", errors = format!("{:?}", errs))), eyre!(
"{}",
t!(
"util.serde.failed-to-compile-expression",
errors = format!("{:?}", errs)
)
),
crate::ErrorKind::InvalidRequest, crate::ErrorKind::InvalidRequest,
)); ));
}; };

View File

@@ -50,7 +50,10 @@ pub async fn prompt<T, E: std::fmt::Display, Parse: FnMut(&str) -> Result<T, E>>
} }
} }
ReadlineEvent::Eof | ReadlineEvent::Interrupted => { ReadlineEvent::Eof | ReadlineEvent::Interrupted => {
return Err(Error::new(eyre!("{}", t!("util.tui.aborted")), ErrorKind::Cancelled)); return Err(Error::new(
eyre!("{}", t!("util.tui.aborted")),
ErrorKind::Cancelled,
));
} }
_ => (), _ => (),
} }
@@ -83,7 +86,10 @@ pub async fn prompt_multiline<
Err(e) => writeln!(&mut rl_ctx.shared_writer, "{e}")?, Err(e) => writeln!(&mut rl_ctx.shared_writer, "{e}")?,
}, },
ReadlineEvent::Eof | ReadlineEvent::Interrupted => { ReadlineEvent::Eof | ReadlineEvent::Interrupted => {
return Err(Error::new(eyre!("{}", t!("util.tui.aborted")), ErrorKind::Cancelled)); return Err(Error::new(
eyre!("{}", t!("util.tui.aborted")),
ErrorKind::Cancelled,
));
} }
_ => (), _ => (),
} }
@@ -119,7 +125,10 @@ pub async fn choose_custom_display<'t, T>(
.await .await
.map_err(map_miette)?; .map_err(map_miette)?;
if choice.len() < 1 { if choice.len() < 1 {
return Err(Error::new(eyre!("{}", t!("util.tui.aborted")), ErrorKind::Cancelled)); return Err(Error::new(
eyre!("{}", t!("util.tui.aborted")),
ErrorKind::Cancelled,
));
} }
let (idx, choice_str) = string_choices let (idx, choice_str) = string_choices
.iter() .iter()

View File

@@ -58,8 +58,9 @@ mod v0_4_0_alpha_15;
mod v0_4_0_alpha_16; mod v0_4_0_alpha_16;
mod v0_4_0_alpha_17; mod v0_4_0_alpha_17;
mod v0_4_0_alpha_18; mod v0_4_0_alpha_18;
mod v0_4_0_alpha_19;
pub type Current = v0_4_0_alpha_18::Version; // VERSION_BUMP pub type Current = v0_4_0_alpha_19::Version; // VERSION_BUMP
impl Current { impl Current {
#[instrument(skip(self, db))] #[instrument(skip(self, db))]
@@ -179,7 +180,8 @@ enum Version {
V0_4_0_alpha_15(Wrapper<v0_4_0_alpha_15::Version>), V0_4_0_alpha_15(Wrapper<v0_4_0_alpha_15::Version>),
V0_4_0_alpha_16(Wrapper<v0_4_0_alpha_16::Version>), V0_4_0_alpha_16(Wrapper<v0_4_0_alpha_16::Version>),
V0_4_0_alpha_17(Wrapper<v0_4_0_alpha_17::Version>), V0_4_0_alpha_17(Wrapper<v0_4_0_alpha_17::Version>),
V0_4_0_alpha_18(Wrapper<v0_4_0_alpha_18::Version>), // VERSION_BUMP V0_4_0_alpha_18(Wrapper<v0_4_0_alpha_18::Version>),
V0_4_0_alpha_19(Wrapper<v0_4_0_alpha_19::Version>), // VERSION_BUMP
Other(exver::Version), Other(exver::Version),
} }
@@ -240,7 +242,8 @@ impl Version {
Self::V0_4_0_alpha_15(v) => DynVersion(Box::new(v.0)), Self::V0_4_0_alpha_15(v) => DynVersion(Box::new(v.0)),
Self::V0_4_0_alpha_16(v) => DynVersion(Box::new(v.0)), Self::V0_4_0_alpha_16(v) => DynVersion(Box::new(v.0)),
Self::V0_4_0_alpha_17(v) => DynVersion(Box::new(v.0)), Self::V0_4_0_alpha_17(v) => DynVersion(Box::new(v.0)),
Self::V0_4_0_alpha_18(v) => DynVersion(Box::new(v.0)), // VERSION_BUMP Self::V0_4_0_alpha_18(v) => DynVersion(Box::new(v.0)),
Self::V0_4_0_alpha_19(v) => DynVersion(Box::new(v.0)), // VERSION_BUMP
Self::Other(v) => { Self::Other(v) => {
return Err(Error::new( return Err(Error::new(
eyre!("unknown version {v}"), eyre!("unknown version {v}"),
@@ -293,7 +296,8 @@ impl Version {
Version::V0_4_0_alpha_15(Wrapper(x)) => x.semver(), Version::V0_4_0_alpha_15(Wrapper(x)) => x.semver(),
Version::V0_4_0_alpha_16(Wrapper(x)) => x.semver(), Version::V0_4_0_alpha_16(Wrapper(x)) => x.semver(),
Version::V0_4_0_alpha_17(Wrapper(x)) => x.semver(), Version::V0_4_0_alpha_17(Wrapper(x)) => x.semver(),
Version::V0_4_0_alpha_18(Wrapper(x)) => x.semver(), // VERSION_BUMP Version::V0_4_0_alpha_18(Wrapper(x)) => x.semver(),
Version::V0_4_0_alpha_19(Wrapper(x)) => x.semver(), // VERSION_BUMP
Version::Other(x) => x.clone(), Version::Other(x) => x.clone(),
} }
} }

View File

@@ -0,0 +1,37 @@
use exver::{PreReleaseSegment, VersionRange};
use super::v0_3_5::V0_3_0_COMPAT;
use super::{VersionT, v0_4_0_alpha_18};
use crate::prelude::*;
lazy_static::lazy_static! {
static ref V0_4_0_alpha_19: exver::Version = exver::Version::new(
[0, 4, 0],
[PreReleaseSegment::String("alpha".into()), 19.into()]
);
}
#[derive(Clone, Copy, Debug, Default)]
pub struct Version;
impl VersionT for Version {
type Previous = v0_4_0_alpha_18::Version;
type PreUpRes = ();
async fn pre_up(self) -> Result<Self::PreUpRes, Error> {
Ok(())
}
fn semver(self) -> exver::Version {
V0_4_0_alpha_19.clone()
}
fn compat(self) -> &'static VersionRange {
&V0_3_0_COMPAT
}
#[instrument(skip_all)]
fn up(self, _db: &mut Value, _: Self::PreUpRes) -> Result<Value, Error> {
Ok(Value::Null)
}
fn down(self, _db: &mut Value) -> Result<(), Error> {
Ok(())
}
}

View File

@@ -132,7 +132,6 @@ export type SDKManifest = {
* `pattern` refers to a regular expression that at least one device of the specified class must match * `pattern` refers to a regular expression that at least one device of the specified class must match
* `patternDescription` is what will be displayed to the user about what kind of device is required * `patternDescription` is what will be displayed to the user about what kind of device is required
* @property {number} ram - Minimum RAM requirement (in megabytes MB) * @property {number} ram - Minimum RAM requirement (in megabytes MB)
* @property {string[]} arch - List of supported arches
* @example * @example
* ``` * ```
hardwareRequirements: { hardwareRequirements: {
@@ -141,14 +140,12 @@ export type SDKManifest = {
{ class: 'processor', pattern: 'i[3579]-10[0-9]{3}U CPU', patternDescription: 'A 10th Generation Intel i-Series processor' }, { class: 'processor', pattern: 'i[3579]-10[0-9]{3}U CPU', patternDescription: 'A 10th Generation Intel i-Series processor' },
], ],
ram: 8192, ram: 8192,
arch: ['x86-64'],
}, },
* ``` * ```
*/ */
readonly hardwareRequirements?: { readonly hardwareRequirements?: {
readonly device?: T.DeviceFilter[] readonly device?: T.DeviceFilter[]
readonly ram?: number | null readonly ram?: number | null
readonly arch?: string[] | null
} }
/** /**

View File

@@ -67,7 +67,7 @@ import {
import { getOwnServiceInterfaces } from "../../base/lib/util/getServiceInterfaces" import { getOwnServiceInterfaces } from "../../base/lib/util/getServiceInterfaces"
import { Volumes, createVolumes } from "./util/Volume" import { Volumes, createVolumes } from "./util/Volume"
export const OSVersion = testTypeVersion("0.4.0-alpha.18") export const OSVersion = testTypeVersion("0.4.0-alpha.19")
// prettier-ignore // prettier-ignore
type AnyNeverCond<T extends any[], Then, Else> = type AnyNeverCond<T extends any[], Then, Else> =

View File

@@ -42,11 +42,11 @@ export function buildManifest<
): Manifest & T.Manifest { ): Manifest & T.Manifest {
const images = Object.entries(manifest.images).reduce( const images = Object.entries(manifest.images).reduce(
(images, [k, v]) => { (images, [k, v]) => {
v.arch = v.arch || ["aarch64", "x86_64"] v.arch = v.arch ?? ["aarch64", "x86_64", "riscv64"]
if (v.emulateMissingAs === undefined) if (v.emulateMissingAs === undefined)
v.emulateMissingAs = (v.arch as string[]).includes("aarch64") v.emulateMissingAs = (v.arch as string[]).includes("x86_64")
? "aarch64" ? "x86_64"
: v.arch[0] || null : (v.arch[0] ?? null)
v.nvidiaContainer = !!v.nvidiaContainer v.nvidiaContainer = !!v.nvidiaContainer
images[k] = v as ImageConfig images[k] = v as ImageConfig
return images return images
@@ -75,21 +75,18 @@ export function buildManifest<
hardwareRequirements: { hardwareRequirements: {
device: manifest.hardwareRequirements?.device || [], device: manifest.hardwareRequirements?.device || [],
ram: manifest.hardwareRequirements?.ram || null, ram: manifest.hardwareRequirements?.ram || null,
arch: arch: Object.values(images).reduce(
manifest.hardwareRequirements?.arch === undefined (arch, inputSpec) => {
? Object.values(images).reduce( if (inputSpec.emulateMissingAs) {
(arch, inputSpec) => { return arch
if (inputSpec.emulateMissingAs) { }
return arch if (arch === null) {
} return inputSpec.arch
if (arch === null) { }
return inputSpec.arch return arch.filter((a) => inputSpec.arch.includes(a))
} },
return arch.filter((a) => inputSpec.arch.includes(a)) null as string[] | null,
}, ),
null as string[] | null,
)
: manifest.hardwareRequirements?.arch,
}, },
hardwareAcceleration: manifest.hardwareAcceleration ?? false, hardwareAcceleration: manifest.hardwareAcceleration ?? false,
} }

View File

@@ -6,4 +6,5 @@ module.exports = {
'projects/marketplace/**/*.ts': () => 'npm run check:marketplace', 'projects/marketplace/**/*.ts': () => 'npm run check:marketplace',
'projects/setup-wizard/**/*.ts': () => 'npm run check:setup', 'projects/setup-wizard/**/*.ts': () => 'npm run check:setup',
'projects/start-tunnel/**/*.ts': () => 'npm run check:tunnel', 'projects/start-tunnel/**/*.ts': () => 'npm run check:tunnel',
'projects/**/*.{ts,html}': () => 'npm run check:i18n',
} }

4
web/package-lock.json generated
View File

@@ -1,12 +1,12 @@
{ {
"name": "startos-ui", "name": "startos-ui",
"version": "0.4.0-alpha.18", "version": "0.4.0-alpha.19",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "startos-ui", "name": "startos-ui",
"version": "0.4.0-alpha.18", "version": "0.4.0-alpha.19",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@angular/animations": "^20.3.0", "@angular/animations": "^20.3.0",

View File

@@ -1,17 +1,18 @@
{ {
"name": "startos-ui", "name": "startos-ui",
"version": "0.4.0-alpha.18", "version": "0.4.0-alpha.19",
"author": "Start9 Labs, Inc", "author": "Start9 Labs, Inc",
"homepage": "https://start9.com/", "homepage": "https://start9.com/",
"license": "MIT", "license": "MIT",
"scripts": { "scripts": {
"ng": "ng", "ng": "ng",
"check": "npm run check:shared && npm run check:marketplace && npm run check:ui && npm run check:setup", "check": "npm run check:i18n && npm run check:shared && npm run check:marketplace && npm run check:ui && npm run check:setup",
"check:shared": "tsc --project projects/shared/tsconfig.json --noEmit --skipLibCheck", "check:shared": "tsc --project projects/shared/tsconfig.json --noEmit --skipLibCheck",
"check:marketplace": "tsc --project projects/marketplace/tsconfig.json --noEmit --skipLibCheck", "check:marketplace": "tsc --project projects/marketplace/tsconfig.json --noEmit --skipLibCheck",
"check:setup": "tsc --project projects/setup-wizard/tsconfig.json --noEmit --skipLibCheck", "check:setup": "tsc --project projects/setup-wizard/tsconfig.json --noEmit --skipLibCheck",
"check:ui": "tsc --project projects/ui/tsconfig.json --noEmit --skipLibCheck", "check:ui": "tsc --project projects/ui/tsconfig.json --noEmit --skipLibCheck",
"check:tunnel": "tsc --project projects/start-tunnel/tsconfig.json --noEmit --skipLibCheck", "check:tunnel": "tsc --project projects/start-tunnel/tsconfig.json --noEmit --skipLibCheck",
"check:i18n": "node scripts/check-i18n.mjs",
"build:deps": "rimraf .angular/cache && (cd ../sdk && make bundle) && (cd ../patch-db/client && npm ci && npm run build)", "build:deps": "rimraf .angular/cache && (cd ../sdk && make bundle) && (cd ../patch-db/client && npm ci && npm run build)",
"build:setup": "ng run setup-wizard:build", "build:setup": "ng run setup-wizard:build",
"build:ui": "ng run ui:build", "build:ui": "ng run ui:build",

View File

@@ -215,9 +215,7 @@ export default class SuccessPage implements AfterViewInit {
.getElementById('cert') .getElementById('cert')
?.setAttribute( ?.setAttribute(
'href', 'href',
URL.createObjectURL( `data:application/octet-stream;base64,${this.result!.rootCa}`,
new Blob([this.result!.rootCa], { type: 'application/octet-stream' }),
),
) )
const html = this.documentation?.nativeElement.innerHTML || '' const html = this.documentation?.nativeElement.innerHTML || ''

View File

@@ -1,5 +1,5 @@
// prettier-ignore // prettier-ignore
export const ENGLISH = { export const ENGLISH: Record<string, number> = {
'Change': 1, // verb 'Change': 1, // verb
'Update': 2, // verb 'Update': 2, // verb
'System': 4, // as in, system preferences 'System': 4, // as in, system preferences
@@ -680,4 +680,4 @@ export const ENGLISH = {
'Installation Complete!': 714, 'Installation Complete!': 714,
'StartOS has been installed successfully.': 715, 'StartOS has been installed successfully.': 715,
'Continue to Setup': 716, 'Continue to Setup': 716,
} as const }

View File

@@ -13,6 +13,8 @@ export class i18nPipe implements PipeTransform {
transform(englishKey: i18nKey | null | undefined | ''): string { transform(englishKey: i18nKey | null | undefined | ''): string {
englishKey = englishKey || ('' as i18nKey) englishKey = englishKey || ('' as i18nKey)
return this.i18n()?.[ENGLISH[englishKey]] || englishKey const id = ENGLISH[englishKey]
return (id !== undefined && this.i18n()?.[id]) || englishKey
} }
} }

View File

@@ -75,10 +75,6 @@ import { InterfaceAddressItemComponent } from './item.component'
th:nth-child(3) { th:nth-child(3) {
width: 4rem; width: 4rem;
} }
th:nth-child(4) {
width: 17rem;
}
} }
.g-table:has(caption) { .g-table:has(caption) {

View File

@@ -29,6 +29,21 @@ const INACTIVE: PrimaryStatus[] = [
'backing-up', 'backing-up',
] ]
const ALLOWED_STATUSES: Record<T.AllowedStatuses, Set<string>> = {
'only-running': new Set(['running']),
'only-stopped': new Set(['stopped']),
any: new Set([
'running',
'stopped',
'restarting',
'restoring',
'stopping',
'starting',
'backing-up',
'task-required',
]),
}
@Component({ @Component({
template: ` template: `
@if (package(); as pkg) { @if (package(); as pkg) {
@@ -92,8 +107,9 @@ export default class ServiceActionsRoute {
const specialGroup = Object.values(pkg.actions).some(a => !!a.group) const specialGroup = Object.values(pkg.actions).some(a => !!a.group)
? 'Other' ? 'Other'
: 'General' : 'General'
const status = renderPkgStatus(pkg).primary
return { return {
status: renderPkgStatus(pkg).primary, status,
icon: pkg.icon, icon: pkg.icon,
manifest: getManifest(pkg), manifest: getManifest(pkg),
actions: Object.entries(pkg.actions) actions: Object.entries(pkg.actions)
@@ -102,6 +118,13 @@ export default class ServiceActionsRoute {
...action, ...action,
id, id,
group: action.group || specialGroup, group: action.group || specialGroup,
visibility: ALLOWED_STATUSES[action.allowedStatuses].has(
status,
)
? action.visibility
: ({
disabled: `${this.i18n.transform('Action can only be executed when service is')} ${this.i18n.transform(action.allowedStatuses === 'only-running' ? 'Running' : 'Stopped')?.toLowerCase()}`,
} as T.ActionVisibility),
})) }))
.sort((a, b) => { .sort((a, b) => {
if (a.group === specialGroup && b.group !== specialGroup) if (a.group === specialGroup && b.group !== specialGroup)

View File

@@ -3,7 +3,6 @@ import {
DialogService, DialogService,
ErrorService, ErrorService,
i18nKey, i18nKey,
i18nPipe,
LoadingService, LoadingService,
} from '@start9labs/shared' } from '@start9labs/shared'
import { PolymorpheusComponent } from '@taiga-ui/polymorpheus' import { PolymorpheusComponent } from '@taiga-ui/polymorpheus'
@@ -16,21 +15,6 @@ import { ActionSuccessPage } from 'src/app/routes/portal/routes/services/modals/
import { ApiService } from 'src/app/services/api/embassy-api.service' import { ApiService } from 'src/app/services/api/embassy-api.service'
import { FormDialogService } from 'src/app/services/form-dialog.service' import { FormDialogService } from 'src/app/services/form-dialog.service'
const allowedStatuses = {
'only-running': new Set(['running']),
'only-stopped': new Set(['stopped']),
any: new Set([
'running',
'stopped',
'restarting',
'restoring',
'stopping',
'starting',
'backing-up',
'task-required',
]),
}
@Injectable({ @Injectable({
providedIn: 'root', providedIn: 'root',
}) })
@@ -40,58 +24,32 @@ export class ActionService {
private readonly errorService = inject(ErrorService) private readonly errorService = inject(ErrorService)
private readonly loader = inject(LoadingService) private readonly loader = inject(LoadingService)
private readonly formDialog = inject(FormDialogService) private readonly formDialog = inject(FormDialogService)
private readonly i18n = inject(i18nPipe)
async present(data: PackageActionData) { async present(data: PackageActionData) {
const { pkgInfo, actionInfo } = data const { pkgInfo, actionInfo } = data
if ( if (actionInfo.metadata.hasInput) {
allowedStatuses[actionInfo.metadata.allowedStatuses].has(pkgInfo.status) this.formDialog.open<PackageActionData>(ActionInputModal, {
) { label: actionInfo.metadata.name as i18nKey,
if (actionInfo.metadata.hasInput) { data,
this.formDialog.open<PackageActionData>(ActionInputModal, { })
label: actionInfo.metadata.name as i18nKey,
data,
})
} else {
if (actionInfo.metadata.warning) {
this.dialog
.openConfirm({
label: 'Warning',
size: 's',
data: {
no: 'Cancel',
yes: 'Run',
content: actionInfo.metadata.warning as i18nKey,
},
})
.pipe(filter(Boolean))
.subscribe(() => this.execute(pkgInfo.id, null, actionInfo.id))
} else {
this.execute(pkgInfo.id, null, actionInfo.id)
}
}
} else { } else {
const statuses = [...allowedStatuses[actionInfo.metadata.allowedStatuses]] if (actionInfo.metadata.warning) {
const last = statuses.pop() this.dialog
let statusesStr = statuses.join(', ') .openConfirm({
if (statuses.length) { label: 'Warning',
if (statuses.length > 1) { size: 's',
// oxford comma data: {
statusesStr += ',' no: 'Cancel',
} yes: 'Run',
statusesStr += ` or ${last}` content: actionInfo.metadata.warning as i18nKey,
} else if (last) { },
statusesStr = last })
.pipe(filter(Boolean))
.subscribe(() => this.execute(pkgInfo.id, null, actionInfo.id))
} else {
this.execute(pkgInfo.id, null, actionInfo.id)
} }
this.dialog
.openAlert(
`${this.i18n.transform('Action can only be executed when service is')} ${statusesStr}` as i18nKey,
{ label: 'Forbidden' },
)
.pipe(filter(Boolean))
.subscribe()
} }
} }

View File

@@ -0,0 +1,95 @@
import { readFileSync, readdirSync, statSync } from 'fs'
import { join, relative } from 'path'
import { fileURLToPath } from 'url'
import { dirname } from 'path'
const __filename = fileURLToPath(import.meta.url)
const __dirname = dirname(__filename)
const root = join(__dirname, '..')
// Extract dictionary keys from en.ts
const enPath = join(
root,
'projects/shared/src/i18n/dictionaries/en.ts',
)
const enSource = readFileSync(enPath, 'utf-8')
const validKeys = new Set()
for (const match of enSource.matchAll(/^\s+'(.+?)':\s*\d+/gm)) {
validKeys.add(match[1])
}
if (validKeys.size === 0) {
console.error('ERROR: Could not parse any keys from en.ts')
process.exit(1)
}
console.log(`Loaded ${validKeys.size} i18n keys from en.ts`)
// Collect all .ts and .html files under projects/
function walk(dir, files = []) {
for (const entry of readdirSync(dir)) {
const full = join(dir, entry)
if (entry === 'node_modules' || entry === 'dist') continue
const stat = statSync(full)
if (stat.isDirectory()) {
walk(full, files)
} else if (full.endsWith('.ts') || full.endsWith('.html')) {
files.push(full)
}
}
return files
}
const projectsDir = join(root, 'projects')
const files = walk(projectsDir)
const errors = []
for (const file of files) {
// Skip the dictionary files themselves
if (file.includes('/i18n/dictionaries/')) continue
const source = readFileSync(file, 'utf-8')
const lines = source.split('\n')
for (let i = 0; i < lines.length; i++) {
const line = lines[i]
// Pattern 1: i18n.transform('Key') or i18n.transform("Key")
for (const m of line.matchAll(/i18n\.transform\(\s*'([^']+)'\s*\)/g)) {
if (!validKeys.has(m[1])) {
errors.push({ file, line: i + 1, key: m[1] })
}
}
for (const m of line.matchAll(/i18n\.transform\(\s*"([^"]+)"\s*\)/g)) {
if (!validKeys.has(m[1])) {
errors.push({ file, line: i + 1, key: m[1] })
}
}
// Pattern 2: 'Key' | i18n or "Key" | i18n (Angular templates)
for (const m of line.matchAll(/'([^']+)'\s*\|\s*i18n/g)) {
if (!validKeys.has(m[1])) {
errors.push({ file, line: i + 1, key: m[1] })
}
}
for (const m of line.matchAll(/"([^"]+)"\s*\|\s*i18n/g)) {
if (!validKeys.has(m[1])) {
errors.push({ file, line: i + 1, key: m[1] })
}
}
}
}
if (errors.length > 0) {
console.error(`\nFound ${errors.length} invalid i18n key(s):\n`)
for (const { file, line, key } of errors) {
const rel = relative(root, file)
console.error(` ${rel}:${line} "${key}"`)
}
console.error()
process.exit(1)
} else {
console.log('All i18n keys are valid.')
}