diff --git a/.github/workflows/startos-iso.yaml b/.github/workflows/startos-iso.yaml index bc0fb626e..3497624e7 100644 --- a/.github/workflows/startos-iso.yaml +++ b/.github/workflows/startos-iso.yaml @@ -78,7 +78,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v5 with: - python-version: '3.x' + python-version: "3.x" - uses: actions/setup-node@v4 with: @@ -156,7 +156,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v5 with: - python-version: '3.x' + python-version: "3.x" - name: Install dependencies run: | @@ -187,11 +187,24 @@ jobs: run: | mkdir -p web/node_modules mkdir -p web/dist/raw - touch core/startos/bindings - touch sdk/lib/osBindings + mkdir -p container-runtime/node_modules mkdir -p container-runtime/dist + mkdir -p container-runtime/dist/node_modules + mkdir -p core/startos/bindings + mkdir -p sdk/dist + mkdir -p patch-db/client/node_modules + mkdir -p patch-db/client/dist + mkdir -p web/.angular + mkdir -p web/dist/raw/ui + mkdir -p web/dist/raw/install-wizard + mkdir -p web/dist/raw/setup-wizard + mkdir -p web/dist/static/ui + mkdir -p web/dist/static/install-wizard + mkdir -p web/dist/static/setup-wizard PLATFORM=${{ matrix.platform }} make -t compiled-${{ env.ARCH }}.tar + - run: git status + - name: Run iso build run: PLATFORM=${{ matrix.platform }} make iso if: ${{ matrix.platform != 'raspberrypi' }} diff --git a/Makefile b/Makefile index a714cd389..d085117b2 100644 --- a/Makefile +++ b/Makefile @@ -6,7 +6,8 @@ BASENAME := $(shell ./basename.sh) PLATFORM := $(shell if [ -f ./PLATFORM.txt ]; then cat ./PLATFORM.txt; else echo unknown; fi) ARCH := $(shell if [ "$(PLATFORM)" = "raspberrypi" ]; then echo aarch64; else echo $(PLATFORM) | sed 's/-nonfree$$//g'; fi) IMAGE_TYPE=$(shell if [ "$(PLATFORM)" = raspberrypi ]; then echo img; else echo iso; fi) -WEB_UIS := web/dist/raw/ui web/dist/raw/setup-wizard web/dist/raw/install-wizard +WEB_UIS := web/dist/raw/ui/index.html web/dist/raw/setup-wizard/index.html web/dist/raw/install-wizard/index.html +COMPRESSED_WEB_UIS := web/dist/static/ui/index.html web/dist/static/setup-wizard/index.html web/dist/static/install-wizard/index.html FIRMWARE_ROMS := ./firmware/$(PLATFORM) $(shell jq --raw-output '.[] | select(.platform[] | contains("$(PLATFORM)")) | "./firmware/$(PLATFORM)/" + .id + ".rom.gz"' build/lib/firmware.json) BUILD_SRC := $(shell git ls-files build) build/lib/depends build/lib/conflicts $(FIRMWARE_ROMS) DEBIAN_SRC := $(shell git ls-files debian/) @@ -16,7 +17,7 @@ COMPAT_SRC := $(shell git ls-files system-images/compat/) UTILS_SRC := $(shell git ls-files system-images/utils/) BINFMT_SRC := $(shell git ls-files system-images/binfmt/) CORE_SRC := $(shell git ls-files core) $(shell git ls-files --recurse-submodules patch-db) $(GIT_HASH_FILE) -WEB_SHARED_SRC := $(shell git ls-files web/projects/shared) $(shell ls -p web/ | grep -v / | sed 's/^/web\//g') web/node_modules/.package-lock.json web/config.json patch-db/client/dist web/patchdb-ui-seed.json sdk/dist +WEB_SHARED_SRC := $(shell git ls-files web/projects/shared) $(shell ls -p web/ | grep -v / | sed 's/^/web\//g') web/node_modules/.package-lock.json web/config.json patch-db/client/dist/index.js web/patchdb-ui-seed.json sdk/dist/package.json WEB_UI_SRC := $(shell git ls-files web/projects/ui) WEB_SETUP_WIZARD_SRC := $(shell git ls-files web/projects/setup-wizard) WEB_INSTALL_WIZARD_SRC := $(shell git ls-files web/projects/install-wizard) @@ -57,7 +58,7 @@ touch: metadata: $(VERSION_FILE) $(PLATFORM_FILE) $(ENVIRONMENT_FILE) $(GIT_HASH_FILE) sudo: - sudo true + sudo -v clean: rm -f system-images/**/*.tar @@ -94,10 +95,10 @@ test: | test-core test-sdk test-container-runtime test-core: $(CORE_SRC) $(ENVIRONMENT_FILE) ./core/run-tests.sh -test-sdk: $(shell git ls-files sdk) sdk/lib/osBindings +test-sdk: $(shell git ls-files sdk) sdk/lib/osBindings/index.ts cd sdk && make test -test-container-runtime: container-runtime/node_modules $(shell git ls-files container-runtime/src) container-runtime/package.json container-runtime/tsconfig.json +test-container-runtime: container-runtime/node_modules/.package-lock.json $(shell git ls-files container-runtime/src) container-runtime/package.json container-runtime/tsconfig.json cd container-runtime && npm test cli: @@ -218,34 +219,34 @@ upload-ota: results/$(BASENAME).squashfs container-runtime/debian.$(ARCH).squashfs: ARCH=$(ARCH) ./container-runtime/download-base-image.sh -container-runtime/node_modules: container-runtime/package.json container-runtime/package-lock.json sdk/dist +container-runtime/node_modules/.package-lock.json: container-runtime/package.json container-runtime/package-lock.json sdk/dist/package.json npm --prefix container-runtime ci - touch container-runtime/node_modules + touch container-runtime/node_modules/.package-lock.json -sdk/lib/osBindings: core/startos/bindings - mkdir -p sdk/lib/osBindings - ls core/startos/bindings/*.ts | sed 's/core\/startos\/bindings\/\([^.]*\)\.ts/export { \1 } from ".\/\1";/g' > core/startos/bindings/index.ts - npm --prefix sdk exec -- prettier --config ./sdk/package.json -w ./core/startos/bindings/*.ts +sdk/lib/osBindings/index.ts: core/startos/bindings/index.ts rsync -ac --delete core/startos/bindings/ sdk/lib/osBindings/ - touch sdk/lib/osBindings + touch sdk/lib/osBindings/index.ts -core/startos/bindings: $(shell git ls-files core) $(ENVIRONMENT_FILE) +core/startos/bindings/index.ts: $(shell git ls-files core) $(ENVIRONMENT_FILE) rm -rf core/startos/bindings ./core/build-ts.sh - touch core/startos/bindings + ls core/startos/bindings/*.ts | sed 's/core\/startos\/bindings\/\([^.]*\)\.ts/export { \1 } from ".\/\1";/g' > core/startos/bindings/index.ts + npm --prefix sdk exec -- prettier --config ./sdk/package.json -w ./core/startos/bindings/*.ts + touch core/startos/bindings/index.ts -sdk/dist: $(shell git ls-files sdk) sdk/lib/osBindings +sdk/dist/package.json: $(shell git ls-files sdk) sdk/lib/osBindings/index.ts (cd sdk && make bundle) + touch sdk/dist/package.json # TODO: make container-runtime its own makefile? -container-runtime/dist/index.js: container-runtime/node_modules $(shell git ls-files container-runtime/src) container-runtime/package.json container-runtime/tsconfig.json +container-runtime/dist/index.js: container-runtime/node_modules/.package-lock.json $(shell git ls-files container-runtime/src) container-runtime/package.json container-runtime/tsconfig.json npm --prefix container-runtime run build -container-runtime/dist/node_modules container-runtime/dist/package.json container-runtime/dist/package-lock.json: container-runtime/package.json container-runtime/package-lock.json sdk/dist container-runtime/install-dist-deps.sh +container-runtime/dist/node_modules/.package-lock.json container-runtime/dist/package.json container-runtime/dist/package-lock.json: container-runtime/package.json container-runtime/package-lock.json sdk/dist/package.json container-runtime/install-dist-deps.sh ./container-runtime/install-dist-deps.sh - touch container-runtime/dist/node_modules + touch container-runtime/dist/node_modules/.package-lock.json -container-runtime/rootfs.$(ARCH).squashfs: container-runtime/debian.$(ARCH).squashfs container-runtime/container-runtime.service container-runtime/update-image.sh container-runtime/deb-install.sh container-runtime/dist/index.js container-runtime/dist/node_modules core/target/$(ARCH)-unknown-linux-musl/release/containerbox | sudo +container-runtime/rootfs.$(ARCH).squashfs: container-runtime/debian.$(ARCH).squashfs container-runtime/container-runtime.service container-runtime/update-image.sh container-runtime/deb-install.sh container-runtime/dist/index.js container-runtime/dist/node_modules/.package-lock.json core/target/$(ARCH)-unknown-linux-musl/release/containerbox | sudo ARCH=$(ARCH) ./container-runtime/update-image.sh build/lib/depends build/lib/conflicts: build/dpkg-deps/* @@ -263,7 +264,7 @@ system-images/utils/docker-images/$(ARCH).tar: $(UTILS_SRC) system-images/binfmt/docker-images/$(ARCH).tar: $(BINFMT_SRC) cd system-images/binfmt && make docker-images/$(ARCH).tar && touch docker-images/$(ARCH).tar -core/target/$(ARCH)-unknown-linux-musl/release/startbox: $(CORE_SRC) web/dist/static web/patchdb-ui-seed.json $(ENVIRONMENT_FILE) +core/target/$(ARCH)-unknown-linux-musl/release/startbox: $(CORE_SRC) $(COMPRESSED_WEB_UIS) web/patchdb-ui-seed.json $(ENVIRONMENT_FILE) ARCH=$(ARCH) ./core/build-startbox.sh touch core/target/$(ARCH)-unknown-linux-musl/release/startbox @@ -271,27 +272,28 @@ core/target/$(ARCH)-unknown-linux-musl/release/containerbox: $(CORE_SRC) $(ENVIR ARCH=$(ARCH) ./core/build-containerbox.sh touch core/target/$(ARCH)-unknown-linux-musl/release/containerbox -web/node_modules/.package-lock.json: web/package.json sdk/dist +web/node_modules/.package-lock.json: web/package.json sdk/dist/package.json npm --prefix web ci touch web/node_modules/.package-lock.json -web/.angular: patch-db/client/dist sdk/dist web/node_modules/.package-lock.json +web/.angular/.updated: patch-db/client/dist/index.js sdk/dist/package.json web/node_modules/.package-lock.json rm -rf web/.angular mkdir -p web/.angular + touch web/.angular/.updated -web/dist/raw/ui: $(WEB_UI_SRC) $(WEB_SHARED_SRC) web/.angular +web/dist/raw/ui/index.html: $(WEB_UI_SRC) $(WEB_SHARED_SRC) web/.angular/.updated npm --prefix web run build:ui - touch web/dist/raw/ui + touch web/dist/raw/ui/index.html -web/dist/raw/setup-wizard: $(WEB_SETUP_WIZARD_SRC) $(WEB_SHARED_SRC) web/.angular +web/dist/raw/setup-wizard/index.html: $(WEB_SETUP_WIZARD_SRC) $(WEB_SHARED_SRC) web/.angular/.updated npm --prefix web run build:setup - touch web/dist/raw/setup-wizard + touch web/dist/raw/setup-wizard/index.html -web/dist/raw/install-wizard: $(WEB_INSTALL_WIZARD_SRC) $(WEB_SHARED_SRC) web/.angular +web/dist/raw/install-wizard/index.html: $(WEB_INSTALL_WIZARD_SRC) $(WEB_SHARED_SRC) web/.angular/.updated npm --prefix web run build:install-wiz - touch web/dist/raw/install-wizard + touch web/dist/raw/install-wizard/index.html -web/dist/static: $(WEB_UIS) $(ENVIRONMENT_FILE) +$(COMPRESSED_WEB_UIS): $(WEB_UIS) $(ENVIRONMENT_FILE) ./compress-uis.sh web/config.json: $(GIT_HASH_FILE) web/config-sample.json @@ -301,13 +303,14 @@ web/patchdb-ui-seed.json: web/package.json jq '."ack-welcome" = $(shell jq '.version' web/package.json)' web/patchdb-ui-seed.json > ui-seed.tmp mv ui-seed.tmp web/patchdb-ui-seed.json -patch-db/client/node_modules: patch-db/client/package.json +patch-db/client/node_modules/.package-lock.json: patch-db/client/package.json npm --prefix patch-db/client ci - touch patch-db/client/node_modules + touch patch-db/client/node_modules/.package-lock.json -patch-db/client/dist: $(PATCH_DB_CLIENT_SRC) patch-db/client/node_modules +patch-db/client/dist/index.js: $(PATCH_DB_CLIENT_SRC) patch-db/client/node_modules/.package-lock.json rm -rf patch-db/client/dist npm --prefix patch-db/client run build + touch patch-db/client/dist/index.js # used by github actions compiled-$(ARCH).tar: $(COMPILED_TARGETS) $(ENVIRONMENT_FILE) $(GIT_HASH_FILE) $(VERSION_FILE) diff --git a/build/dpkg-deps/depends b/build/dpkg-deps/depends index cd29714b2..f495df85d 100644 --- a/build/dpkg-deps/depends +++ b/build/dpkg-deps/depends @@ -9,6 +9,7 @@ ca-certificates cifs-utils cryptsetup curl +dnsutils dmidecode dosfstools e2fsprogs diff --git a/container-runtime/src/Adapters/EffectCreator.ts b/container-runtime/src/Adapters/EffectCreator.ts index e0390b1e1..3338560c9 100644 --- a/container-runtime/src/Adapters/EffectCreator.ts +++ b/container-runtime/src/Adapters/EffectCreator.ts @@ -127,7 +127,7 @@ function makeEffects(context: EffectContext): Effects { > }, subcontainer: { - createFs(options: { imageId: string }) { + createFs(options: { imageId: string; name: string }) { return rpcRound("subcontainer.create-fs", options) as ReturnType< T.Effects["subcontainer"]["createFs"] > diff --git a/container-runtime/src/Adapters/RpcListener.ts b/container-runtime/src/Adapters/RpcListener.ts index 860f1c066..458b1af0f 100644 --- a/container-runtime/src/Adapters/RpcListener.ts +++ b/container-runtime/src/Adapters/RpcListener.ts @@ -198,7 +198,11 @@ export class RpcListener { .then((x) => this.dealWithInput(x)) .catch(mapError) .then(logData("response")) - .then(writeDataToSocket), + .then(writeDataToSocket) + .catch((e) => { + console.error(`Major error in socket handling: ${e}`) + console.debug(`Data in: ${a.toString()}`) + }), ) }) } diff --git a/container-runtime/src/Adapters/Systems/SystemForEmbassy/DockerProcedureContainer.ts b/container-runtime/src/Adapters/Systems/SystemForEmbassy/DockerProcedureContainer.ts index 805f9b531..0c1f9d07a 100644 --- a/container-runtime/src/Adapters/Systems/SystemForEmbassy/DockerProcedureContainer.ts +++ b/container-runtime/src/Adapters/Systems/SystemForEmbassy/DockerProcedureContainer.ts @@ -20,6 +20,7 @@ export class DockerProcedureContainer { packageId: string, data: DockerProcedure, volumes: { [id: VolumeId]: Volume }, + name: string, options: { subcontainer?: ExecSpawnable } = {}, ) { const subcontainer = @@ -29,6 +30,7 @@ export class DockerProcedureContainer { packageId, data, volumes, + name, )) return new DockerProcedureContainer(subcontainer) } @@ -37,8 +39,13 @@ export class DockerProcedureContainer { packageId: string, data: DockerProcedure, volumes: { [id: VolumeId]: Volume }, + name: string, ) { - const subcontainer = await SubContainer.of(effects, { id: data.image }) + const subcontainer = await SubContainer.of( + effects, + { id: data.image }, + name, + ) if (data.mounts) { const mounts = data.mounts diff --git a/container-runtime/src/Adapters/Systems/SystemForEmbassy/MainLoop.ts b/container-runtime/src/Adapters/Systems/SystemForEmbassy/MainLoop.ts index cae3405b9..91b0177eb 100644 --- a/container-runtime/src/Adapters/Systems/SystemForEmbassy/MainLoop.ts +++ b/container-runtime/src/Adapters/Systems/SystemForEmbassy/MainLoop.ts @@ -62,6 +62,7 @@ export class MainLoop { this.system.manifest.id, this.system.manifest.main, this.system.manifest.volumes, + `Main - ${currentCommand.join(" ")}`, ) return CommandController.of()( this.effects, @@ -162,26 +163,29 @@ export class MainLoop { const subcontainer = actionProcedure.inject ? this.mainSubContainerHandle : undefined - // prettier-ignore - const container = - await DockerProcedureContainer.of( - effects, - manifest.id, - actionProcedure, - manifest.volumes, - { - subcontainer, - } - ) + const commands = [ + actionProcedure.entrypoint, + ...actionProcedure.args, + ] + const container = await DockerProcedureContainer.of( + effects, + manifest.id, + actionProcedure, + manifest.volumes, + `Health Check - ${commands.join(" ")}`, + { + subcontainer, + }, + ) const env: Record = actionProcedure.inject ? { HOME: "/root", } : {} - const executed = await container.exec( - [actionProcedure.entrypoint, ...actionProcedure.args], - { input: JSON.stringify(timeChanged), env }, - ) + const executed = await container.exec(commands, { + input: JSON.stringify(timeChanged), + env, + }) if (executed.exitCode === 0) { await effects.setHealth({ diff --git a/container-runtime/src/Adapters/Systems/SystemForEmbassy/index.ts b/container-runtime/src/Adapters/Systems/SystemForEmbassy/index.ts index 9e6382ca7..5b0380a1b 100644 --- a/container-runtime/src/Adapters/Systems/SystemForEmbassy/index.ts +++ b/container-runtime/src/Adapters/Systems/SystemForEmbassy/index.ts @@ -443,6 +443,7 @@ export class SystemForEmbassy implements System { ): Promise { const backup = this.manifest.backup.create if (backup.type === "docker") { + const commands = [backup.entrypoint, ...backup.args] const container = await DockerProcedureContainer.of( effects, this.manifest.id, @@ -451,8 +452,9 @@ export class SystemForEmbassy implements System { ...this.manifest.volumes, BACKUP: { type: "backup", readonly: false }, }, + `Backup - ${commands.join(" ")}`, ) - await container.execFail([backup.entrypoint, ...backup.args], timeoutMs) + await container.execFail(commands, timeoutMs) } else { const moduleCode = await this.moduleCode await moduleCode.createBackup?.(polyfillEffects(effects, this.manifest)) @@ -464,6 +466,7 @@ export class SystemForEmbassy implements System { ): Promise { const restoreBackup = this.manifest.backup.restore if (restoreBackup.type === "docker") { + const commands = [restoreBackup.entrypoint, ...restoreBackup.args] const container = await DockerProcedureContainer.of( effects, this.manifest.id, @@ -472,11 +475,9 @@ export class SystemForEmbassy implements System { ...this.manifest.volumes, BACKUP: { type: "backup", readonly: true }, }, + `Restore Backup - ${commands.join(" ")}`, ) - await container.execFail( - [restoreBackup.entrypoint, ...restoreBackup.args], - timeoutMs, - ) + await container.execFail(commands, timeoutMs) } else { const moduleCode = await this.moduleCode await moduleCode.restoreBackup?.(polyfillEffects(effects, this.manifest)) @@ -495,20 +496,17 @@ export class SystemForEmbassy implements System { const config = this.manifest.config?.get if (!config) return { spec: {} } if (config.type === "docker") { + const commands = [config.entrypoint, ...config.args] const container = await DockerProcedureContainer.of( effects, this.manifest.id, config, this.manifest.volumes, + `Get Config - ${commands.join(" ")}`, ) // TODO: yaml return JSON.parse( - ( - await container.execFail( - [config.entrypoint, ...config.args], - timeoutMs, - ) - ).stdout.toString(), + (await container.execFail(commands, timeoutMs)).stdout.toString(), ) } else { const moduleCode = await this.moduleCode @@ -543,24 +541,21 @@ export class SystemForEmbassy implements System { const setConfigValue = this.manifest.config?.set if (!setConfigValue) return if (setConfigValue.type === "docker") { + const commands = [ + setConfigValue.entrypoint, + ...setConfigValue.args, + JSON.stringify(newConfig), + ] const container = await DockerProcedureContainer.of( effects, this.manifest.id, setConfigValue, this.manifest.volumes, + `Set Config - ${commands.join(" ")}`, ) const answer = matchSetResult.unsafeCast( JSON.parse( - ( - await container.execFail( - [ - setConfigValue.entrypoint, - ...setConfigValue.args, - JSON.stringify(newConfig), - ], - timeoutMs, - ) - ).stdout.toString(), + (await container.execFail(commands, timeoutMs)).stdout.toString(), ), ) const dependsOn = answer["depends-on"] ?? answer.dependsOn ?? {} @@ -652,23 +647,20 @@ export class SystemForEmbassy implements System { if (migration) { const [version, procedure] = migration if (procedure.type === "docker") { + const commands = [ + procedure.entrypoint, + ...procedure.args, + JSON.stringify(fromVersion), + ] const container = await DockerProcedureContainer.of( effects, this.manifest.id, procedure, this.manifest.volumes, + `Migration - ${commands.join(" ")}`, ) return JSON.parse( - ( - await container.execFail( - [ - procedure.entrypoint, - ...procedure.args, - JSON.stringify(fromVersion), - ], - timeoutMs, - ) - ).stdout.toString(), + (await container.execFail(commands, timeoutMs)).stdout.toString(), ) } else if (procedure.type === "script") { const moduleCode = await this.moduleCode @@ -695,20 +687,17 @@ export class SystemForEmbassy implements System { const setConfigValue = this.manifest.properties if (!setConfigValue) throw new Error("There is no properties") if (setConfigValue.type === "docker") { + const commands = [setConfigValue.entrypoint, ...setConfigValue.args] const container = await DockerProcedureContainer.of( effects, this.manifest.id, setConfigValue, this.manifest.volumes, + `Properties - ${commands.join(" ")}`, ) const properties = matchProperties.unsafeCast( JSON.parse( - ( - await container.execFail( - [setConfigValue.entrypoint, ...setConfigValue.args], - timeoutMs, - ) - ).stdout.toString(), + (await container.execFail(commands, timeoutMs)).stdout.toString(), ), ) return asProperty(properties.data) @@ -761,6 +750,7 @@ export class SystemForEmbassy implements System { this.manifest.id, actionProcedure, this.manifest.volumes, + `Action ${actionId}`, { subcontainer, }, @@ -801,23 +791,20 @@ export class SystemForEmbassy implements System { const actionProcedure = this.manifest.dependencies?.[id]?.config?.check if (!actionProcedure) return { message: "Action not found", value: null } if (actionProcedure.type === "docker") { + const commands = [ + actionProcedure.entrypoint, + ...actionProcedure.args, + JSON.stringify(oldConfig), + ] const container = await DockerProcedureContainer.of( effects, this.manifest.id, actionProcedure, this.manifest.volumes, + `Dependencies Check - ${commands.join(" ")}`, ) return JSON.parse( - ( - await container.execFail( - [ - actionProcedure.entrypoint, - ...actionProcedure.args, - JSON.stringify(oldConfig), - ], - timeoutMs, - ) - ).stdout.toString(), + (await container.execFail(commands, timeoutMs)).stdout.toString(), ) } else if (actionProcedure.type === "script") { const moduleCode = await this.moduleCode diff --git a/container-runtime/src/Adapters/Systems/SystemForEmbassy/polyfillEffects.ts b/container-runtime/src/Adapters/Systems/SystemForEmbassy/polyfillEffects.ts index c212722e6..675cb2e9a 100644 --- a/container-runtime/src/Adapters/Systems/SystemForEmbassy/polyfillEffects.ts +++ b/container-runtime/src/Adapters/Systems/SystemForEmbassy/polyfillEffects.ts @@ -129,6 +129,7 @@ export const polyfillEffects = ( manifest.id, manifest.main, manifest.volumes, + [input.command, ...(input.args || [])].join(" "), ) const daemon = promiseSubcontainer.then((subcontainer) => daemons.runCommand()( diff --git a/core/Cargo.lock b/core/Cargo.lock index e9b9330e3..ebbcf6b5d 100644 --- a/core/Cargo.lock +++ b/core/Cargo.lock @@ -533,7 +533,7 @@ dependencies = [ "rustc-hash", "shlex", "syn 2.0.74", - "which 4.4.2", + "which", ] [[package]] @@ -3055,18 +3055,6 @@ dependencies = [ "unicase", ] -[[package]] -name = "nix" -version = "0.20.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa9b4819da1bc61c0ea48b63b7bc8604064dd43013e7cc325df098d49cd7c18a" -dependencies = [ - "bitflags 1.3.2", - "cc", - "cfg-if", - "libc", -] - [[package]] name = "nix" version = "0.24.3" @@ -5147,11 +5135,9 @@ dependencies = [ "tty-spawn", "typed-builder", "unix-named-pipe", - "unshare", "url", "urlencoding", "uuid", - "which 6.0.3", "zeroize", ] @@ -6051,16 +6037,6 @@ dependencies = [ "libc", ] -[[package]] -name = "unshare" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ceda295552a1eda89f8a748237654ad76b9c87e383fc07af5c4e423eb8e7b9b" -dependencies = [ - "libc", - "nix 0.20.0", -] - [[package]] name = "untrusted" version = "0.9.0" @@ -6278,18 +6254,6 @@ dependencies = [ "rustix", ] -[[package]] -name = "which" -version = "6.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4ee928febd44d98f2f459a4a79bd4d928591333a494a10a868418ac1b39cf1f" -dependencies = [ - "either", - "home", - "rustix", - "winsafe", -] - [[package]] name = "whoami" version = "1.5.1" @@ -6516,12 +6480,6 @@ dependencies = [ "windows-sys 0.48.0", ] -[[package]] -name = "winsafe" -version = "0.0.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d135d17ab770252ad95e9a872d365cf3090e3be864a34ab46f48555993efc904" - [[package]] name = "wyz" version = "0.2.0" diff --git a/core/startos/Cargo.toml b/core/startos/Cargo.toml index 9b3e7a048..f2b780b7b 100644 --- a/core/startos/Cargo.toml +++ b/core/startos/Cargo.toml @@ -39,7 +39,7 @@ path = "src/main.rs" [features] cli = [] -container-runtime = ["procfs", "unshare", "tty-spawn"] +container-runtime = ["procfs", "tty-spawn"] daemon = [] registry = [] default = ["cli", "daemon", "registry", "container-runtime"] @@ -207,9 +207,7 @@ trust-dns-server = "0.23.1" ts-rs = { git = "https://github.com/dr-bonez/ts-rs.git", branch = "feature/top-level-as" } # "8.1.0" tty-spawn = { version = "0.4.0", optional = true } typed-builder = "0.18.0" -which = "6.0.3" unix-named-pipe = "0.2.0" -unshare = { version = "0.7.0", optional = true } url = { version = "2.4.1", features = ["serde"] } urlencoding = "2.1.3" uuid = { version = "1.4.1", features = ["v4"] } diff --git a/core/startos/src/auth.rs b/core/startos/src/auth.rs index d998e9897..9a881d0e6 100644 --- a/core/startos/src/auth.rs +++ b/core/startos/src/auth.rs @@ -275,8 +275,8 @@ pub struct Session { #[serde(rename_all = "camelCase")] #[ts(export)] pub struct SessionList { - #[ts(type = "string")] - current: InternedString, + #[ts(type = "string | null")] + current: Option, sessions: Sessions, } @@ -323,7 +323,7 @@ fn display_sessions(params: WithIoFormat, arg: SessionList) { session.user_agent.as_deref().unwrap_or("N/A"), &format!("{}", session.metadata), ]; - if id == arg.current { + if Some(id) == arg.current { row.iter_mut() .map(|c| c.style(Attr::ForegroundColor(color::GREEN))) .collect::<()>() @@ -340,7 +340,7 @@ pub struct ListParams { #[arg(skip)] #[ts(skip)] #[serde(rename = "__auth_session")] // from Auth middleware - session: InternedString, + session: Option, } // #[command(display(display_sessions))] diff --git a/core/startos/src/context/rpc.rs b/core/startos/src/context/rpc.rs index 5330c58bc..c78f118ea 100644 --- a/core/startos/src/context/rpc.rs +++ b/core/startos/src/context/rpc.rs @@ -58,7 +58,7 @@ pub struct RpcContextSeed { pub shutdown: broadcast::Sender>, pub tor_socks: SocketAddr, pub lxc_manager: Arc, - pub open_authed_continuations: OpenAuthedContinuations, + pub open_authed_continuations: OpenAuthedContinuations>, pub rpc_continuations: RpcContinuations, pub callbacks: ServiceCallbacks, pub wifi_manager: Option>>, @@ -431,8 +431,8 @@ impl AsRef for RpcContext { &self.rpc_continuations } } -impl AsRef> for RpcContext { - fn as_ref(&self) -> &OpenAuthedContinuations { +impl AsRef>> for RpcContext { + fn as_ref(&self) -> &OpenAuthedContinuations> { &self.open_authed_continuations } } diff --git a/core/startos/src/db/mod.rs b/core/startos/src/db/mod.rs index ef35bd30d..ff42d6a5c 100644 --- a/core/startos/src/db/mod.rs +++ b/core/startos/src/db/mod.rs @@ -115,7 +115,7 @@ pub struct SubscribeParams { pointer: Option, #[ts(skip)] #[serde(rename = "__auth_session")] - session: InternedString, + session: Option, } #[derive(Deserialize, Serialize, TS)] diff --git a/core/startos/src/install/mod.rs b/core/startos/src/install/mod.rs index 7a545a3aa..3778d21ad 100644 --- a/core/startos/src/install/mod.rs +++ b/core/startos/src/install/mod.rs @@ -172,7 +172,7 @@ pub async fn install( pub struct SideloadParams { #[ts(skip)] #[serde(rename = "__auth_session")] - session: InternedString, + session: Option, } #[derive(Deserialize, Serialize, TS)] diff --git a/core/startos/src/lib.rs b/core/startos/src/lib.rs index feeb5a647..b5629aa04 100644 --- a/core/startos/src/lib.rs +++ b/core/startos/src/lib.rs @@ -323,6 +323,13 @@ pub fn package() -> ParentHandler { "connect", from_fn_async(service::connect_rpc_cli).no_display(), ) + .subcommand( + "attach", + from_fn_async(service::attach) + .with_metadata("get_session", Value::Bool(true)) + .no_cli(), + ) + .subcommand("attach", from_fn_async(service::cli_attach).no_display()) } pub fn diagnostic_api() -> ParentHandler { diff --git a/core/startos/src/middleware/auth.rs b/core/startos/src/middleware/auth.rs index 9b04afb38..7c5eaa4c2 100644 --- a/core/startos/src/middleware/auth.rs +++ b/core/startos/src/middleware/auth.rs @@ -49,7 +49,7 @@ impl HasLoggedOutSessions { .map(|s| s.as_logout_session_id()) .collect(); for sid in &to_log_out { - ctx.open_authed_continuations.kill(sid) + ctx.open_authed_continuations.kill(&Some(sid.clone())) } ctx.ephemeral_sessions.mutate(|s| { for sid in &to_log_out { diff --git a/core/startos/src/service/effects/subcontainer/mod.rs b/core/startos/src/service/effects/subcontainer/mod.rs index 0375ef6c2..65fcbd387 100644 --- a/core/startos/src/service/effects/subcontainer/mod.rs +++ b/core/startos/src/service/effects/subcontainer/mod.rs @@ -1,12 +1,15 @@ use std::path::{Path, PathBuf}; +use imbl_value::InternedString; use models::ImageId; use tokio::process::Command; -use crate::disk::mount::filesystem::overlayfs::OverlayGuard; use crate::rpc_continuations::Guid; use crate::service::effects::prelude::*; use crate::util::Invoke; +use crate::{ + disk::mount::filesystem::overlayfs::OverlayGuard, service::persistent_container::Subcontainer, +}; #[cfg(feature = "container-runtime")] mod sync; @@ -38,7 +41,7 @@ pub async fn destroy_subcontainer_fs( .await .remove(&guid) { - overlay.unmount(true).await?; + overlay.overlay.unmount(true).await?; } else { tracing::warn!("Could not find a subcontainer fs to destroy; assumming that it already is destroyed and will be skipping"); } @@ -50,11 +53,13 @@ pub async fn destroy_subcontainer_fs( #[ts(export)] pub struct CreateSubcontainerFsParams { image_id: ImageId, + #[ts(type = "string | null")] + name: Option, } #[instrument(skip_all)] pub async fn create_subcontainer_fs( context: EffectContext, - CreateSubcontainerFsParams { image_id }: CreateSubcontainerFsParams, + CreateSubcontainerFsParams { image_id, name }: CreateSubcontainerFsParams, ) -> Result<(PathBuf, Guid), Error> { let context = context.deref()?; if let Some(image) = context @@ -87,7 +92,13 @@ pub async fn create_subcontainer_fs( .with_kind(ErrorKind::Incoherent)?, ); tracing::info!("Mounting overlay {guid} for {image_id}"); - let guard = OverlayGuard::mount(image, &mountpoint).await?; + let subcontainer_wrapper = Subcontainer { + overlay: OverlayGuard::mount(image, &mountpoint).await?, + name: name + .unwrap_or_else(|| InternedString::intern(format!("subcontainer-{}", image_id))), + image_id: image_id.clone(), + }; + Command::new("chown") .arg("100000:100000") .arg(&mountpoint) @@ -100,7 +111,7 @@ pub async fn create_subcontainer_fs( .subcontainers .lock() .await - .insert(guid.clone(), guard); + .insert(guid.clone(), subcontainer_wrapper); Ok((container_mountpoint, guid)) } else { Err(Error::new( diff --git a/core/startos/src/service/effects/subcontainer/sync.rs b/core/startos/src/service/effects/subcontainer/sync.rs index f31eb8f62..702f34bbe 100644 --- a/core/startos/src/service/effects/subcontainer/sync.rs +++ b/core/startos/src/service/effects/subcontainer/sync.rs @@ -1,4 +1,3 @@ -use std::borrow::Cow; use std::collections::BTreeMap; use std::ffi::{c_int, OsStr, OsString}; use std::fs::File; @@ -12,7 +11,6 @@ use nix::unistd::Pid; use signal_hook::consts::signal::*; use tokio::sync::oneshot; use tty_spawn::TtySpawn; -use unshare::Command as NSCommand; use crate::service::effects::prelude::*; use crate::service::effects::ContainerCliContext; @@ -50,11 +48,13 @@ fn open_file_read(path: impl AsRef) -> Result { #[derive(Debug, Clone, Serialize, Deserialize, Parser)] pub struct ExecParams { - #[arg(short = 'e', long = "env")] + #[arg(long)] + force_tty: bool, + #[arg(short, long)] env: Option, - #[arg(short = 'w', long = "workdir")] + #[arg(short, long)] workdir: Option, - #[arg(short = 'u', long = "user")] + #[arg(short, long)] user: Option, chroot: PathBuf, #[arg(trailing_var_arg = true)] @@ -68,6 +68,7 @@ impl ExecParams { user, chroot, command, + .. } = self; let Some(([command], args)) = command.split_at_checked(1) else { return Err(Error::new( @@ -88,16 +89,6 @@ impl ExecParams { .collect::>(); std::os::unix::fs::chroot(chroot) .with_ctx(|_| (ErrorKind::Filesystem, lazy_format!("chroot {chroot:?}")))?; - let command = which::which_in( - command, - env.get("PATH") - .copied() - .map(Cow::Borrowed) - .or_else(|| std::env::var("PATH").ok().map(Cow::Owned)) - .as_deref(), - workdir.as_deref().unwrap_or(Path::new("/")), - ) - .with_kind(ErrorKind::Filesystem)?; let mut cmd = StdCommand::new(command); cmd.args(args); for (k, v) in env { @@ -135,6 +126,7 @@ impl ExecParams { pub fn launch( _: ContainerCliContext, ExecParams { + force_tty, env, workdir, user, @@ -142,47 +134,8 @@ pub fn launch( command, }: ExecParams, ) -> Result<(), Error> { - use unshare::{Namespace, Stdio}; - - use crate::service::cli::ContainerCliContext; - let mut sig = signal_hook::iterator::Signals::new(FWD_SIGNALS)?; - let mut cmd = NSCommand::new("/usr/bin/start-cli"); - cmd.arg("subcontainer").arg("launch-init"); - if let Some(env) = env { - cmd.arg("--env").arg(env); - } - if let Some(workdir) = workdir { - cmd.arg("--workdir").arg(workdir); - } - if let Some(user) = user { - cmd.arg("--user").arg(user); - } - cmd.arg(&chroot); - cmd.args(&command); - cmd.unshare(&[Namespace::Pid, Namespace::Cgroup, Namespace::Ipc]); - cmd.stdin(Stdio::piped()); - cmd.stdout(Stdio::piped()); - cmd.stderr(Stdio::piped()); - let (stdin_send, stdin_recv) = oneshot::channel(); - std::thread::spawn(move || { - if let Ok(mut stdin) = stdin_recv.blocking_recv() { - std::io::copy(&mut std::io::stdin(), &mut stdin).unwrap(); - } - }); - let (stdout_send, stdout_recv) = oneshot::channel(); - std::thread::spawn(move || { - if let Ok(mut stdout) = stdout_recv.blocking_recv() { - std::io::copy(&mut stdout, &mut std::io::stdout()).unwrap(); - } - }); - let (stderr_send, stderr_recv) = oneshot::channel(); - std::thread::spawn(move || { - if let Ok(mut stderr) = stderr_recv.blocking_recv() { - std::io::copy(&mut stderr, &mut std::io::stderr()).unwrap(); - } - }); if chroot.join("proc/1").exists() { - let ns_id = procfs::process::Process::new_with_root(chroot.join("proc")) + let ns_id = procfs::process::Process::new_with_root(chroot.join("proc/1")) .with_ctx(|_| (ErrorKind::Filesystem, "open subcontainer procfs"))? .namespaces() .with_ctx(|_| (ErrorKind::Filesystem, "read subcontainer pid 1 ns"))? @@ -225,20 +178,92 @@ pub fn launch( nix::mount::umount(&chroot.join("proc")) .with_ctx(|_| (ErrorKind::Filesystem, "unmounting subcontainer procfs"))?; } + + if (std::io::stdin().is_terminal() + && std::io::stdout().is_terminal() + && std::io::stderr().is_terminal()) + || force_tty + { + let mut cmd = TtySpawn::new("/usr/bin/start-cli"); + cmd.arg("subcontainer").arg("launch-init"); + if let Some(env) = env { + cmd.arg("--env").arg(env); + } + if let Some(workdir) = workdir { + cmd.arg("--workdir").arg(workdir); + } + if let Some(user) = user { + cmd.arg("--user").arg(user); + } + cmd.arg(&chroot); + cmd.args(command.iter()); + nix::sched::unshare(CloneFlags::CLONE_NEWPID) + .with_ctx(|_| (ErrorKind::Filesystem, "unshare pid ns"))?; + nix::sched::unshare(CloneFlags::CLONE_NEWCGROUP) + .with_ctx(|_| (ErrorKind::Filesystem, "unshare cgroup ns"))?; + nix::sched::unshare(CloneFlags::CLONE_NEWIPC) + .with_ctx(|_| (ErrorKind::Filesystem, "unshare ipc ns"))?; + std::process::exit(cmd.spawn().with_kind(ErrorKind::Filesystem)?); + } + + let mut sig = signal_hook::iterator::Signals::new(FWD_SIGNALS)?; + let (send_pid, recv_pid) = oneshot::channel(); + std::thread::spawn(move || { + if let Ok(pid) = recv_pid.blocking_recv() { + for sig in sig.forever() { + nix::sys::signal::kill( + Pid::from_raw(pid), + Some(nix::sys::signal::Signal::try_from(sig).unwrap()), + ) + .unwrap(); + } + } + }); + let mut cmd = StdCommand::new("/usr/bin/start-cli"); + cmd.arg("subcontainer").arg("launch-init"); + if let Some(env) = env { + cmd.arg("--env").arg(env); + } + if let Some(workdir) = workdir { + cmd.arg("--workdir").arg(workdir); + } + if let Some(user) = user { + cmd.arg("--user").arg(user); + } + cmd.arg(&chroot); + cmd.args(&command); + cmd.stdin(Stdio::piped()); + cmd.stdout(Stdio::piped()); + cmd.stderr(Stdio::piped()); + let (stdin_send, stdin_recv) = oneshot::channel(); + std::thread::spawn(move || { + if let Ok(mut stdin) = stdin_recv.blocking_recv() { + std::io::copy(&mut std::io::stdin(), &mut stdin).unwrap(); + } + }); + let (stdout_send, stdout_recv) = oneshot::channel(); + std::thread::spawn(move || { + if let Ok(mut stdout) = stdout_recv.blocking_recv() { + std::io::copy(&mut stdout, &mut std::io::stdout()).unwrap(); + } + }); + let (stderr_send, stderr_recv) = oneshot::channel(); + std::thread::spawn(move || { + if let Ok(mut stderr) = stderr_recv.blocking_recv() { + std::io::copy(&mut stderr, &mut std::io::stderr()).unwrap(); + } + }); + nix::sched::unshare(CloneFlags::CLONE_NEWPID) + .with_ctx(|_| (ErrorKind::Filesystem, "unshare pid ns"))?; + nix::sched::unshare(CloneFlags::CLONE_NEWCGROUP) + .with_ctx(|_| (ErrorKind::Filesystem, "unshare cgroup ns"))?; + nix::sched::unshare(CloneFlags::CLONE_NEWIPC) + .with_ctx(|_| (ErrorKind::Filesystem, "unshare ipc ns"))?; let mut child = cmd .spawn() .map_err(color_eyre::eyre::Report::msg) .with_ctx(|_| (ErrorKind::Filesystem, "spawning child process"))?; - let pid = child.pid(); - std::thread::spawn(move || { - for sig in sig.forever() { - nix::sys::signal::kill( - Pid::from_raw(pid), - Some(nix::sys::signal::Signal::try_from(sig).unwrap()), - ) - .unwrap(); - } - }); + send_pid.send(child.id() as i32).unwrap_or_default(); stdin_send .send(child.stdin.take().unwrap()) .unwrap_or_default(); @@ -253,16 +278,16 @@ pub fn launch( .wait() .with_ctx(|_| (ErrorKind::Filesystem, "waiting on child process"))?; if let Some(code) = exit.code() { + nix::mount::umount(&chroot.join("proc")) + .with_ctx(|_| (ErrorKind::Filesystem, "umount procfs"))?; std::process::exit(code); + } else if exit.success() { + Ok(()) } else { - if exit.success() { - Ok(()) - } else { - Err(Error::new( - color_eyre::eyre::Report::msg(exit), - ErrorKind::Unknown, - )) - } + Err(Error::new( + color_eyre::eyre::Report::msg(exit), + ErrorKind::Unknown, + )) } } @@ -288,6 +313,7 @@ pub fn launch_init(_: ContainerCliContext, params: ExecParams) -> Result<(), Err pub fn exec( _: ContainerCliContext, ExecParams { + force_tty, env, workdir, user, @@ -295,7 +321,11 @@ pub fn exec( command, }: ExecParams, ) -> Result<(), Error> { - if std::io::stdin().is_terminal() { + if (std::io::stdin().is_terminal() + && std::io::stdout().is_terminal() + && std::io::stderr().is_terminal()) + || force_tty + { let mut cmd = TtySpawn::new("/usr/bin/start-cli"); cmd.arg("subcontainer").arg("exec-command"); if let Some(env) = env { @@ -407,15 +437,13 @@ pub fn exec( .with_ctx(|_| (ErrorKind::Filesystem, "waiting on child process"))?; if let Some(code) = exit.code() { std::process::exit(code); + } else if exit.success() { + Ok(()) } else { - if exit.success() { - Ok(()) - } else { - Err(Error::new( - color_eyre::eyre::Report::msg(exit), - ErrorKind::Unknown, - )) - } + Err(Error::new( + color_eyre::eyre::Report::msg(exit), + ErrorKind::Unknown, + )) } } diff --git a/core/startos/src/service/mod.rs b/core/startos/src/service/mod.rs index 5eae62756..55f76fd14 100644 --- a/core/startos/src/service/mod.rs +++ b/core/startos/src/service/mod.rs @@ -1,18 +1,31 @@ +use std::io::IsTerminal; use std::ops::Deref; +use std::os::unix::process::ExitStatusExt; +use std::path::Path; +use std::process::Stdio; use std::sync::{Arc, Weak}; use std::time::Duration; +use std::{ffi::OsString, path::PathBuf}; +use axum::extract::ws::WebSocket; use chrono::{DateTime, Utc}; use clap::Parser; use futures::future::BoxFuture; -use imbl::OrdMap; -use models::{HealthCheckId, PackageId, ProcedureName}; -use persistent_container::PersistentContainer; +use futures::stream::FusedStream; +use futures::{SinkExt, StreamExt, TryStreamExt}; +use imbl_value::{json, InternedString}; +use itertools::Itertools; +use models::{ImageId, PackageId, ProcedureName}; +use nix::sys::signal::Signal; +use persistent_container::{PersistentContainer, Subcontainer}; use rpc_toolkit::{from_fn_async, CallRemoteHandler, Empty, HandlerArgs, HandlerFor}; use serde::{Deserialize, Serialize}; use service_actor::ServiceActor; use start_stop::StartStop; +use tokio::io::{AsyncReadExt, AsyncWriteExt}; +use tokio::process::Command; use tokio::sync::Notify; +use tokio_tungstenite::tungstenite::protocol::frame::coding::CloseCode; use ts_rs::TS; use crate::context::{CliContext, RpcContext}; @@ -24,15 +37,16 @@ use crate::install::PKG_ARCHIVE_DIR; use crate::lxc::ContainerId; use crate::prelude::*; use crate::progress::{NamedProgress, Progress}; -use crate::rpc_continuations::Guid; +use crate::rpc_continuations::{Guid, RpcContinuation}; use crate::s9pk::S9pk; use crate::service::service_map::InstallProgressHandles; -use crate::status::health_check::NamedHealthCheckResult; use crate::util::actor::concurrent::ConcurrentActor; -use crate::util::io::create_file; +use crate::util::io::{create_file, AsyncReadStream}; +use crate::util::net::WebSocketExt; use crate::util::serde::{NoOutput, Pem}; use crate::util::Never; use crate::volume::data_dir; +use crate::CAP_1_KiB; mod action; pub mod cli; @@ -68,6 +82,8 @@ pub enum LoadDisposition { Undo, } +struct RootCommand(pub String); + pub struct ServiceRef(Arc); impl ServiceRef { pub fn weak(&self) -> Weak { @@ -183,7 +199,7 @@ impl ServiceRef { impl Deref for ServiceRef { type Target = Service; fn deref(&self) -> &Self::Target { - &*self.0 + &self.0 } } impl From for ServiceRef { @@ -354,7 +370,7 @@ impl Service { tracing::debug!("{e:?}") }) { - match ServiceRef::from(service).uninstall(None).await { + match service.uninstall(None).await { Err(e) => { tracing::error!("Error uninstalling service: {e}"); tracing::debug!("{e:?}") @@ -578,3 +594,488 @@ pub async fn connect_rpc_cli( crate::lxc::connect_cli(&ctx, guid).await } + +#[derive(Deserialize, Serialize, TS)] +#[serde(rename_all = "camelCase")] +pub struct AttachParams { + pub id: PackageId, + #[ts(type = "string[]")] + pub command: Vec, + pub tty: bool, + #[ts(skip)] + #[serde(rename = "__auth_session")] + session: Option, + #[ts(type = "string | null")] + subcontainer: Option, + #[ts(type = "string | null")] + name: Option, + #[ts(type = "string | null")] + image_id: Option, +} +pub async fn attach( + ctx: RpcContext, + AttachParams { + id, + command, + tty, + session, + subcontainer, + image_id, + name, + }: AttachParams, +) -> Result { + let (container_id, subcontainer_id, image_id, workdir, root_command) = { + let id = &id; + + 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 root_dir = container + .lxc_container + .get() + .map(|x| x.rootfs_dir().to_owned()) + .or_not_found(format!("container for {id}"))?; + + let subcontainer = subcontainer.map(|x| AsRef::::as_ref(&x).to_uppercase()); + let name = name.map(|x| AsRef::::as_ref(&x).to_uppercase()); + let image_id = image_id.map(|x| AsRef::::as_ref(&x).to_string_lossy().to_uppercase()); + + let subcontainers = container.subcontainers.lock().await; + let subcontainer_ids: Vec<_> = subcontainers + .iter() + .filter(|(x, wrapper)| { + if let Some(subcontainer) = subcontainer.as_ref() { + AsRef::::as_ref(x).contains(AsRef::::as_ref(subcontainer)) + } else if let Some(name) = name.as_ref() { + AsRef::::as_ref(&wrapper.name) + .to_uppercase() + .contains(AsRef::::as_ref(name)) + } else if let Some(image_id) = image_id.as_ref() { + let Some(wrapper_image_id) = AsRef::::as_ref(&wrapper.image_id).to_str() + else { + return false; + }; + wrapper_image_id + .to_uppercase() + .contains(AsRef::::as_ref(&image_id)) + } else { + true + } + }) + .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 + .first() + .map::<(Guid, ImageId), _>(|&x| (x.0.clone(), x.1.image_id.clone())) + else { + drop(subcontainers); + let subcontainers = container + .subcontainers + .lock() + .await + .iter() + .map(format_subcontainer_pair) + .join("\n"); + return Err(Error::new( + eyre!("no matching subcontainers are running for {id}; some possible choices are:\n{subcontainers}"), + ErrorKind::NotFound, + )); + }; + + let passwd = root_dir + .join("media/startos/subcontainers") + .join(subcontainer_id.as_ref()) + .join("etc") + .join("passwd"); + + let root_command = get_passwd_root_command(passwd).await; + + let workdir = attach_workdir(&image_id, &root_dir).await?; + + if subcontainer_ids.len() > 1 { + let subcontainer_ids = subcontainer_ids + .into_iter() + .map(format_subcontainer_pair) + .join("\n"); + return Err(Error::new( + eyre!("multiple subcontainers found for {id}: \n{subcontainer_ids}"), + ErrorKind::InvalidRequest, + )); + } + + ( + service_ref.container_id()?, + subcontainer_id, + image_id, + workdir, + root_command, + ) + }; + + let guid = Guid::new(); + async fn handler( + ws: &mut WebSocket, + container_id: ContainerId, + subcontainer_id: Guid, + command: Vec, + tty: bool, + image_id: ImageId, + workdir: Option, + root_command: &RootCommand, + ) -> Result<(), Error> { + use axum::extract::ws::Message; + + let mut ws = ws.fuse(); + + let mut cmd = Command::new("lxc-attach"); + let root_path = Path::new("/media/startos/subcontainers").join(subcontainer_id.as_ref()); + cmd.kill_on_drop(true); + + cmd.arg(&*container_id) + .arg("--") + .arg("start-cli") + .arg("subcontainer") + .arg("exec") + .arg("--env") + .arg( + Path::new("/media/startos/images") + .join(image_id) + .with_extension("env"), + ); + + if let Some(workdir) = workdir { + cmd.arg("--workdir").arg(workdir); + } + + if tty { + cmd.arg("--force-tty"); + } + + cmd.arg(&root_path).arg("--"); + + if command.is_empty() { + cmd.arg(&root_command.0); + } else { + cmd.args(&command); + } + + let mut child = cmd + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn()?; + + let pid = nix::unistd::Pid::from_raw(child.id().or_not_found("child pid")? as i32); + + let mut stdin = child.stdin.take().or_not_found("child stdin")?; + + let mut current_in = "stdin".to_owned(); + let mut current_out = "stdout"; + ws.send(Message::Text(current_out.into())) + .await + .with_kind(ErrorKind::Network)?; + let mut stdout = AsyncReadStream::new( + child.stdout.take().or_not_found("child stdout")?, + 4 * CAP_1_KiB, + ) + .fuse(); + let mut stderr = AsyncReadStream::new( + child.stderr.take().or_not_found("child stderr")?, + 4 * CAP_1_KiB, + ) + .fuse(); + + loop { + futures::select_biased! { + out = stdout.try_next() => { + if let Some(out) = out? { + if current_out != "stdout" { + ws.send(Message::Text("stdout".into())) + .await + .with_kind(ErrorKind::Network)?; + current_out = "stdout"; + } + dbg!(¤t_out); + ws.send(Message::Binary(out)) + .await + .with_kind(ErrorKind::Network)?; + } + } + err = stderr.try_next() => { + if let Some(err) = err? { + if current_out != "stderr" { + ws.send(Message::Text("stderr".into())) + .await + .with_kind(ErrorKind::Network)?; + current_out = "stderr"; + } + dbg!(¤t_out); + ws.send(Message::Binary(err)) + .await + .with_kind(ErrorKind::Network)?; + } + } + msg = ws.try_next() => { + if let Some(msg) = msg.with_kind(ErrorKind::Network)? { + match msg { + Message::Text(in_ty) => { + current_in = in_ty; + } + Message::Binary(data) => { + match &*current_in { + "stdin" => { + stdin.write_all(&data).await?; + } + "signal" => { + if data.len() != 4 { + return Err(Error::new( + eyre!("invalid byte length for signal: {}", data.len()), + ErrorKind::InvalidRequest + )); + } + let mut sig_buf = [0u8; 4]; + sig_buf.clone_from_slice(&data); + nix::sys::signal::kill( + pid, + Signal::try_from(i32::from_be_bytes(sig_buf)) + .with_kind(ErrorKind::InvalidRequest)? + ).with_kind(ErrorKind::Filesystem)?; + } + _ => (), + } + } + _ => () + } + } else { + return Ok(()) + } + } + } + if stdout.is_terminated() && stderr.is_terminated() { + break; + } + } + + let exit = child.wait().await?; + ws.send(Message::Text("exit".into())) + .await + .with_kind(ErrorKind::Network)?; + ws.send(Message::Binary(i32::to_be_bytes(exit.into_raw()).to_vec())) + .await + .with_kind(ErrorKind::Network)?; + + Ok(()) + } + ctx.rpc_continuations + .add( + guid.clone(), + RpcContinuation::ws_authed( + &ctx, + session, + move |mut ws| async move { + if let Err(e) = handler( + &mut ws, + container_id, + subcontainer_id, + command, + tty, + image_id, + workdir, + &root_command, + ) + .await + { + tracing::error!("Error in attach websocket: {e}"); + tracing::debug!("{e:?}"); + ws.close_result(Err::<&str, _>(e)).await.log_err(); + } else { + ws.normal_close("exit").await.log_err(); + } + }, + Duration::from_secs(30), + ), + ) + .await; + + Ok(guid) +} + +async fn attach_workdir(image_id: &ImageId, root_dir: &Path) -> Result, Error> { + let path_str = root_dir.join("media/startos/images/"); + + let mut subcontainer_json = + tokio::fs::File::open(path_str.join(image_id).with_extension("json")).await?; + let mut contents = vec![]; + subcontainer_json.read_to_end(&mut contents).await?; + let subcontainer_json: serde_json::Value = + serde_json::from_slice(&contents).with_kind(ErrorKind::Filesystem)?; + Ok(subcontainer_json["workdir"].as_str().map(|x| x.to_string())) +} + +async fn get_passwd_root_command(etc_passwd_path: PathBuf) -> RootCommand { + async { + let mut file = tokio::fs::File::open(etc_passwd_path).await?; + + let mut contents = vec![]; + file.read_to_end(&mut contents).await?; + + let contents = String::from_utf8_lossy(&contents); + + for line in contents.split('\n') { + let line_information = line.split(':').collect::>(); + if let (Some(&"root"), Some(shell)) = + (line_information.first(), line_information.last()) + { + return Ok(shell.to_string()); + } + } + Err(Error::new( + eyre!("Could not parse /etc/passwd for shell: {}", contents), + ErrorKind::Filesystem, + )) + } + .await + .map(RootCommand) + .unwrap_or_else(|e| { + tracing::error!("Could not get the /etc/passwd: {e}"); + tracing::debug!("{e:?}"); + RootCommand("/bin/sh".to_string()) + }) +} + +#[derive(Deserialize, Serialize, Parser)] +pub struct CliAttachParams { + pub id: PackageId, + #[arg(long)] + pub force_tty: bool, + #[arg(trailing_var_arg = true)] + pub command: Vec, + #[arg(long, short)] + subcontainer: Option, + #[arg(long, short)] + name: Option, + #[arg(long, short)] + image_id: Option, +} +pub async fn cli_attach( + HandlerArgs { + context, + parent_method, + method, + params, + .. + }: HandlerArgs, +) -> Result<(), Error> { + use tokio_tungstenite::tungstenite::Message; + + let guid: Guid = from_value( + context + .call_remote::( + &parent_method.into_iter().chain(method).join("."), + json!({ + "id": params.id, + "command": params.command, + "tty": (std::io::stdin().is_terminal() + && std::io::stdout().is_terminal() + && std::io::stderr().is_terminal()) + || params.force_tty, + "subcontainer": params.subcontainer, + "imageId": params.image_id, + "name": params.name, + }), + ) + .await?, + )?; + let mut ws = context.ws_continuation(guid).await?; + + let mut current_in = "stdin"; + let mut current_out = "stdout".to_owned(); + ws.send(Message::Text(current_in.into())) + .await + .with_kind(ErrorKind::Network)?; + let mut stdin = AsyncReadStream::new(tokio::io::stdin(), 4 * CAP_1_KiB).fuse(); + let mut stdout = tokio::io::stdout(); + let mut stderr = tokio::io::stderr(); + loop { + futures::select_biased! { + // signal = tokio:: => { + // let exit = exit?; + // if current_out != "exit" { + // ws.send(Message::Text("exit".into())) + // .await + // .with_kind(ErrorKind::Network)?; + // current_out = "exit"; + // } + // ws.send(Message::Binary( + // i32::to_be_bytes(exit.into_raw()).to_vec() + // )).await.with_kind(ErrorKind::Network)?; + // } + input = stdin.try_next() => { + if let Some(input) = input? { + if current_in != "stdin" { + ws.send(Message::Text("stdin".into())) + .await + .with_kind(ErrorKind::Network)?; + current_in = "stdin"; + } + ws.send(Message::Binary(input)) + .await + .with_kind(ErrorKind::Network)?; + } + } + msg = ws.try_next() => { + if let Some(msg) = msg.with_kind(ErrorKind::Network)? { + match msg { + Message::Text(out_ty) => { + current_out = out_ty; + } + Message::Binary(data) => { + match &*current_out { + "stdout" => { + stdout.write_all(&data).await?; + stdout.flush().await?; + } + "stderr" => { + stderr.write_all(&data).await?; + stderr.flush().await?; + } + "exit" => { + if data.len() != 4 { + return Err(Error::new( + eyre!("invalid byte length for exit code: {}", data.len()), + ErrorKind::InvalidRequest + )); + } + let mut exit_buf = [0u8; 4]; + exit_buf.clone_from_slice(&data); + let code = i32::from_be_bytes(exit_buf); + std::process::exit(code); + } + _ => (), + } + } + Message::Close(Some(close)) => { + if close.code != CloseCode::Normal { + return Err(Error::new( + color_eyre::eyre::Report::msg(close.reason), + ErrorKind::Network + )); + } + } + _ => () + } + } else { + return Ok(()) + } + } + } + } +} diff --git a/core/startos/src/service/persistent_container.rs b/core/startos/src/service/persistent_container.rs index dd7b5766d..ff9d74009 100644 --- a/core/startos/src/service/persistent_container.rs +++ b/core/startos/src/service/persistent_container.rs @@ -4,9 +4,10 @@ use std::sync::{Arc, Weak}; use std::time::Duration; use futures::future::ready; -use futures::{Future, FutureExt}; +use futures::Future; use helpers::NonDetachingJoinHandle; use imbl::Vector; +use imbl_value::InternedString; use models::{ImageId, ProcedureName, VolumeId}; use rpc_toolkit::{Empty, Server, ShutdownHandle}; use serde::de::DeserializeOwned; @@ -36,7 +37,7 @@ use crate::service::{rpc, RunningStatus, Service}; use crate::util::io::create_file; use crate::util::rpc_client::UnixRpcClient; use crate::util::Invoke; -use crate::volume::{asset_dir, data_dir}; +use crate::volume::data_dir; use crate::ARCH; const RPC_CONNECT_TIMEOUT: Duration = Duration::from_secs(10); @@ -84,6 +85,14 @@ impl ServiceState { } } +/// Want to have a wrapper for uses like the inject where we are going to be finding the subcontainer and doing some filtering on it. +/// As well, the imageName is also used for things like env. +pub struct Subcontainer { + pub(super) name: InternedString, + pub(super) image_id: ImageId, + pub(super) overlay: OverlayGuard>, +} + // @DRB On top of this we need to also have the procedures to have the effects and get the results back for them, maybe lock them to the running instance? /// This contains the LXC container running the javascript init system /// that can be used via a JSON RPC Client connected to a unix domain @@ -98,7 +107,7 @@ pub struct PersistentContainer { volumes: BTreeMap, assets: BTreeMap, pub(super) images: BTreeMap>, - pub(super) subcontainers: Arc>>>>, + pub(super) subcontainers: Arc>>, pub(super) state: Arc>, pub(super) net_service: Mutex, destroyed: bool, @@ -405,7 +414,7 @@ impl PersistentContainer { errs.handle(assets.unmount(true).await); } for (_, overlay) in std::mem::take(&mut *subcontainers.lock().await) { - errs.handle(overlay.unmount(true).await); + errs.handle(overlay.overlay.unmount(true).await); } for (_, images) in images { errs.handle(images.unmount().await); diff --git a/core/startos/src/upload.rs b/core/startos/src/upload.rs index 20f294400..73519c603 100644 --- a/core/startos/src/upload.rs +++ b/core/startos/src/upload.rs @@ -25,7 +25,7 @@ use crate::util::io::{create_file, TmpDir}; pub async fn upload( ctx: &RpcContext, - session: InternedString, + session: Option, ) -> Result<(Guid, UploadingFile), Error> { let guid = Guid::new(); let (mut handle, file) = UploadingFile::new().await?; diff --git a/core/startos/src/util/io.rs b/core/startos/src/util/io.rs index 6d9c8a4ff..e3059c862 100644 --- a/core/startos/src/util/io.rs +++ b/core/startos/src/util/io.rs @@ -1,6 +1,7 @@ use std::collections::VecDeque; use std::future::Future; use std::io::Cursor; +use std::mem::MaybeUninit; use std::os::unix::prelude::MetadataExt; use std::path::{Path, PathBuf}; use std::pin::Pin; @@ -11,7 +12,7 @@ use std::time::Duration; use bytes::{Buf, BytesMut}; use futures::future::{BoxFuture, Fuse}; -use futures::{AsyncSeek, FutureExt, TryStreamExt}; +use futures::{AsyncSeek, FutureExt, Stream, TryStreamExt}; use helpers::NonDetachingJoinHandle; use nix::unistd::{Gid, Uid}; use tokio::fs::File; @@ -23,6 +24,7 @@ use tokio::sync::{Notify, OwnedMutexGuard}; use tokio::time::{Instant, Sleep}; use crate::prelude::*; +use crate::{CAP_1_KiB, CAP_1_MiB}; pub trait AsyncReadSeek: AsyncRead + AsyncSeek {} impl AsyncReadSeek for T {} @@ -1267,3 +1269,33 @@ impl AsyncWrite for MutexIO { Pin::new(&mut *self.get_mut().0).poll_shutdown(cx) } } + +#[pin_project::pin_project] +pub struct AsyncReadStream { + buffer: Vec>, + #[pin] + pub io: T, +} +impl AsyncReadStream { + pub fn new(io: T, buffer_size: usize) -> Self { + Self { + buffer: vec![MaybeUninit::uninit(); buffer_size], + io, + } + } +} +impl Stream for AsyncReadStream { + type Item = Result, Error>; + fn poll_next( + self: Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + ) -> Poll> { + let this = self.project(); + let mut buf = ReadBuf::uninit(this.buffer); + match futures::ready!(this.io.poll_read(cx, &mut buf)) { + Ok(()) if buf.filled().is_empty() => Poll::Ready(None), + Ok(()) => Poll::Ready(Some(Ok(buf.filled().to_vec()))), + Err(e) => Poll::Ready(Some(Err(e.into()))), + } + } +} diff --git a/sdk/lib/StartSdk.ts b/sdk/lib/StartSdk.ts index 658597bc2..87c4e14ce 100644 --- a/sdk/lib/StartSdk.ts +++ b/sdk/lib/StartSdk.ts @@ -782,6 +782,7 @@ export async function runCommand( effects, image, options.mounts || [], + commands.join(" "), (subcontainer) => subcontainer.exec(commands), ) } diff --git a/sdk/lib/mainFn/CommandController.ts b/sdk/lib/mainFn/CommandController.ts index 8a0505f68..9c206803a 100644 --- a/sdk/lib/mainFn/CommandController.ts +++ b/sdk/lib/mainFn/CommandController.ts @@ -31,6 +31,7 @@ export class CommandController { | SubContainer, command: T.CommandType, options: { + subcontainerName?: string // Defaults to the DEFAULT_SIGTERM_TIMEOUT = 30_000ms sigtermTimeout?: number mounts?: { path: string; options: MountOptions }[] @@ -51,7 +52,11 @@ export class CommandController { subcontainer instanceof SubContainer ? subcontainer : await (async () => { - const subc = await SubContainer.of(effects, subcontainer) + const subc = await SubContainer.of( + effects, + subcontainer, + options?.subcontainerName || commands.join(" "), + ) for (let mount of options.mounts || []) { await subc.mount(mount.options, mount.path) } @@ -119,6 +124,11 @@ export class CommandController { async term({ signal = SIGTERM, timeout = this.sigtermTimeout } = {}) { try { if (!this.state.exited) { + if (signal !== "SIGKILL") { + setTimeout(() => { + if (!this.state.exited) this.process.kill("SIGKILL") + }, timeout) + } if (!this.process.kill(signal)) { console.error( `failed to send signal ${signal} to pid ${this.process.pid}`, @@ -126,11 +136,6 @@ export class CommandController { } } - if (signal !== "SIGKILL") { - setTimeout(() => { - this.process.kill("SIGKILL") - }, timeout) - } await this.runningAnswer } finally { await this.subcontainer.destroy?.() diff --git a/sdk/lib/mainFn/Daemon.ts b/sdk/lib/mainFn/Daemon.ts index 87a7d705d..6fa1d1085 100644 --- a/sdk/lib/mainFn/Daemon.ts +++ b/sdk/lib/mainFn/Daemon.ts @@ -28,6 +28,7 @@ export class Daemon { | SubContainer, command: T.CommandType, options: { + subcontainerName?: string mounts?: { path: string; options: MountOptions }[] env?: | { diff --git a/sdk/lib/mainFn/Daemons.ts b/sdk/lib/mainFn/Daemons.ts index 1ecec28d3..b17f3a1a1 100644 --- a/sdk/lib/mainFn/Daemons.ts +++ b/sdk/lib/mainFn/Daemons.ts @@ -127,6 +127,7 @@ export class Daemons { const daemon = Daemon.of()(this.effects, options.image, options.command, { ...options, mounts: options.mounts.build(), + subcontainerName: id, }) const healthDaemon = new HealthDaemon( daemon, diff --git a/sdk/lib/osBindings/CreateSubcontainerFsParams.ts b/sdk/lib/osBindings/CreateSubcontainerFsParams.ts index 729ad4240..32d301e4a 100644 --- a/sdk/lib/osBindings/CreateSubcontainerFsParams.ts +++ b/sdk/lib/osBindings/CreateSubcontainerFsParams.ts @@ -1,4 +1,7 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. import type { ImageId } from "./ImageId" -export type CreateSubcontainerFsParams = { imageId: ImageId } +export type CreateSubcontainerFsParams = { + imageId: ImageId + name: string | null +} diff --git a/sdk/lib/osBindings/SessionList.ts b/sdk/lib/osBindings/SessionList.ts index a85a42dee..af36aaa8a 100644 --- a/sdk/lib/osBindings/SessionList.ts +++ b/sdk/lib/osBindings/SessionList.ts @@ -1,4 +1,4 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. import type { Sessions } from "./Sessions" -export type SessionList = { current: string; sessions: Sessions } +export type SessionList = { current: string | null; sessions: Sessions } diff --git a/sdk/lib/types.ts b/sdk/lib/types.ts index 4820f419d..68bd7b588 100644 --- a/sdk/lib/types.ts +++ b/sdk/lib/types.ts @@ -366,7 +366,10 @@ export type Effects = { // subcontainer subcontainer: { /** A low level api used by SubContainer */ - createFs(options: { imageId: string }): Promise<[string, string]> + createFs(options: { + imageId: string + name: string | null + }): Promise<[string, string]> /** A low level api used by SubContainer */ destroyFs(options: { guid: string }): Promise } diff --git a/sdk/lib/util/SubContainer.ts b/sdk/lib/util/SubContainer.ts index bbc5c5f64..82c1ae1b3 100644 --- a/sdk/lib/util/SubContainer.ts +++ b/sdk/lib/util/SubContainer.ts @@ -86,10 +86,12 @@ export class SubContainer implements ExecSpawnable { static async of( effects: T.Effects, image: { id: T.ImageId; sharedRun?: boolean }, + name: string, ) { const { id, sharedRun } = image const [rootfs, guid] = await effects.subcontainer.createFs({ imageId: id as string, + name, }) const shared = ["dev", "sys"] @@ -115,9 +117,10 @@ export class SubContainer implements ExecSpawnable { effects: T.Effects, image: { id: T.ImageId; sharedRun?: boolean }, mounts: { options: MountOptions; path: string }[], + name: string, fn: (subContainer: SubContainer) => Promise, ): Promise { - const subContainer = await SubContainer.of(effects, image) + const subContainer = await SubContainer.of(effects, image, name) try { for (let mount of mounts) { await subContainer.mount(mount.options, mount.path) @@ -179,10 +182,12 @@ export class SubContainer implements ExecSpawnable { } return new Promise((resolve, reject) => { try { + let timeout = setTimeout(() => this.leader.kill("SIGKILL"), 30000) this.leader.on("exit", () => { + clearTimeout(timeout) resolve() }) - if (!this.leader.kill("SIGKILL")) { + if (!this.leader.kill("SIGTERM")) { reject(new Error("kill(2) failed")) } } catch (e) {