Compare commits

...

411 Commits

Author SHA1 Message Date
Aiden McClelland
04196df976 fix getNext 2025-12-19 12:35:06 -07:00
Matt Hill
30b1654666 random subnet and also 80 to 5443 2025-12-19 09:59:12 -07:00
Aiden McClelland
f41710c892 dynamic subnet in port forward 2025-12-18 20:19:08 -07:00
Aiden McClelland
df3f79f282 fix ws timeouts 2025-12-18 14:54:19 -07:00
Aiden McClelland
f8df692865 Merge pull request #3079 from Start9Labs/hotfix/alpha.16
hotfixes for alpha.16
2025-12-18 11:32:47 -07:00
Aiden McClelland
0c6d3b188d Merge branch 'hotfix/alpha.16' of github.com:Start9Labs/start-os into hotfix/alpha.16 2025-12-18 11:31:51 -07:00
Aiden McClelland
e7a38863ab fix registry auth 2025-12-18 11:31:30 -07:00
Alex Inkin
720e0fcdab fix: keep uptime width constant and service table DOM cached (#3078)
* fix: keep uptime width constant and service table DOM cached

* show error status and fix columns spacing

* revert const

---------

Co-authored-by: Matt Hill <mattnine@protonmail.com>
2025-12-18 07:51:34 -07:00
Aiden McClelland
bf8ff84522 inconsequential ssl changes 2025-12-18 05:56:51 -07:00
Aiden McClelland
5a9510238e add map & eq to getServiceInterface 2025-12-18 04:30:08 -07:00
Aiden McClelland
7b3c74179b add local auth to registry 2025-12-18 04:29:34 -07:00
Aiden McClelland
cd70fa4c32 hotfixes for alpha.16 2025-12-18 04:22:56 -07:00
Aiden McClelland
83133ced6a consolidate crates 2025-12-17 21:15:24 -07:00
Aiden McClelland
6c5179a179 handle flavor atom version range 2025-12-17 14:18:43 -07:00
Aiden McClelland
e33ab39b85 hotfix 2025-12-17 12:17:22 -07:00
Aiden McClelland
9567bcec1b randomize default start-tunnel subnet 2025-12-16 17:34:23 -07:00
Aiden McClelland
550b16dc0b fix build for cargo deps 2025-12-16 17:33:55 -07:00
Matt Hill
5d8331b7f7 Feature/tor logs (#3077)
* add tor logs, rework services page, other small things

* feat: sortable service table and mobile view

---------

Co-authored-by: waterplea <alexander@inkin.ru>
2025-12-16 12:47:43 -07:00
Aiden McClelland
e35b643e51 use arm runner for riscv 2025-12-15 16:19:06 -07:00
Aiden McClelland
bc6a92677b readd riscv target 2025-12-15 16:18:44 -07:00
Aiden McClelland
f52072e6ec sdk beta.45 2025-12-15 15:23:05 -07:00
Remco Ros
9c43c43a46 fix: shutdown order (#3073)
* fix: race condition in Daemon.stop()

* fix: do not stop Daemon on context leave

* fix: remove duplicate Daemons.term calls

* feat: honor dependency order when shutting terminating Daemons

* fixes, and remove started

---------

Co-authored-by: Aiden McClelland <me@drbonez.dev>
2025-12-15 15:21:23 -07:00
Aiden McClelland
0430e0f930 alpha.16 (#3068)
* add support for idmapped mounts to start-sdk

* misc fixes

* misc fixes

* add default to textarea

* fix iptables masquerade rule

* fix textarea types

* more fixes

* better logging for rsync

* fix tty size

* fix wg conf generation for android

* disable file mounts on dependencies

* mostly there, some styling issues (#3069)

* mostly there, some styling issues

* fix: address comments (#3070)

* fix: address comments

* fix: fix

* show SSL for any address with secure protocol and ssl added

* better sorting and messaging

---------

Co-authored-by: Alex Inkin <alexander@inkin.ru>

* fixes for nextcloud

* allow sidebar navigation during service state traansitions

* wip: x-forwarded headers

* implement x-forwarded-for proxy

* lowercase domain names and fix warning popover bug

* fix http2 websockets

* fix websocket retry behavior

* add arch filters to s9pk pack

* use docker for start-cli install

* add version range to package signer on registry

* fix rcs < 0

* fix user information parsing

* refactor service interface getters

* disable idmaps

* build fixes

* update docker login action

* streamline build

* add start-cli workflow

* rename

* riscv64gc

* fix ui packing

* no default features on cli

* make cli depend on GIT_HASH

* more build fixes

* more build fixes

* interpolate arch within dockerfile

* fix tests

* add launch ui to service page plus other small improvements (#3075)

* add launch ui to service page plus other small improvements

* revert translation disable

* add spinner to service list if service is health and loading

* chore: some visual tune up

* chore: update Taiga UI

---------

Co-authored-by: waterplea <alexander@inkin.ru>

* fix backups

* feat: use arm hosted runners and don't fail when apt package does not exist (#3076)

---------

Co-authored-by: Matt Hill <mattnine@protonmail.com>
Co-authored-by: Shadowy Super Coder <musashidisciple@proton.me>
Co-authored-by: Matt Hill <MattDHill@users.noreply.github.com>
Co-authored-by: Alex Inkin <alexander@inkin.ru>
Co-authored-by: Remco Ros <remcoros@live.nl>
2025-12-15 13:30:50 -07:00
Mariusz Kogen
b945243d1a refactor(tor-check): improve proxy support, error handling ... (#3072)
refactor(tor-check): improve proxy support, error handling, and output formatting
2025-12-15 18:14:46 +01:00
Aiden McClelland
d8484a8b26 add docker build for start-registry (#3067)
* add docker build for start-registry

* login to docker

* add workflow dependency

* fix path

* fix add

* fix gh actions permissions

* use apt-get
2025-12-05 18:12:09 -07:00
Matt Hill
3c27499795 Refactor/status info (#3066)
* refactor status info

* wip fe

* frontend changes and version bump

* fix tests and motd

* add registry workflow

* better starttunnel instructions

* placeholders for starttunnel tables

---------

Co-authored-by: Aiden McClelland <me@drbonez.dev>
2025-12-02 23:31:02 +00:00
Remco Ros
7c772e873d fix: pass --allow-discards to luksOpen for trim support (#3064) 2025-12-02 22:00:10 +00:00
Matt Hill
db2fab245e better language and show wg config on device save (#3065)
* better language and show wg config on device save

* chore: fix

---------

Co-authored-by: waterplea <alexander@inkin.ru>
2025-12-02 21:42:14 +00:00
Matt Hill
a9c9917f1a better ST instructions 2025-12-01 18:03:07 -07:00
Matt Hill
23e2e9e9cc Update START-TUNNEL.md 2025-11-30 16:32:40 -07:00
Aiden McClelland
2369e92460 Update download link for StartTunnel installation 2025-11-28 14:28:54 -07:00
Aiden McClelland
a53b15f2a3 improve StartTunnel validation and GC (#3062)
* improve StartTunnel validation and GC

* update sdk formatting
2025-11-28 13:14:52 -07:00
Aiden McClelland
72eb8b1eb6 Update START-TUNNEL.md 2025-11-27 08:57:38 -07:00
Aiden McClelland
4db54f3b83 Update START-TUNNEL.md 2025-11-27 08:56:05 -07:00
Aiden McClelland
24eb27f005 minor bugfixes for alpha.14 (#3058)
* overwrite AllowedIPs in wg config
mute UnknownCA errors

* fix upgrade issues

* allow start9 user to access journal

* alpha.15

* sort actions lexicographically and show desc in marketplace details

* add registry package download cli command

---------

Co-authored-by: Matt Hill <mattnine@protonmail.com>
2025-11-26 16:23:08 -07:00
Matt Hill
009d76ea35 More FE fixes (#3056)
* tell user to restart server after kiosk chnage

* remove unused import

* dont show tor address on server setup

* chore: address comments

* revert mock

* chore: remove uptime block on mobile

* utiliser le futur proche

* chore: comments

* don't show loading on authorities tab

* chore: fix mobile unions

---------

Co-authored-by: waterplea <alexander@inkin.ru>
Co-authored-by: Aiden McClelland <3732071+dr-bonez@users.noreply.github.com>
2025-11-25 23:43:19 +00:00
Aiden McClelland
6e8a425eb1 overwrite AllowedIPs in wg config (#3055)
mute UnknownCA errors
2025-11-21 11:30:21 -07:00
Aiden McClelland
66188d791b fix start-tunnel artifact upload 2025-11-20 10:53:23 -07:00
Aiden McClelland
015ff02d71 fix build 2025-11-20 01:05:50 -07:00
Aiden McClelland
10bfaf5415 fix start-tunnel build 2025-11-20 00:30:33 -07:00
Aiden McClelland
e3e0b85e0c Bugfix/alpha.13 (#3053)
* bugfixes for alpha.13

* minor fixes

* version bump

* start-tunnel workflow

* sdk beta 44

* defaultFilter

* fix reset-password on tunnel auth

* explicitly rebuild types

* fix typo

* ubuntu-latest runner

* add cleanup steps

* fix env on attach
2025-11-19 22:48:49 -07:00
Matt Hill
ad0632892e Various (#3051)
* tell user to restart server after kiosk chnage

* remove unused import

* dont show tor address on server setup

* chore: address comments

* revert mock

* chore: remove uptime block on mobile

* utiliser le futur proche

---------

Co-authored-by: waterplea <alexander@inkin.ru>
Co-authored-by: Aiden McClelland <3732071+dr-bonez@users.noreply.github.com>
2025-11-19 10:35:07 -07:00
Aiden McClelland
f26791ba39 fix raspi fsck 2025-11-17 12:17:58 -07:00
Aiden McClelland
2fbaaebf44 Bugfixes for alpha.12 (#3049)
* squashfs-wip

* sdk fixes

* misc fixes

* bump sdk

* Include StartTunnel installation command

Added installation instructions for StartTunnel.

* CA instead of leaf for StartTunnel (#3046)

* updated docs for CA instead of cert

* generate ca instead of self-signed in start-tunnel

* Fix formatting in START-TUNNEL.md installation instructions

* Fix formatting in START-TUNNEL.md

* fix infinite loop

* add success message to install

* hide loopback and bridge gateways

---------

Co-authored-by: Aiden McClelland <me@drbonez.dev>
Co-authored-by: Aiden McClelland <3732071+dr-bonez@users.noreply.github.com>

* prevent gateways from getting stuck empty

* fix set-password

* misc networking fixes

* build and efi fixes

* efi fixes

* alpha.13

* remove cross

* fix tests

* provide path to upgrade

* fix networkmanager issues

* remove squashfs before creating

---------

Co-authored-by: Matt Hill <MattDHill@users.noreply.github.com>
2025-11-15 22:33:03 -07:00
StuPleb
edb916338c minor typos and grammar (#3047)
* minor typos and grammar

* added missing word - compute
2025-11-14 12:48:41 -07:00
Aiden McClelland
f7e947d37d Fix installation command for StartTunnel (#3048) 2025-11-14 12:42:05 -07:00
Aiden McClelland
a9e3d1ed75 Revise StartTunnel installation and update commands
Updated installation and update instructions for StartTunnel.
2025-11-14 12:39:53 -07:00
Matt Hill
ce97827c42 CA instead of leaf for StartTunnel (#3046)
* updated docs for CA instead of cert

* generate ca instead of self-signed in start-tunnel

* Fix formatting in START-TUNNEL.md installation instructions

* Fix formatting in START-TUNNEL.md

* fix infinite loop

* add success message to install

* hide loopback and bridge gateways

---------

Co-authored-by: Aiden McClelland <me@drbonez.dev>
Co-authored-by: Aiden McClelland <3732071+dr-bonez@users.noreply.github.com>
2025-11-10 14:15:58 -07:00
Aiden McClelland
3efec07338 Include StartTunnel installation command
Added installation instructions for StartTunnel.
2025-11-07 20:00:31 +00:00
Aiden McClelland
68f401bfa3 Feature/start tunnel (#3037)
* fix live-build resolv.conf

* improved debuggability

* wip: start-tunnel

* fixes for trixie and tor

* non-free-firmware on trixie

* wip

* web server WIP

* wip: tls refactor

* FE patchdb, mocks, and most endpoints

* fix editing records and patch mocks

* refactor complete

* finish api

* build and formatter update

* minor change toi viewing addresses and fix build

* fixes

* more providers

* endpoint for getting config

* fix tests

* api fixes

* wip: separate port forward controller into parts

* simplify iptables rules

* bump sdk

* misc fixes

* predict next subnet and ip, use wan ips, and form validation

* refactor: break big components apart and address todos (#3043)

* refactor: break big components apart and address todos

* starttunnel readme, fix pf mocks, fix adding tor domain in startos

---------

Co-authored-by: Matt Hill <mattnine@protonmail.com>

* better tui

* tui tweaks

* fix: address comments

* better regex for subnet

* fixes

* better validation

* handle rpc errors

* build fixes

* fix: address comments (#3044)

* fix: address comments

* fix unread notification mocks

* fix row click for notification

---------

Co-authored-by: Matt Hill <mattnine@protonmail.com>

* fix raspi build

* fix build

* fix build

* fix build

* fix build

* try to fix build

* fix tests

* fix tests

* fix rsync tests

* delete useless effectful test

---------

Co-authored-by: Matt Hill <mattnine@protonmail.com>
Co-authored-by: Alex Inkin <alexander@inkin.ru>
2025-11-07 10:12:05 +00:00
Matt Hill
1ea525feaa make textarea rows configurable (#3042)
* make textarea rows configurable

* add comments

* better defaults
2025-10-31 11:46:49 -06:00
Alex Inkin
57c4a7527e fix: make CPU meter not go to 11 (#3038) 2025-10-30 16:13:39 -06:00
Aiden McClelland
5aa9c045e1 fix live-build resolv.conf (#3035)
* fix live-build resolv.conf

* improved debuggability
2025-09-24 22:44:25 -06:00
Matt Hill
6f1900f3bb limit adding gateway to StartTunnel, better copy around Tor SSL (#3033)
* limit adding gateway to StartTunnel, better copy around Tor SSL

* properly differentiate ssl

* exclude disconnected gateways

* better error handling

---------

Co-authored-by: Aiden McClelland <me@drbonez.dev>
2025-09-24 13:22:26 -06:00
Aiden McClelland
bc62de795e bugfixes for alpha.10 (#3032)
* bugfixes for alpha.10

* bump raspi kernel

* rpi kernel bump

* alpha.11
2025-09-23 22:42:17 +00:00
Alex Inkin
c62ca4b183 fix: make long dropdown options wrap (#3031) 2025-09-21 06:06:33 -06:00
Alex Inkin
876e5bc683 fix: fix overflowing interface table (#3027) 2025-09-20 07:10:58 -06:00
Alex Inkin
b99f3b73cd fix: make logs page take up all space (#3030) 2025-09-20 07:10:28 -06:00
Matt Hill
7eecf29449 fix dep error display, show starting if any health check starting, show disabled health check message, remove loader from service list, animated dots, better color (#3025)
* refector addresses to not need gateways array

* fix dep error display, show starting if any health check starting, show disabled health check message, remove loader from service list, animated dots, better color

* fix: fix action results textfields

---------

Co-authored-by: waterplea <alexander@inkin.ru>
2025-09-17 10:32:20 -06:00
Mariusz Kogen
1d331d7810 Fix file permissions for developer key and auth cookie (#3024)
* fix permissions

* include read for group
2025-09-16 09:09:33 -06:00
Aiden McClelland
68414678d8 sdk updates; beta.39 (#3022)
* sdk updates; beta.39

* beta.40
2025-09-11 15:47:48 -06:00
Aiden McClelland
2f6b9dac26 Bugfix/dns recursion (#3023)
* fix dns recursion and localhost

* additional fix
2025-09-11 15:47:38 -06:00
Aiden McClelland
d1812d875b fix dns recursion and localhost (#3021) 2025-09-11 12:35:12 -06:00
Aiden McClelland
723dea100f add more gateway info to hostnameInfo (#3019) 2025-09-10 12:16:35 -06:00
Matt Hill
c4419ed31f show correct gateway name when adding public domain 2025-09-10 09:57:39 -06:00
Matt Hill
754ab86e51 only show http for tor if protocol is http 2025-09-10 09:36:03 -06:00
Mariusz Kogen
04dab532cd Motd Redesign - Visual and Structural Upgrade (#3018)
New 040 motd
2025-09-10 06:36:27 +00:00
Matt Hill
add01ebc68 Gateways, domains, and new service interface (#3001)
* add support for inbound proxies

* backend changes

* fix file type

* proxy -> tunnel, implement backend apis

* wip start-tunneld

* add domains and gateways, remove routers, fix docs links

* dont show hidden actions

* show and test dns

* edit instead of chnage acme and change gateway

* refactor: domains page

* refactor: gateways page

* domains and acme refactor

* certificate authorities

* refactor public/private gateways

* fix fe types

* domains mostly finished

* refactor: add file control to form service

* add ip util to sdk

* domains api + migration

* start service interface page, WIP

* different options for clearnet domains

* refactor: styles for interfaces page

* minor

* better placeholder for no addresses

* start sorting addresses

* best address logic

* comments

* fix unnecessary export

* MVP of service interface page

* domains preferred

* fix: address comments

* only translations left

* wip: start-tunnel & fix build

* forms for adding domain, rework things based on new ideas

* fix: dns testing

* public domain, max width, descriptions for dns

* nix StartOS domains, implement public and private domains at interface scope

* restart tor instead of reset

* better icon for restart tor

* dns

* fix sort functions for public and private domains

* with todos

* update types

* clean up tech debt, bump dependencies

* revert to ts-rs v9

* fix all types

* fix dns form

* add missing translations

* it builds

* fix: comments (#3009)

* fix: comments

* undo default

---------

Co-authored-by: Matt Hill <mattnine@protonmail.com>

* fix: refactor legacy components (#3010)

* fix: comments

* fix: refactor legacy components

* remove default again

---------

Co-authored-by: Matt Hill <mattnine@protonmail.com>

* more translations

* wip

* fix deadlock

* coukd work

* simple renaming

* placeholder for empty service interfaces table

* honor hidden form values

* remove logs

* reason instead of description

* fix dns

* misc fixes

* implement toggling gateways for service interface

* fix showing dns records

* move status column in service list

* remove unnecessary truthy check

* refactor: refactor forms components and remove legacy Taiga UI package (#3012)

* handle wh file uploads

* wip: debugging tor

* socks5 proxy working

* refactor: fix multiple comments (#3013)

* refactor: fix multiple comments

* styling changes, add documentation to sidebar

* translations for dns page

* refactor: subtle colors

* rearrange service page

---------

Co-authored-by: Matt Hill <mattnine@protonmail.com>

* fix file_stream and remove non-terminating test

* clean  up logs

* support for sccache

* fix gha sccache

* more marketplace translations

* install wizard clarity

* stub hostnameInfo in migration

* fix address info after setup, fix styling on SI page, new 040 release notes

* remove tor logs from os

* misc fixes

* reset tor still not functioning...

* update ts

* minor styling and wording

* chore: some fixes (#3015)

* fix gateway renames

* different handling for public domains

* styling fixes

* whole navbar should not be clickable on service show page

* timeout getState request

* remove links from changelog

* misc fixes from pairing

* use custom name for gateway in more places

* fix dns parsing

* closes #3003

* closes #2999

* chore: some fixes (#3017)

* small copy change

* revert hardcoded error for testing

* dont require port forward if gateway is public

* use old wan ip when not available

* fix .const hanging on undefined

* fix test

* fix doc test

* fix renames

* update deps

* allow specifying dependency metadata directly

* temporarily make dependencies not cliackable in marketplace listings

* fix socks bind

* fix test

---------

Co-authored-by: Aiden McClelland <me@drbonez.dev>
Co-authored-by: waterplea <alexander@inkin.ru>
2025-09-10 03:43:51 +00:00
Mariusz Kogen
1cc9a1a30b build(cli): harden build-cli.sh (zig check, env defaults, GIT_HASH) (#3016) 2025-09-09 14:18:52 -06:00
Dominion5254
92a1de7500 remove entire service package directory on hard uninstall (#3007)
* remove entire service package directory on hard uninstall

* fix package path
2025-08-12 15:46:01 -06:00
Alex Inkin
a6fedcff80 fix: extract correct manifest in updating state (#3004) 2025-08-01 22:51:38 -06:00
Alex Inkin
55eb999305 fix: update notifications design (#3000) 2025-07-29 22:17:59 -06:00
Aiden McClelland
377b7b12ce update/alpha.9 (#2988)
* import marketplac preview for sideload

* fix: improve state service (#2977)

* fix: fix sideload DI

* fix: update Angular

* fix: cleanup

* fix: fix version selection

* Bump node version to fix build for Angular

* misc fixes
- update node to v22
- fix chroot-and-upgrade access to prune-images
- don't self-migrate legacy packages
- #2985
- move dataVersion to volume folder
- remove "instructions.md" from s9pk
- add "docsUrl" to manifest

* version bump

* include flavor when clicking view listing from updates tab

* closes #2980

* fix: fix select button

* bring back ssh keys

* fix: drop 'portal' from all routes

* fix: implement longtap action to select table rows

* fix description for ssh page

* replace instructions with docsLink and refactor marketplace preview

* delete unused translations

* fix patchdb diffing algorithm

* continue refactor of marketplace lib show components

* Booting StartOS instead of Setting up your server on init

* misc fixes
- closes #2990
- closes #2987

* fix build

* docsUrl and clickable service headers

* don't cleanup after update until new service install succeeds

* update types

* misc fixes

* beta.35

* sdkversion, githash for sideload, correct logs for init, startos pubkey display

* bring back reboot button on install

* misc fixes

* beta.36

* better handling of setup and init for websocket errors

* reopen init and setup logs even on graceful closure

* better logging, misc fixes

* fix build

* dont let package stats hang

* dont show docsurl in marketplace if no docsurl

* re-add needs-config

* show error if init fails, shorten hover state on header icons

* fix operator precedemce

---------

Co-authored-by: Matt Hill <mattnine@protonmail.com>
Co-authored-by: Alex Inkin <alexander@inkin.ru>
Co-authored-by: Mariusz Kogen <k0gen@pm.me>
2025-07-18 18:31:12 +00:00
gStart9
ba2906a42e Fallback DNS/NTP server privacy enhancements (#2992) 2025-07-16 21:31:52 +00:00
gStart9
ee27f14be0 Update/kiosk mode firefox settings (#2973)
* Privacy settings for firefox's kiosk mode

* Re-add extensions.update.enabled, false

* Don't enable letterboxing by default
2025-07-15 20:17:52 +00:00
Aiden McClelland
46c8be63a7 0.4.0-alpha.8 (#2975) 2025-07-08 12:28:21 -06:00
Matt Hill
7ba66c419a Misc frontend fixes (#2974)
* fix dependency input warning and extra comma

* clean up buttons during install in marketplace preview

* chore: grayscale and closing action-bar

* fix prerelease precedence

* fix duplicate url for addSsl on ssl proto

* no warning for soft uninstall

* fix: stop logs from repeating disconnected status and add 1 second delay between reconnection attempts

* fix stop on reactivation of critical task

* fix: fix disconnected toast

* fix: updates styles

* fix: updates styles

* misc fixes

* beta.33

* fix updates badge and initialization of marketplace preview controls

---------

Co-authored-by: waterplea <alexander@inkin.ru>
Co-authored-by: Aiden McClelland <me@drbonez.dev>
2025-07-08 12:08:27 -06:00
Aiden McClelland
340775a593 Feature/more dynamic unions (#2972)
* with validators

* more dynamic unions

* fixes from v31

* better constructor for dynamic unions

* version bump

* fix build
2025-07-01 17:40:39 -06:00
Alex Inkin
35d2ec8a44 chore: fix font in Safari (#2970) 2025-06-25 09:55:50 -04:00
Alex Inkin
2983b9950f chore: fix issues from dev channel (#2968) 2025-06-25 09:30:28 -04:00
Aiden McClelland
dbf08a6cf8 stop service if critical task activated (#2966)
filter out union lists instead of erroring
2025-06-18 22:03:27 +00:00
Alex Inkin
28f31be36f Fix/fe 6 (#2965)
* fix backup reports modal

* chore: fix comments

* chore: fix controls status

* chore: fix stale marketplace data

* slightly better registry switching

---------

Co-authored-by: Matt Hill <mattnine@protonmail.com>
2025-06-18 10:40:39 -06:00
Aiden McClelland
3ec4db0225 addHealthCheck instead of additionalHealthChecks for Daemons (#2962)
* addHealthCheck on Daemons

* fix bug that prevents domains without protocols from being deleted

* fixes from testing

* version bump

* add sdk version to UI

* fix useEntrypoint

* fix dependency health check error display

* minor fixes

* beta.29

* fixes from testing

* beta.30

* set /etc/os-release (#2918)

* remove check-monitor from kiosk (#2059)

* add units for progress (#2693)

* use new progress type

* alpha.7

* fix up pwa stuff

* fix wormhole-squashfs and prune boot (#2964)

* don't exit on expected errors

* use bash

---------

Co-authored-by: Matt Hill <mattnine@protonmail.com>
2025-06-17 17:50:01 -06:00
Matt Hill
f5688e077a misc fixes (#2961)
* fix backup reports modal

* chore: fix comments

---------

Co-authored-by: waterplea <alexander@inkin.ru>
2025-06-17 11:22:32 -06:00
Aiden McClelland
2464d255d5 improve daemons init system (#2960)
* repeatable command launch fn

* allow js fn for daemon exec

* improve daemon init system

* fixes from testing
2025-06-06 14:35:03 -06:00
Aiden McClelland
586d950b8c update cargo deps (#2959)
* update cargo deps

* readd device info header
2025-06-05 17:17:02 -06:00
Matt Hill
e7469388cc minor fixes (#2957)
* minor fixes

* more minor touchups

* minor fix

* fix release notes display
2025-06-05 17:02:54 -06:00
Dominion5254
ab6ca8e16a Bugfix/ssl proxy to ssl (#2956)
* fix registry rm command

* fix bind with addSsl on ssl proto

* fix bind with addSsl on ssl proto

* Add pre-release version migrations

* fix os build

* add mime to package deps

* update lockfile

* more ssl fixes

* add waitFor

* improve restart lockup

* beta.26

* fix dependency health check logic

* handle missing health check

* fix port forwards

---------

Co-authored-by: Aiden McClelland <me@drbonez.dev>
2025-06-04 19:41:21 -06:00
Alex Inkin
02413a4fac Update Angular (#2952)
* fix Tor logs actually fetching od logs

* chore: update to Angular 18

* chore: update to Angular 19

* bump patchDB

* chore: update Angular

* chore: fix setup-wizard success page

* chore: fix

* chore: fix

* chore: fix

* chore: fix

---------

Co-authored-by: Matt Hill <mattnine@protonmail.com>
Co-authored-by: Aiden McClelland <me@drbonez.dev>
2025-05-30 10:34:24 -04:00
Dominion5254
05b8dd9ad8 fix registry rm command (#2955) 2025-05-27 19:00:29 -06:00
Dominion5254
29c9419a6e add nfs-common (#2954) 2025-05-27 16:11:34 -06:00
Aiden McClelland
90e61989a4 misc bugfixes for alpha.4 (#2953)
* fix lockup when stop during init

* Fix incorrect description for registry package remove command

* alpha.5

* beta.25

---------

Co-authored-by: Mariusz Kogen <k0gen@pm.me>
2025-05-23 11:23:29 -06:00
Matt Hill
b1f9f90fec Frontend fixes/improvements (#2950)
* fix Tor logs actually fetching od logs

* chore: switch from `mime-types` to `mime` for browser environment support (#2951)

* change V2 s9pk title to Legacy

* show warning for domains when not public, disable launch too

---------

Co-authored-by: Alex Inkin <alexander@inkin.ru>
Co-authored-by: Mariusz Kogen <k0gen@pm.me>
2025-05-23 10:45:06 -06:00
Matt Hill
b40849f672 Fix/fe bugs 3 (#2943)
* fix typeo in patch db seed

* show all registries in updates tab, fix required dependnecy display in marketplace, update browser tab title desc

* always show pointer for version select

* chore: fix comments

* support html in action desc and marketplace long desc, only show qr in action res if qr is true

* disable save if smtp creds not edited, show better smtp success message

* dont dismiss login spinner until patchDB returns

* feat: redesign of service dashboard and interface (#2946)

* feat: redesign of service dashboard and interface

* chore: comments

* re-add setup complete

* dibale launch UI when not running, re-style things, rename things

* back to 1000

* fix clearnet docs link and require password retype in setup wiz

* faster hint display

* display dependency ID if title not available

* fix migration

* better init progress view

* fix setup success page by providing VERSION and notifications page fixes

* force uninstall from service error page, soft or hard

* handle error state better

* chore: fixed for install and setup wizards

* chore: fix issues (#2949)

* enable and disable kiosk mode

* minor fixes

* fix dependency mounts

* dismissable tasks

* provide replayId

* default if health check success message is null

* look for wifi interface too

* dash for null user agent in sessions

* add disk repair to diagnostic api

---------

Co-authored-by: waterplea <alexander@inkin.ru>
Co-authored-by: Aiden McClelland <me@drbonez.dev>
2025-05-21 19:04:26 -06:00
Aiden McClelland
44560c8da8 Refactor/sdk init (#2947)
* fixes for main

* refactor package initialization

* fixes from testing

* more fixes

* beta.21

* do not use instanceof

* closes #2921

* beta22

* allow disabling kiosk

* migration

* fix /etc/shadow

* actionRequest -> task

* beta.23
2025-05-21 10:24:37 -06:00
Aiden McClelland
46fd01c264 0.4.0-alpha.4 (#2948) 2025-05-20 16:11:50 -06:00
nbxl21
100695c262 Add french translation (#2945)
* Add french translation

* Remove outdated instruction

* Fix missing instructions
2025-05-17 18:10:24 -06:00
H0mer
54b5a4ae55 Update pl.ts (#2944)
Collaboration with @k0gen
2025-05-14 08:07:56 -06:00
Aiden McClelland
ffb252962b hotfix migration 2025-05-12 06:38:48 -06:00
Aiden McClelland
ae31270e63 alpha3 (#2942) 2025-05-11 07:48:32 -06:00
Matt Hill
9b2b54d585 fix osUpdate check and address parser for Tor without protocol (#2941) 2025-05-10 12:18:20 -06:00
Aiden McClelland
e1ccc583a3 0.4.0-alpha.2 (#2940) 2025-05-09 16:34:29 -06:00
Aiden McClelland
7750e33f82 misc sdk changes (#2934)
* misc sdk changes

* delete the store ☠️

* port comments

* fix build

* fix removing

* fix tests

* beta.20

---------

Co-authored-by: Matt Hill <mattnine@protonmail.com>
2025-05-09 15:10:51 -06:00
Matt Hill
d2c4741f0b use fallback icon for missing dep 2025-05-09 15:07:49 -06:00
Matt Hill
c79c4f6bde remove http timeout for sideloading 2025-05-09 14:55:11 -06:00
Lucy
3849d0d1a9 Fix/controls (#2938)
* adjust copy to display package state

* use package id for service column in notifications table

* fixes

* less translations, fix notificaiton item, remove unnecessary conditional

* tidy up

* forgot spanish

---------

Co-authored-by: Matt Hill <mattnine@protonmail.com>
2025-05-09 14:53:46 -06:00
Alex Inkin
8bd71ccd5e fix: welcome page on mobile (#2936) 2025-05-09 14:49:19 -06:00
Matt Hill
b731f7fb64 handle removing and backing up state, fix ackInstructions too (#2935) 2025-05-09 14:48:40 -06:00
Matt Hill
cd554f77f3 unbang the test bang 2025-05-09 11:06:53 -06:00
Matt Hill
8c977c51ca frontend fixes for alpha.2 (#2919)
* rmeove icon from toggles

* fix: comments

* fix: more comments

* always show public domains even if interface private, only show delete on domains

* fix: even more comments

* fix: last comments

* feat: empty state for dashboard

* rework welcome, dlete update-toast, minor

* translation improvements

---------

Co-authored-by: waterplea <alexander@inkin.ru>
2025-05-09 10:29:17 -06:00
Aiden McClelland
a3252f9671 allow mounting files directly (#2931)
* allow mounting files directly

* fixes from testing

* more fixes
2025-05-07 12:47:45 -06:00
Aiden McClelland
9bc945f76f upcast v0 action results to v1 (#2930) 2025-05-07 09:33:26 -06:00
Aiden McClelland
f6b4dfffb6 fix shell support in package attach (#2929)
* fix package attach

* final fixes

* apply changes to launch

* Update core/startos/src/service/effects/subcontainer/sync.rs
2025-05-07 09:19:38 -06:00
Aiden McClelland
68955c29cb add transformers to file helpers (#2922)
* fix undefined handling in INI

* beta.14

* Partial -> DeepPartial in action request

* boolean laziness kills

* beta.16

* misc fixes

* file transformers

* infer validator source argument

* simplify validator

* readd toml

* beta.17

* filter undefined instead of parse/stringify

* handle arrays of objects in filterUndefined
2025-05-06 11:04:11 -06:00
Matt Hill
97e4d036dc fix adding new registry 2025-05-03 20:35:13 -06:00
Matt Hill
0f49f54c29 dont show indeterminate progress when waiting for phase 2025-05-01 18:54:14 -06:00
Aiden McClelland
828e13adbb add support for "oneshot" daemons (#2917)
* add support for "oneshot" daemons

* add docs for oneshot

* add support for runAsInit in daemon.of

* beta.13
2025-05-01 16:00:35 -06:00
Matt Hill
e6f0067728 rework installing page and add cancel install button (#2915)
* rework installing page and add cancel install button

* actually call cancel endpoint

* fix two bugs

* include translations in progress component

* cancellable installs

* fix: comments (#2916)

* fix: comments

* delete comments

* ensure trailing slash and no qp for new registry url

---------

Co-authored-by: Matt Hill <mattnine@protonmail.com>

* fix raspi

* bump sdk

---------

Co-authored-by: Aiden McClelland <me@drbonez.dev>
Co-authored-by: Alex Inkin <alexander@inkin.ru>
2025-04-30 13:50:08 -06:00
Lucy
5c473eb9cc update marketplace url to reflect build version (#2914)
* update marketplace url to reflect build version

* adjust marketplace config

* use helper function to compare urls

* rework some registry stuff

* #2900, #2899, and other registry changes

* alpha.1

* trailing /

* add startosRegistry

* fix migration

---------

Co-authored-by: Matt Hill <mattnine@protonmail.com>
Co-authored-by: Aiden McClelland <me@drbonez.dev>
Co-authored-by: Aiden McClelland <3732071+dr-bonez@users.noreply.github.com>
2025-04-29 14:12:21 -06:00
Aiden McClelland
2adf34fbaf misc fixes (#2892)
* use docker for build steps that require linux when not on linux

* use fuse for overlay

* quiet mountpoint

* node 22

* misc fixes

* make shasum more compliant

* optimize download-base-image.sh with cleaner url handling and checksum verification

* fix script

* fixes #2900

* bump node and npm versions in web readme

* Minor pl.ts fixes

* fixes in response to synapse issues

* beta.8

* update ts-matches

* beta.11

* pl.ts finetuning

---------

Co-authored-by: Mariusz Kogen <k0gen@pm.me>
Co-authored-by: Matt Hill <mattnine@protonmail.com>
2025-04-28 17:33:41 -06:00
Matt Hill
05dd760388 Fix links for docs (#2908)
* fix docs paths

* docsLink directive

* fix: bugs (#2909)

---------

Co-authored-by: Alex Inkin <alexander@inkin.ru>
2025-04-24 14:14:08 -06:00
Mariusz Kogen
2cf4864078 Polish language refactor (#2887)
* Refactored Polish Translation

* even more language fixes and no comments
2025-04-23 10:47:43 -06:00
Lucy
df4c92672f Fix/os update version (#2890)
* dynamically set a registry to use for os updates

* fix os updates response type

* fix saving high score

---------

Co-authored-by: Matt Hill <mattnine@protonmail.com>
2025-04-23 07:17:46 -06:00
Alex Inkin
5b173315f9 fix: store language properly (#2891) 2025-04-23 07:17:18 -06:00
Aiden McClelland
c85ea7d8fa sdk beta.6 (#2885)
beta.6
2025-04-22 12:00:34 -06:00
Alex Inkin
113154702f fix: fix logs overflow (#2888) 2025-04-22 08:58:20 -06:00
Lucy
33ae46f76a add proxima nova font (#2883)
* add proxima nova font

* update to woff files

* clean up defaults
2025-04-21 11:30:38 -06:00
Matt Hill
27272680a2 Bugfix/040 UI (#2881)
* fix sideload and install flow

* move updates chevron inside upddate button

* update dictionaries to include langauge names

* fix: address todos (#2880)

* fix: address todos

* fix enlgish translation

---------

Co-authored-by: Matt Hill <mattnine@protonmail.com>

* use existing translation, no need to duplicate

* fix: update dialog and other fixes (#2882)

---------

Co-authored-by: Alex Inkin <alexander@inkin.ru>
Co-authored-by: Aiden McClelland <me@drbonez.dev>
2025-04-21 10:57:12 -06:00
Matt Hill
b1621f6b34 Copy changes for 040 release (#2874)
* update 040 changelog

* remove post_up from 036-alpha6

* backend copy updates

* beta.4

* beta.5

* fix spelling

---------

Co-authored-by: Aiden McClelland <me@drbonez.dev>
2025-04-21 09:43:35 -06:00
Aiden McClelland
2c65033c0a Feature/sdk improvements (#2879)
* sdk improvements

* subcontainer fixes, disable wifi on migration if not in use, filterable interfaces
2025-04-18 14:11:13 -06:00
Matt Hill
dcfbaa9243 fix start bug in service dashboard 2025-04-17 21:29:08 -06:00
Matt Hill
accef65ede bug fixes (#2878)
* bug fixes, wip

* revert tranlstion
2025-04-17 21:06:50 -06:00
Alex Inkin
50755d8ba3 Refactor i18n approach (#2875)
* Refactor i18n approach

* chore: move to shared

* chore: add default

* create DialogService and update LoadingService (#2876)

* complete translation infra for ui project, currently broken

* cleanup and more dictionaries

* chore: fix

---------

Co-authored-by: Matt Hill <MattDHill@users.noreply.github.com>
Co-authored-by: Matt Hill <mattnine@protonmail.com>
2025-04-17 09:00:59 -06:00
Aiden McClelland
47b6509f70 sdk improvements (#2877) 2025-04-16 12:53:10 -06:00
Aiden McClelland
89f3fdc05f reduce task leaking (#2868)
* reduce task leaking

* fix onLeaveContext
2025-04-16 11:00:46 -06:00
Matt Hill
03f8b73627 minor web cleanup chores 2025-04-13 13:03:51 -06:00
Matt Hill
2e6e9635c3 fix a few, more to go (#2869)
* fix a few, more to go

* chore: comments (#2871)

* chore: comments

* chore: typo

* chore: stricter typescript (#2872)

* chore: comments

* chore: stricter typescript

---------

Co-authored-by: Matt Hill <MattDHill@users.noreply.github.com>

* minor styling

---------

Co-authored-by: Alex Inkin <alexander@inkin.ru>
2025-04-12 09:53:03 -06:00
Aiden McClelland
6a312e3fdd Merge branch 'next/minor' of github.com:Start9Labs/start-os into next/major 2025-04-11 13:03:55 -06:00
Aiden McClelland
0e8961efe3 Merge branch 'master' into next/major 2025-04-10 15:50:29 -06:00
Matt Hill
fc2be42418 sideload wip, websockets, styling, multiple todos (#2865)
* sideload wip, websockets, styling, multiple todos

* sideload

* misc backend updates

* chore: comments

* prep for license and instructions display

* comment for Matt

* s9pk updates and 040 sdk

* fix dependency error for actions

* 0.4.0-beta.1

* beta.2

---------

Co-authored-by: Aiden McClelland <me@drbonez.dev>
Co-authored-by: waterplea <alexander@inkin.ru>
Co-authored-by: Aiden McClelland <3732071+dr-bonez@users.noreply.github.com>
2025-04-10 19:51:05 +00:00
Aiden McClelland
ab4336cfd7 Merge branch 'next/minor' of github.com:Start9Labs/start-os into next/major 2025-04-07 14:00:42 -06:00
w3irdrobot
63a29d3a4a update github workflow actions to most recent versions (#2852) 2025-04-07 19:53:23 +00:00
Alex Inkin
31856d9895 chore: comments (#2863) 2025-04-06 07:18:01 -06:00
Alex Inkin
f51dcf23d6 feat: refactor metrics (#2861) 2025-03-31 14:22:54 -06:00
Alex Inkin
1883c9666e feat: refactor updates (#2860) 2025-03-29 12:50:23 -06:00
Matt Hill
4b4cf76641 remove ssh, deprecate wifi (#2859) 2025-03-28 14:38:49 -06:00
Alex Inkin
495bbecc01 feat: refactor logs (#2856)
* feat: refactor logs

* download root ca, minor transaltions, better interfaces buttons

* chore: comments

---------

Co-authored-by: Matt Hill <mattnine@protonmail.com>
2025-03-28 07:02:06 -06:00
Alex Inkin
e6af7e9885 feat: finalize desktop and mobile design of system routes (#2855)
* feat: finalize desktop and mobile design of system routes

* clean up messaging and mobile tabbar utilities

---------

Co-authored-by: Matt Hill <mattnine@protonmail.com>
2025-03-27 06:41:47 -06:00
w3irdrobot
182b8c2283 update time dependency in core (#2850) 2025-03-25 16:49:50 +00:00
Alex Inkin
5318cccc5f feat: add i18n infrastructure (#2854)
* feat: add i18n infrastructure

* store langauge selection to patchDB ui section

* feat: react to patchdb language change

---------

Co-authored-by: Matt Hill <mattnine@protonmail.com>
2025-03-25 05:30:35 -06:00
Alex Inkin
99739575d4 chore: refactor system settings routes (#2853)
* chore: refactor system settings routes

* switch mock to null to see backup page

---------

Co-authored-by: Matt Hill <mattnine@protonmail.com>
2025-03-23 11:03:41 -06:00
Aiden McClelland
6f9069a4fb Merge branch 'next/minor' of github.com:Start9Labs/start-os into next/major 2025-03-17 14:00:49 -06:00
Alex Inkin
a18ab7f1e9 chore: refactor interfaces (#2849)
* chore: refactor interfaces

* chore: fix uptime
2025-03-17 12:07:52 -06:00
Alex Inkin
be0371fb11 chore: refactor settings (#2846)
* small type changes and clear todos

* handle notifications and metrics

* wip

* fixes

* migration

* dedup all urls

* better handling of clearnet ips

* add rfkill dependency

* chore: refactor settings

---------

Co-authored-by: Matt Hill <mattnine@protonmail.com>
Co-authored-by: Aiden McClelland <me@drbonez.dev>
2025-03-10 13:09:08 -06:00
Matt Hill
fa3329abf2 remove duplicate dir with pages 2025-03-07 13:18:08 -07:00
Aiden McClelland
e830fade06 Update/040 types (#2845)
* small type changes and clear todos

* handle notifications and metrics

* wip

* fixes

* migration

* dedup all urls

* better handling of clearnet ips

* add rfkill dependency

---------

Co-authored-by: Matt Hill <mattnine@protonmail.com>
2025-03-06 20:36:19 -07:00
Alex Inkin
ac392dcb96 feat: more refactors (#2844) 2025-03-05 13:30:07 -07:00
Aiden McClelland
00a5fdf491 Merge branch 'next/minor' of github.com:Start9Labs/start-os into next/major 2025-03-03 12:51:40 -07:00
Alex Inkin
7fff9579c0 feat: redesign service route (#2835)
* feat: redesign service route

* chore: more changes

* remove automated backups and fix interface addresses

* fix rpc methods and slightly better mocks

---------

Co-authored-by: Matt Hill <mattnine@protonmail.com>
2025-02-25 08:33:35 -07:00
Alex Inkin
1b006599cf feat: add service uptime and start style changes (#2831) 2025-02-14 11:32:30 -07:00
Matt Hill
ce2842d365 fix patch db types and comment out future domain and proxy features 2025-02-11 21:17:20 -07:00
Matt Hill
7d1096dbd8 compiles 2025-02-10 22:41:29 -07:00
Matt Hill
95722802dc Merge branch 'next/minor' of github.com:Start9Labs/start-os into next/major 2025-02-08 19:19:35 -07:00
Alex Inkin
95cad7bdd9 fix: properly handle values in unions (#2826) 2025-02-08 17:36:32 -07:00
Alex Inkin
b2b98643d8 feat: better form array validation (#2822) 2025-01-27 17:46:29 -07:00
Alex Inkin
bb8109f67d fix: fix resetting config and other minor issues (#2819) 2025-01-24 11:12:57 -07:00
Alex Inkin
e6f02bf8f7 feat: hover state for navigation (#2807)
* feat: hover state for navigation

* chore: fix
2025-01-06 11:56:16 -07:00
Alex Inkin
57e75e3614 feat: implement top navigation (#2805)
* feat: implement top navigation

* chore: fix order
2024-12-30 09:07:44 -07:00
Alex Inkin
89ab67e067 fix: finish porting minor changes to major (#2799) 2024-12-11 16:16:46 -07:00
Matt Hill
115c599fd8 remove welcome component 2024-12-02 17:00:40 -07:00
Matt Hill
3121c08ee8 Merge branch 'next/minor' of github.com:Start9Labs/start-os into next/major 2024-12-02 16:59:03 -07:00
Matt Hill
a5bac39196 Merge branch 'next/minor' of github.com:Start9Labs/start-os into next/major 2024-12-02 16:50:37 -07:00
Alex Inkin
9f640b24b3 fix: fix building UI project (#2794)
* fix: fix building UI project

* fix makefile

* inputspec instead of config

---------

Co-authored-by: Matt Hill <mattnine@protonmail.com>
2024-12-02 16:44:27 -07:00
Matt Hill
75e7556bfa Merge branch 'next/minor' of github.com:Start9Labs/start-os into next/major 2024-11-25 19:02:07 -07:00
Alex Inkin
beb3a9f60a feat: make favicon react to theme (#2787) 2024-11-12 19:29:41 -07:00
Lucy
dfda2f7d5d Update Marketplace (#2742)
* update abstract marketplace for usage accuracy andrename store to registry

* use new abstract functions

* fix(marketplace): get rid of `AbstractMarketplaceService`

* bump shared marketplace lib

* update marketplace to use query params for registry url; comment out updates page - will be refactored

* cleanup

* cleanup duplicate

* cleanup unused imports

* rework setting registry url when loading marketplace

* cleanup marketplace service

* fix background

---------

Co-authored-by: Matt Hill <mattnine@protonmail.com>
Co-authored-by: waterplea <alexander@inkin.ru>
Co-authored-by: Matt Hill <MattDHill@users.noreply.github.com>
2024-10-09 11:23:08 -06:00
Matt Hill
a77ebd3b55 Merge pull request #2747 from Start9Labs/chore/remove-mastodon-from-readme
Update README.md
2024-09-26 11:04:19 -06:00
Aiden McClelland
00114287e5 Update README.md 2024-09-26 17:00:52 +00:00
Matt Hill
a9569d0ed9 Merge pull request #2740 from Start9Labs/update/new-registry
Update/new registry
2024-09-20 09:52:16 -06:00
Lucy Cifferello
88d9388be2 remote package lock from root 2024-09-20 11:51:19 -04:00
Lucy Cifferello
93c72ecea5 adjust buttons on marketplace show page and bump marketplace lib 2024-09-20 11:42:12 -04:00
Lucy Cifferello
b5b0ac50bd update shared and marketplace libs for taiga dep 2024-09-20 11:27:40 -04:00
Lucy Cifferello
4d2afdb1a9 misc cleanup and bump marketplace lib 2024-09-19 13:37:34 -04:00
Lucy Cifferello
39a177bd70 misc copy 2024-09-19 13:19:24 -04:00
Lucy Cifferello
34fb6ac837 add bitcoind dep for proxy in mocks 2024-09-19 13:19:14 -04:00
Lucy Cifferello
f868a454d9 move registry component to shared marketplace lib 2024-09-19 13:18:16 -04:00
Lucy Cifferello
751ceab04e fix icons and flavor filtering 2024-09-12 17:48:57 -04:00
Matt Hill
b6c48d0f98 Merge branch 'next/minor' of github.com:Start9Labs/start-os into next/major 2024-08-28 15:49:28 -06:00
Matt Hill
097d77f7b3 Merge pull request #2727 from Start9Labs/final-fixes
fix: final fixes
2024-08-28 15:38:44 -06:00
waterplea
7a0586684b fix: refresh edit target 2024-08-27 12:27:23 +04:00
waterplea
8f34d1c555 fix: final fixes 2024-08-27 12:17:05 +04:00
Matt Hill
5270a6781f Merge pull request #2719 from Start9Labs/flavor
fix: implement flavor across the app
2024-08-20 22:09:07 -06:00
waterplea
fa93e195cb fix: implement flavor across the app 2024-08-20 18:53:55 +04:00
Matt Hill
befa9eb16d Merge pull request #2714 from Start9Labs/sideload-and-backups
fix: implement back sideload and server selection in restoring
2024-08-18 07:52:02 -06:00
waterplea
a278c630bb fix: implement back sideload and server selection in restoring 2024-08-17 16:44:33 +04:00
Matt Hill
76eb0f1775 Merge pull request #2709 from Start9Labs/fix-build
fix: fix build after minor merged into major
2024-08-16 05:28:33 -06:00
waterplea
0abe08f243 chore: small changes 2024-08-16 14:39:54 +04:00
Matt Hill
015131f198 address comments and more 2024-08-15 08:05:37 -06:00
waterplea
a730543c76 fix: fix build after minor merged into major 2024-08-15 12:40:49 +04:00
Matt Hill
b43ad93c54 Merge pull request #2706 from Start9Labs/setup-wizard
fix: fix merge issues for setup-wizard project
2024-08-10 15:04:01 -06:00
waterplea
7850681ce1 chore: add back logs 2024-08-11 00:41:26 +04:00
Matt Hill
846189b15b address comments 2024-08-10 05:57:33 -06:00
waterplea
657aac0d68 fix: fix merge issues for setup-wizard project 2024-08-10 14:45:50 +04:00
Matt Hill
81932c8cff Merge branch 'next/minor' of github.com:Start9Labs/start-os into next/major 2024-08-08 10:52:49 -06:00
Matt Hill
20f6a5e797 Merge pull request #2687 from Start9Labs/taiga
chore: update taiga
2024-07-29 11:39:56 -06:00
waterplea
949f1c648a chore: update taiga 2024-07-29 18:18:00 +04:00
Matt Hill
d159dde2ca Merge pull request #2676 from Start9Labs/wifi
chore: improve wifi icons
2024-07-19 07:33:45 -06:00
waterplea
729a510c5b chore: improve wifi icons 2024-07-19 12:25:50 +05:00
Matt Hill
fffc7f4098 Merge pull request #2672 from Start9Labs/taiga-4
feat: update Taiga UI to 4 release candidate
2024-07-15 11:40:16 -06:00
waterplea
c7a2e7ada1 feat: update Taiga UI to 4 release candidate 2024-07-15 11:16:19 +05:00
Matt Hill
a2b1968d6e Merge pull request #2632 from Start9Labs/tables
feat: add mobile view for all the tables
2024-07-10 11:49:24 -06:00
waterplea
398eb13a7f chore: remove test 2024-07-10 16:01:57 +05:00
waterplea
956c8a8e03 chore: more comments 2024-07-10 10:35:21 +05:00
waterplea
6aba166c82 chore: address comments 2024-07-09 13:45:04 +05:00
waterplea
fd7c7ea6b7 chore: compact cards 2024-07-05 18:27:40 +05:00
waterplea
d85e621bb3 chore: address comments 2024-07-05 18:23:18 +05:00
Matt Hill
25801f374c Merge pull request #2638 from Start9Labs/update/marketplace-for-brochure
misc fixes and backwards compatibility with new registry types for brochure
2024-06-28 11:52:06 -06:00
Lucy Cifferello
8fd2d0b35c Merge remote-tracking branch 'origin' into update/marketplace-for-brochure 2024-06-26 16:29:15 -04:00
Lucy Cifferello
dd196c0e11 remove mismatched type for now 2024-06-25 18:00:56 -04:00
Lucy Cifferello
6e2cf8bb3f fix version sorting and icon display for marketplace 2024-06-24 17:49:57 -04:00
Lucy Cifferello
b8eb8a90a5 fix hero icon 2024-06-07 14:59:43 -04:00
Lucy Cifferello
bd4d89fc21 fixes and backwards compatability with new registry types 2024-06-06 16:57:51 -04:00
waterplea
6234391229 feat: add mobile view for all the tables
Signed-off-by: waterplea <alexander@inkin.ru>
2024-06-03 15:10:49 +05:00
Matt Hill
206c185a3b Merge pull request #2631 from Start9Labs/sections
chore: add sections
2024-05-30 06:29:02 -06:00
waterplea
7689cbbe0d chore: add sections
Signed-off-by: waterplea <alexander@inkin.ru>
2024-05-30 12:04:38 +01:00
Matt Hill
b57a9351b3 Merge pull request #2629 from Start9Labs/navigation
refactor: change navigation
2024-05-28 21:18:44 -06:00
waterplea
f0ae9e21ae refactor: change navigation
Signed-off-by: waterplea <alexander@inkin.ru>
2024-05-28 13:20:25 +01:00
Matt Hill
9510c92288 Merge pull request #2626 from Start9Labs/comments
chore: address comments
2024-05-21 15:04:13 -06:00
waterplea
755f3f05d8 chore: input-file on mobile 2024-05-21 10:29:29 +01:00
waterplea
5d8114b475 chore: address comments 2024-05-20 21:59:46 +01:00
Matt Hill
85b39ecf99 Merge pull request #2621 from Start9Labs/paths
chore: types imports
2024-05-16 07:03:56 -06:00
waterplea
230838c22b chore: types imports 2024-05-16 12:28:59 +01:00
Matt Hill
a7bfcdcb01 Merge pull request #2618 from Start9Labs/service
refactor: change service page to the new design
2024-05-14 10:01:27 -06:00
Matt Hill
47ff630c55 fix dependency errors and navigation 2024-05-14 07:02:21 -06:00
waterplea
70dc53bda7 chore: add backups info 2024-05-14 11:34:14 +01:00
waterplea
7e1b433c17 refactor: change service page to the new design 2024-05-12 16:53:01 +01:00
Matt Hill
ec878defab Merge pull request #2604 from Start9Labs/metrics
feat: implement metrics on the dashboard
2024-04-26 09:07:45 -06:00
waterplea
1786b70e14 chore: add todo 2024-04-26 18:06:19 +03:00
waterplea
7f525fa7dc chore: fix comments 2024-04-26 13:17:25 +03:00
waterplea
8b89e03999 feat: implement metrics on the dashboard 2024-04-18 13:45:35 +07:00
Matt Hill
2693b9a42d Merge pull request #2598 from Start9Labs/fix/preview-loader
fix preview loader
2024-04-09 20:03:43 -06:00
Matt Hill
6b336b7b2f Merge pull request #2591 from Start9Labs/ionic
refactor: completely remove ionic
2024-04-09 14:28:24 -06:00
Lucy Cifferello
3c0e77241d fix preview loader 2024-04-09 13:29:54 -04:00
Lucy Cifferello
87461c7f72 fix gap space 2024-04-09 12:42:28 -04:00
Lucy Cifferello
a67f2b4976 add back variables 2024-04-09 12:42:11 -04:00
waterplea
8594781780 refactor: completely remove ionic 2024-04-09 12:45:47 +07:00
Matt Hill
b2c8907635 Merge pull request #2590 from Start9Labs/update/marketplace-shared
update marketplace/shared to sync with next/major and brochure
2024-04-08 11:46:02 -06:00
Lucy Cifferello
05f4df1a30 fix changig registry 2024-04-08 10:33:02 -04:00
Lucy Cifferello
35fe06a892 fix registry connect styles and remove view complete listing link 2024-04-05 16:56:06 -04:00
Lucy Cifferello
cd933ce6e4 fix sideload display and other misc style adjustments 2024-04-05 14:14:23 -04:00
Lucy Cifferello
0b93988450 feedback fixes 2024-04-05 09:26:32 -04:00
Lucy Cifferello
12a323f691 fix dependency icon and layout 2024-04-03 12:35:27 -04:00
Lucy Cifferello
9c4c211233 fix preview versions, icons, and misc styling 2024-04-03 12:34:46 -04:00
Lucy Cifferello
74ba68ff2c fix scroll areas and side menu 2024-04-03 12:32:57 -04:00
Lucy Cifferello
7273b37c16 cleanup dep item for new type 2024-04-01 18:10:16 -04:00
Lucy Cifferello
0d4ebffc0e marketplace release fixes and refactors 2024-04-01 18:09:28 -04:00
Matt Hill
352b2fb4e7 Merge branch 'next/minor' of github.com:Start9Labs/start-os into next/major 2024-04-01 15:19:33 -06:00
Matt Hill
6e6ef57303 Merge pull request #2339 from Start9Labs/rebase/feat/domains
Most things FE 040
2024-04-01 15:12:02 -06:00
Matt Hill
b80e41503f Merge branch 'next/minor' of github.com:Start9Labs/start-os into rebase/feat/domains 2024-03-30 21:14:53 -06:00
Alex Inkin
7f28fc17ca feat: add mobile view for dashboard (#2581) 2024-03-30 08:48:36 -06:00
Matt Hill
70d4a0c022 Merge branch 'integration/new-container-runtime' of github.com:Start9Labs/start-os into rebase/feat/domains 2024-03-27 10:35:42 -06:00
Alex Inkin
8cfd994170 fix: address todos (#2578) 2024-03-27 10:27:51 -06:00
Matt Hill
641e829e3f better interfaces abstractions 2024-03-26 16:37:06 -06:00
Matt Hill
d202cb731d fix bugs 2024-03-25 13:31:30 -06:00
J H
4ab7300376 fix: Bringing in a building for the browser 2024-03-25 11:07:59 -06:00
Matt Hill
18cc5e0ee8 re-add ionic config 2024-03-25 10:28:49 -06:00
Matt Hill
af0cda5dbf update sdk imports 2024-03-24 13:27:13 -06:00
Matt Hill
a730a3719b Merge branch 'integration/new-container-runtime' of github.com:Start9Labs/start-os into rebase/feat/domains 2024-03-24 12:19:37 -06:00
Matt Hill
3b669193f6 refactor downstream for 036 changes (#2577)
refactor codebase for 036 changes
2024-03-24 12:12:55 -06:00
Matt Hill
22cd2e3337 Merge branch 'update/camelCase' of github.com:Start9Labs/start-os into rebase/feat/domains 2024-03-22 13:09:11 -06:00
Matt Hill
7e9d453a2c switch all fe to camelCase 2024-03-22 12:05:41 -06:00
Matt Hill
a4338b0d03 Merge branch 'integration/new-container-runtime' of github.com:Start9Labs/start-os into rebase/feat/domains 2024-03-22 10:10:55 -06:00
Lucy
2021431e2f Update/marketplace (#2575)
* make category link generic

* fix ai category display and svg icons

* fix markdown display and ansi module; cleanup

* convert tailwindcss to scss in marketplace menu component

* convert tailwindcss to scss in marketplace categories component

* convert tailwindcss to scss in marketplace item component

* update launch icon to taiga icon

* convert tailwindcss to scss in marketplace search component + cleanup

* convert tailwindcss to scss in marketplace release notes component + cleanup

* convert tailwindcss to scss in marketplace about component + cleanup

* convert tailwindcss to scss in marketplace additional component

* convert tailwindcss to scss in marketplace dependencies component + misc style fixes

* convert tailwindcss to scss in marketplace hero component + misc style fixes

* convert tailwindcss to scss in marketplace screenshots component

* convert tailwindcss to scss in portal marketplace components

* remove the rest of tailwindscss and fix reset styles

* bump shared and marketplace package versions

* misc style + build fixes

* sync package lock

* fix markdown + cleanup

* fix markdown margins and git hash size

* fix mobile zindex for hero and mobile changing categories routing link
2024-03-21 12:23:28 -06:00
Matt Hill
5e6a7e134f merge 036, everything broken 2024-03-20 13:32:57 -06:00
Alex Inkin
f4fadd366e feat: add new dashboard (#2574)
* feat: add new dashboard

* chore: comments

* fix duplicate

---------

Co-authored-by: Matt Hill <mattnine@protonmail.com>
2024-03-19 08:56:16 -06:00
Alex Inkin
a5b1b4e103 refactor: remove ionic from remaining places (#2565) 2024-02-27 13:15:32 -07:00
Alex Inkin
7b41b295b7 chore: refactor install and setup wizards (#2561)
* chore: refactor install and setup wizards

* chore: return tui-root
2024-02-22 06:58:01 -07:00
Matt Hill
69d5f521a5 remove tor http warnings 2024-02-16 14:12:10 -07:00
Alex Inkin
c0a55142b5 chore: refactor interfaces and remove UI routes (#2560) 2024-02-16 13:45:30 -07:00
Alex Inkin
513fb3428a feat: implement mobile header (#2559)
* feat: implement mobile header

* chore: remove remaining ties to old ui project

* chore: remove ionic from login page

* chore: address comments
2024-02-13 09:03:09 -07:00
Alex Inkin
9a0ae549f6 feat: refactor logs (#2555)
* feat: refactor logs

* chore: comments

* feat: add system logs

* feat: update shared logs
2024-02-05 19:26:00 -07:00
Lucy
4410d7f195 update angular in shared (#2556) 2024-01-31 11:25:25 -05:00
Alex Inkin
92aa70182d refactor: implement breadcrumbs (#2552) 2024-01-22 21:32:11 -05:00
Alex Inkin
90f5864f1e refactor: finalize new portal (#2543) 2023-12-22 16:22:16 -05:00
Alex Inkin
e47f126bd5 feat(portal): refactor marketplace for new portal (#2539)
* feat(portal): refactor marketplace for new portal

* fix background position

* chore: refactor sidebar

* chore: small fix

---------

Co-authored-by: Lucy Cifferello <12953208+elvece@users.noreply.github.com>
2023-12-18 16:43:59 -05:00
Alex Inkin
ea6f70e3c5 chore: migrate to Angular 17 (#2538)
* chore: migrate to Angular 17

* chore: update
2023-12-11 07:06:55 -07:00
Lucy
0469aab433 Feature/marketplace redesign (#2395)
* wip

* update marketplace categories styling

* update logo icons

* add sort pipe

* update search component styling

* clean up categories component

* cleanup and remove unnecessary sort pipe

* query packages in selected category

* fix search styling

* add reg icon and font, adjust category styles

* fix build from rebasing integration/refactors

* adjust marketplace types for icon with store data, plus formatting

* formatting

* update categories and search

* hover styling for categories

* category styling

* refactor for category as a behavior subject

* more category styling

* base functionality with new marketplace components

* styling cleanup

* misc style fixes and fix category selection from package page

* fixes from review feedback

* add and style additional details

* implement release notes modal

* fix menu when on service show page mobile to display change marketplace

* style and responsiveness fixes

* rename header to sidebar

* input icon config to sidebar

* add mime type pipe and type fn

* review feedback fixes

* skeleton text, more abstraction

* reorder categories, clean up a little

* audit sidebar, categories, store-icon, marketplace-sidebar, search

* finish code cleanup and fix few bugs

* misc fixes and cleanup

* fix broken styles and markdown

* bump shared marketplace version

* more cleanup

* sync package lock

* rename sidebar component to menu

* wip preview sidebar

* sync package lock

* breakout package show elements into components

* link to brochure in preview; custom taiga button styles

* move marketplace preview component into ui; open preview when viewing service in marketplace

* sync changes post file struture rename

* further cleanup

* create service for sidebar toggle and cleanup marketplace components

* bump shared marketplace version

* bump shared for new images needed for brochure marketplace

* cleanup

---------

Co-authored-by: Matt Hill <mattnine@protonmail.com>
2023-12-08 13:12:38 -07:00
Alex Inkin
ad13b5eb4e feat(portal): refactor settings (#2536)
* feat(portal): refactor settings

* chore: refactor

* small updates

---------

Co-authored-by: Matt Hill <mattnine@protonmail.com>
2023-12-08 12:19:33 -07:00
Alex Inkin
7324a4973f feat(portal): add notifications sidebar (#2516)
* feat(portal): add notifications sidebar

* chore: add service

* chore: simplify style

* chore: fix comments

* WIP, moving notifications to patch-db

* revamp notifications

* chore: small adjustments

---------

Co-authored-by: Matt Hill <mattnine@protonmail.com>
2023-12-08 09:12:03 -07:00
Matt Hill
8bc93d23b2 Merge branch 'next/major' of github.com:Start9Labs/start-os into rebase/feat/domains 2023-11-21 21:18:27 -07:00
Matt Hill
c708b685e1 fix type error 2023-11-20 15:30:53 -07:00
Aiden McClelland
cbde91744f Merge branch 'next/minor' of github.com:Start9Labs/start-os into next/major 2023-11-20 13:21:04 -07:00
Aiden McClelland
147e24204b Merge branch 'next/minor' of github.com:Start9Labs/start-os into next/major 2023-11-16 15:15:43 -07:00
Matt Hill
13c50e428f Merge branch 'next/major' of github.com:Start9Labs/start-os into rebase/feat/domains 2023-11-13 17:13:32 -07:00
Matt Hill
8403ccd3da fix ts errors 2023-11-13 16:58:55 -07:00
Aiden McClelland
e92bd61545 Merge branch 'next/minor' of github.com:Start9Labs/start-os into next/major 2023-11-13 16:36:59 -07:00
Aiden McClelland
8215e0221a Merge branch 'next/minor' of github.com:Start9Labs/start-os into next/major 2023-11-13 16:28:28 -07:00
Aiden McClelland
4b44d6fb83 Merge branch 'next/minor' of github.com:Start9Labs/start-os into next/major 2023-11-13 16:25:24 -07:00
Matt Hill
0ae3e83ce4 fix makefile 2023-11-13 16:21:14 -07:00
Matt Hill
f4b573379d Merge branch 'next/major' of github.com:Start9Labs/start-os into rebase/feat/domains 2023-11-13 16:14:31 -07:00
Matt Hill
862ca375ee rename frontend to web 2023-11-13 15:59:16 -07:00
Aiden McClelland
530de6741b Merge branch 'next/minor' of github.com:Start9Labs/start-os into next/major 2023-11-13 15:41:58 -07:00
Aiden McClelland
35c1ff9014 Merge branch 'next/minor' of github.com:Start9Labs/start-os into next/major 2023-11-13 14:59:16 -07:00
Aiden McClelland
3f4caed922 Merge branch 'next/minor' of github.com:Start9Labs/start-os into next/major 2023-11-09 16:33:15 -07:00
Matt Hill
09303ab2fb make it run 2023-11-09 16:04:18 -07:00
Matt Hill
df1ac8e1e2 makefile too 2023-11-09 15:36:20 -07:00
Matt Hill
7a55c91349 rebased and compiling again 2023-11-09 15:35:47 -07:00
Alex Inkin
c491dfdd3a feat: use routes for service sections (#2502)
* feat: use routes for service sections

* chore: fix comment
2023-11-09 12:23:58 -07:00
Matt Hill
d9cc21f761 merge from master and fix typescript errors 2023-11-08 15:44:05 -07:00
Alex Inkin
06207145af refactor: refactor sideload page (#2475)
* refactor: refactor sideload page

* chore: improve ux

* chore: update

* chore: update
2023-11-06 12:05:05 -05:00
Alex Inkin
b195e3435f fix: fix discussed issues (#2467)
* fix: fix discussed issues

* chore: fix issues

* fix package lock

---------

Co-authored-by: Matt Hill <mattnine@protonmail.com>
2023-10-22 16:49:05 -06:00
Aiden McClelland
34b4577c0b Merge branch 'next' of github.com:Start9Labs/start-os into rebase/integration/refactors 2023-10-18 17:55:09 -06:00
Alex Inkin
8034e5bbcb refactor: refactor updates page to get rid of ionic (#2459) 2023-10-15 21:31:34 -06:00
Alex Inkin
df7a30bd14 refactor: refactor backups page to get rid of ionic (#2446) 2023-10-13 09:04:20 -06:00
Matt Hill
d9dfacaaf4 fe fixes after merge 2023-09-28 14:10:04 -06:00
Aiden McClelland
d43767b945 Merge branch 'next' of github.com:Start9Labs/start-os into rebase/integration/refactors 2023-09-28 13:27:41 -06:00
Alex Inkin
cb36754c46 refactor: refactor service page to get rid of ionic (#2421) 2023-09-26 12:37:46 -06:00
Alex Inkin
7e18aafe20 feat(portal): add scrolling to the desktop (#2410)
* feat(portal): add scrolling to the desktop

* chore: comments

* chore: fix
2023-09-13 12:52:25 -06:00
Matt Hill
f7b079b1b4 start menu lol (#2389)
* start menu lol

* add icons, abstract header menu

* chore: few tweaks

---------

Co-authored-by: waterplea <alexander@inkin.ru>
2023-08-14 07:39:07 -06:00
Aiden McClelland
72ffedead7 fix build 2023-08-08 12:03:54 -06:00
Matt Hill
cf3a501562 Merge branch 'rebase/integration/refactors' of github.com:Start9Labs/start-os into rebase/feat/domains 2023-08-08 10:46:07 -06:00
Matt Hill
7becdc3034 remove qr from deprecated component 2023-08-08 10:43:16 -06:00
Aiden McClelland
f0d599781d Merge branch 'rebase/integration/refactors' of github.com:Start9Labs/start-os into rebase/feat/domains 2023-08-08 10:17:28 -06:00
Aiden McClelland
3386105048 Merge branch 'next' of github.com:Start9Labs/start-os into rebase/integration/refactors 2023-08-08 10:08:59 -06:00
Lucy
3b8fb70db1 Fix/shared module build (#2385)
* update shared version so latest can be pulled into brochure marketplace project

* updated shared to work with new brochure marketplace
2023-08-08 09:52:00 -06:00
Matt Hill
c3ae146580 proxies (#2376)
* proxies

* OS outbound proxy. ugly, needs work

* abstract interface address management

* clearnet and outbound proxies for services

* clean up

* router tab

* smart launching of UIs

* update sdk types

* display outbound proxy on service show and rework menu
2023-08-07 15:14:03 -06:00
Alex Inkin
0d079f0d89 feat(portal): implement drag and drop add/remove (#2383) 2023-08-06 13:30:53 -06:00
Alex Inkin
9f5a90ee9c feat(portal): implement adding/removing to desktop (#2374)
* feat(portal): implement adding/removing to desktop, reordering desktop items, baseline for system utils

* chore: fix comments

---------

Co-authored-by: Matt Hill <MattDHill@users.noreply.github.com>
2023-07-27 12:51:15 -06:00
Matt Hill
a5307fd8cc modernize reset password flow for TUI 2023-07-26 11:53:29 -06:00
Matt Hill
180589144a fix server show type errors 2023-07-26 11:39:20 -06:00
Matt Hill
d9c1867bd7 Merge branch 'rebase/integration/refactors' of github.com:Start9Labs/start-os into rebase/feat/domains 2023-07-26 11:01:56 -06:00
Matt Hill
da37d649ec Merge branch 'master' of github.com:Start9Labs/start-os into rebase/integration/refactors 2023-07-26 10:48:45 -06:00
Alex Inkin
4204b4af90 feat(portal): basis for drawer and cards (#2370) 2023-07-20 18:17:32 -06:00
Mariusz Kogen
941650f668 Fix motd 2023-07-16 18:39:26 +02:00
Alex Inkin
9c0c6c1bd6 feat: basis for portal (#2352) 2023-07-16 09:50:56 -06:00
Matt Hill
bd0ddafcd0 round out adding new domains 2023-07-14 12:53:26 -06:00
Matt Hill
19f5e92a74 drop ipv4 from specific domain config 2023-07-12 17:09:49 -06:00
BluJ
3202c38061 chore: Remove some of the things that are missing 2023-07-12 14:01:56 -06:00
Alex Inkin
e35a8c942b fix: libraries build (#2346) 2023-07-11 17:28:05 -04:00
Matt Hill
31811eb91e fix errors in shared and marketplace 2023-07-11 12:53:42 -06:00
Alex Inkin
b9316a4112 Update angular (#2343)
* chore: update to Angular 15

* chore: update to Angular 16

* chore: update Taiga UI
2023-07-10 13:35:53 -06:00
BluJ
b7abd878ac chore: Use send_modify instead of send 2023-07-10 10:36:16 -06:00
Matt Hill
38c2c47789 Feat/domains
update FE types and unify sideload page with marketplace show

begin popover for UI launch select

update node version for github workflows

fix type errors

eager load more components

fix mocks for types

recalculate updates bad on pkg uninstall

chore: break form-object file structure

files for config

finish file upload API and implement for config

chore: break down form-object by type, part 1

remove NEW from config

comment entire setTimeout for new

generic form options

chore: break down form-object by type, part 2

headers for enums and unions

implement select and multiselect for config

update union types and camel case for specs

implement textarea config value

inputspec and required instead of nullable

remove subtype from list spec

update start-sdk

bump start-sdk

feat: use Taiga UI for config modal (#2250)

* feat: use Taiga UI for config modal

* chore: finish remaining changes

* chore: address comments

* bump sdk version

---------

Co-authored-by: Matt Hill <matthewonthemoon@gmail.com>

update package lock

update to sdk 20 and fix types

chore: update Taiga UI and migrate some more forms (#2252)

update form to latest sdk

validate length for textarea too

chore: accommodate new changes to the specs (#2254)

* chore: accommodate new changes to the specs

* chore: fix error

* chore: fix error

feat: add input color (#2257)

* feat: add input color

* patterns will always be there

---------

Co-authored-by: Matt Hill <matthewonthemoon@gmail.com>

chore: properly type pattern error

update to latest sdk

Add sans-serif font fallback (#2263)

* Add sans-serif font fallback

* Update frontend readme start scripts

feat: add datetime spec support (#2264)

Wifi optional (#2249)

* begin work

* allow enable and disable wifi

* nice styling

* done except for popover not dismissing

* update wifi.ts

* address comments

Feat/automated backups (#2142)

* initial restructuring

* very cool

* new structure in place

* delete unnecessary T

* down the rabbit hole

* getting better

* dont like it

* nice

* very nice

* sessions select all

* nice

* backup runs

* fix targets and more

* small improvements

* mostly working

* address PR comments

* fix error

* delete issue with merge

* fix checkboxes and add API for deleting backup runs

* better styling for checkboxes

* small button in ssh kpage too

* complete multiple UI launcher

* fix actions

* present error toast too

* fix target forms

Add logs window to setup wizard loading screen (#2076)

* add logs window to setup wizard loading screen

* fix type error

* Update frontend/projects/setup-wizard/src/app/services/api/live-api.service.ts

Co-authored-by: Lucy C <12953208+elvece@users.noreply.github.com>

---------

Co-authored-by: Lucy C <12953208+elvece@users.noreply.github.com>

statically type server metrics and use websocket (#2124)

Co-authored-by: Matt Hill <matthewonthemoon@gmail.com>

Feat/external-smtp (#1791)

* UI for EOS smtp, missing API layer

* implement api

* fix errors

* switch to external smtp creds

* fix things up

* fix types

* update types for new forms

* feat: add new form to emails and marketplace (#2268)

* import tuilet module

* feat: get rid of old form completely (#2270)

* move to builder spec and delete developer menu

* update sdk

* tiny

* getting better

* working

* done

* feat: add step to number config

* chore: small fixes

* update SDK and step for numbers

---------

Co-authored-by: Alex Inkin <alexander@inkin.ru>

latest sdk, fix build

update SDK for better disabled props

feat: implement `disabled`, `immutable` and `generate` (#2280)

* feat: implement `disabled`, `immutable` and `generate`

* chore: remove unnecessary code

* chore: add generate to textarea and implement immutable

* no generate for textarea

---------

Co-authored-by: Matt Hill <matthewonthemoon@gmail.com>

update lockfile

refactor: extract loading status to shared library (#2282)

* refactor: extract loading status to shared library

* chore: remove inline style

refactor: break routing down to apps level (#2285)

closes #2212 and closes #2214

Feat/credentials (#2290)

add credentials and remove properties

refactor: break ui up further down (#2292)

* refactor: break ui up further down

* permit loading even when authed

---------

Co-authored-by: Matt Hill <matthewonthemoon@gmail.com>

update patchdb for package compatability fixes

fix file structure

WIP

finish rebase

mvp complete

port forwards mvp

looking good

cleaner system page

move experimental features

manual port overrides

better info headers for jobs pages

refactor: move diagnostic-ui app under ui route (#2306)

* refactor: move diagnostic-ui app under ui route

* chore: hide navigation

* chore: remove ionic from diagnostic

* fix navbar showing on login

---------

Co-authored-by: Matt Hill <matthewonthemoon@gmail.com>

chore: partially remove ionic modals and loaders (#2308)

* chore: partially remove ionic modals and loaders

* change to snake

---------

Co-authored-by: Matt Hill <matthewonthemoon@gmail.com>

better session data fetching

abstract store icon component to shared marketplace project (#2311)

* abstract store icon component to shared marketplace project

* better than using a pipe

* minor cleanup

* chore: fix missing node types in libraries

* typo

---------

Co-authored-by: Matt Hill <matthewonthemoon@gmail.com>
Co-authored-by: waterplea <alexander@inkin.ru>

refactor: continue to get rid of ionic infrastructure (#2325)

refactor: finish removing ionic entities: (#2333)

* refactor: finish removing ionic entities:

ToastController
ErrorToastService
ModalController
AlertController
LoadingController

* chore: rollback testing code

* chore: fix comments

* minor form change

* chore: fix comments

* update clearnet address parts

* move around patchDB

* chore: fix comments

---------

Co-authored-by: Matt Hill <matthewonthemoon@gmail.com>

fixup after rebase
2023-07-07 12:59:31 -06:00
Aiden McClelland
c03778ec8b fixup after rebase 2023-07-07 09:53:03 -06:00
BluJ
29b0850a94 fix: Add in the sub repos 2023-07-06 15:16:17 -06:00
Matt Hill
712fde46eb fix file structure 2023-07-06 15:12:02 -06:00
Lucy Cifferello
c2e79ca5a7 update patchdb for package compatability fixes 2023-07-06 15:12:02 -06:00
Alex Inkin
c3a52b3989 refactor: break ui up further down (#2292)
* refactor: break ui up further down

* permit loading even when authed

---------

Co-authored-by: Matt Hill <matthewonthemoon@gmail.com>
2023-07-06 15:12:02 -06:00
Matt Hill
7213d82f1b Feat/credentials (#2290)
add credentials and remove properties
2023-07-06 15:12:02 -06:00
Matt Hill
5bcad69cf7 closes #2212 and closes #2214 2023-07-06 15:12:02 -06:00
Alex Inkin
c9a487fa4d refactor: break routing down to apps level (#2285) 2023-07-06 15:11:13 -06:00
Alex Inkin
3804a46f3b refactor: extract loading status to shared library (#2282)
* refactor: extract loading status to shared library

* chore: remove inline style
2023-07-06 15:11:13 -06:00
Matt Hill
52c0bb5302 update lockfile 2023-07-06 15:10:56 -06:00
Alex Inkin
8aa19e6420 feat: implement disabled, immutable and generate (#2280)
* feat: implement `disabled`, `immutable` and `generate`

* chore: remove unnecessary code

* chore: add generate to textarea and implement immutable

* no generate for textarea

---------

Co-authored-by: Matt Hill <matthewonthemoon@gmail.com>
2023-07-06 15:10:56 -06:00
Matt Hill
4d1c7a3884 update SDK for better disabled props 2023-07-06 15:10:56 -06:00
Matt Hill
25f2c057b7 latest sdk, fix build 2023-07-06 15:10:56 -06:00
Matt Hill
010be05920 Feat/external-smtp (#1791)
* UI for EOS smtp, missing API layer

* implement api

* fix errors

* switch to external smtp creds

* fix things up

* fix types

* update types for new forms

* feat: add new form to emails and marketplace (#2268)

* import tuilet module

* feat: get rid of old form completely (#2270)

* move to builder spec and delete developer menu

* update sdk

* tiny

* getting better

* working

* done

* feat: add step to number config

* chore: small fixes

* update SDK and step for numbers

---------

Co-authored-by: Alex Inkin <alexander@inkin.ru>
2023-07-06 15:10:56 -06:00
Matt Hill
4c465850a2 lockfile 2023-07-06 15:10:56 -06:00
Aiden McClelland
8313dfaeb9 statically type server metrics and use websocket (#2124)
Co-authored-by: Matt Hill <matthewonthemoon@gmail.com>
2023-07-06 15:10:56 -06:00
Matt Hill
873f2b2814 Add logs window to setup wizard loading screen (#2076)
* add logs window to setup wizard loading screen

* fix type error

* Update frontend/projects/setup-wizard/src/app/services/api/live-api.service.ts

Co-authored-by: Lucy C <12953208+elvece@users.noreply.github.com>

---------

Co-authored-by: Lucy C <12953208+elvece@users.noreply.github.com>
2023-07-06 15:10:56 -06:00
Matt Hill
e53c90f8f0 Feat/automated backups (#2142)
* initial restructuring

* very cool

* new structure in place

* delete unnecessary T

* down the rabbit hole

* getting better

* dont like it

* nice

* very nice

* sessions select all

* nice

* backup runs

* fix targets and more

* small improvements

* mostly working

* address PR comments

* fix error

* delete issue with merge

* fix checkboxes and add API for deleting backup runs

* better styling for checkboxes

* small button in ssh kpage too

* complete multiple UI launcher

* fix actions

* present error toast too

* fix target forms
2023-07-06 15:10:56 -06:00
Matt Hill
9499ea8ca9 Wifi optional (#2249)
* begin work

* allow enable and disable wifi

* nice styling

* done except for popover not dismissing

* update wifi.ts

* address comments
2023-07-06 15:10:43 -06:00
Alex Inkin
f6c09109ba feat: add datetime spec support (#2264) 2023-07-06 15:10:43 -06:00
Benjamin B
273b5768c4 Add sans-serif font fallback (#2263)
* Add sans-serif font fallback

* Update frontend readme start scripts
2023-07-06 15:10:43 -06:00
Matt Hill
ee13cf7dd9 update to latest sdk 2023-07-06 15:10:43 -06:00
waterplea
fecbae761e chore: properly type pattern error 2023-07-06 15:10:43 -06:00
Alex Inkin
e0ee89bdd9 feat: add input color (#2257)
* feat: add input color

* patterns will always be there

---------

Co-authored-by: Matt Hill <matthewonthemoon@gmail.com>
2023-07-06 15:10:43 -06:00
Alex Inkin
833c1f22a3 chore: accommodate new changes to the specs (#2254)
* chore: accommodate new changes to the specs

* chore: fix error

* chore: fix error
2023-07-06 15:10:43 -06:00
Matt Hill
6fed6c8d30 validate length for textarea too 2023-07-06 15:10:43 -06:00
Matt Hill
94cdaf5314 update form to latest sdk 2023-07-06 15:10:43 -06:00
Alex Inkin
f83ae27352 chore: update Taiga UI and migrate some more forms (#2252) 2023-07-06 15:10:43 -06:00
Matt Hill
6badf047c3 update to sdk 20 and fix types 2023-07-06 15:10:43 -06:00
Matt Hill
47de9ad15f update package lock 2023-07-06 15:10:43 -06:00
Alex Inkin
09b91cc663 feat: use Taiga UI for config modal (#2250)
* feat: use Taiga UI for config modal

* chore: finish remaining changes

* chore: address comments

* bump sdk version

---------

Co-authored-by: Matt Hill <matthewonthemoon@gmail.com>
2023-07-06 15:10:43 -06:00
Matt Hill
ded16549f7 bump start-sdk 2023-07-06 15:10:43 -06:00
Matt Hill
c89e47577b update start-sdk 2023-07-06 15:10:43 -06:00
Matt Hill
bb50beb7ab remove subtype from list spec 2023-07-06 15:10:43 -06:00
Matt Hill
e4cd4d64d7 inputspec and required instead of nullable 2023-07-06 15:10:43 -06:00
Matt Hill
5675fc51a0 implement textarea config value 2023-07-06 15:10:43 -06:00
Matt Hill
c7438c4aff update union types and camel case for specs 2023-07-06 15:10:43 -06:00
Matt Hill
4a6a3da36c implement select and multiselect for config 2023-07-06 15:10:43 -06:00
Matt Hill
a657c332b1 headers for enums and unions 2023-07-06 15:10:43 -06:00
waterplea
cc9cd3fc14 chore: break down form-object by type, part 2 2023-07-06 15:10:43 -06:00
Matt Hill
234258a077 generic form options 2023-07-06 15:10:43 -06:00
Matt Hill
13cda80ee6 comment entire setTimeout for new 2023-07-06 15:10:43 -06:00
Matt Hill
f6e142baf5 remove NEW from config 2023-07-06 15:10:43 -06:00
waterplea
ddf1f9bcd5 chore: break down form-object by type, part 1 2023-07-06 15:10:43 -06:00
Matt Hill
aa950669f6 finish file upload API and implement for config 2023-07-06 15:10:43 -06:00
Matt Hill
dacd5d3e6b files for config 2023-07-06 15:09:48 -06:00
waterplea
e76ccba2f7 chore: break form-object file structure 2023-07-06 15:09:48 -06:00
Matt Hill
3933819d53 recalculate updates bad on pkg uninstall 2023-07-06 15:09:48 -06:00
Matt Hill
99019c2b1f fix mocks for types 2023-07-06 15:09:48 -06:00
Matt Hill
4bf5eb398b eager load more components 2023-07-06 15:09:48 -06:00
Matt Hill
dbfbac62c0 fix type errors 2023-07-06 15:09:48 -06:00
Lucy Cifferello
7685293da4 update node version for github workflows 2023-07-06 15:09:48 -06:00
Matt Hill
ee9c328606 begin popover for UI launch select 2023-07-06 15:09:48 -06:00
Matt Hill
cb7790ccba update FE types and unify sideload page with marketplace show 2023-07-06 15:09:48 -06:00
Matt Hill
6556fcc531 fix types 2023-07-06 15:08:30 -06:00
Aiden McClelland
178391e7b2 integration/refactors
wip: Refactoring the service

-> Made new skeleton
-> Added service manager
-> Manager Refactored
-> Cleanup
-> Add gid struct
-> remove synchronizer
-> Added backup into manager
-> Fix the configure signal not send
-> Fixes around backup and sync

wip: Moved over the config into the service manager

js effect for subscribing to config

js effect for subscribing to config

fix errors

chore: Fix some things in the manager for clippy

add interfaces from manifest automatically

make OsApi manager-based

wip: Starting down the bind for the effects

todo: complete a ip todo

chore: Fix the result type on something

todo: Address returning

chore: JS with callbacks

chore: Add in the chown and permissions

chore: Add in the binds and unbinds in

feat: Add in the ability to get configs

makefile changes

add start/stop/restart to effects

config hooks

fix: add a default always to the get status

chore: Only do updates when the thing is installed.

use nistp256 to satisfy firefox

use ed25519 if available

chore: Make the thing buildable for testing

chore: Add in the debugging

fix ip signing

chore: Remove the bluj tracing

fix SQL error

chore: Fix the build

update prettytable to fix segfault

Chore: Make these fn's instead of allways ran.

chore: Fix the testing

fix: The stopping/ restarting service

fix: Fix the restarting.

remove current-dependents, derive instead

remove pointers from current-dependencies

remove pointers and system pointers from FE

v0.3.4

remove health checks from manifest

remove "restarting" bool on "starting" status

remove restarting attr

update makefile

fix

add efi support

fix efi

add redirect if connecting to https over http

clean up

lan port forwarding

add `make update` and `make update-overlay`

fix migration

more protections

fix: Fix a lint

chore: remove the limit on the long-running

fix: Starting sometimes.

fix: Make it so the stop of the main works

fix: Bind local and tor with package.

wip: envs

closes #2152, closes #2155, closes #2157

fix TS error

import config types from sdk

update package.json
2023-07-06 15:08:30 -06:00
Aiden McClelland
18922a1c6d fixup after rebase 2023-07-06 15:08:30 -06:00
BluJ
5e9e26fa67 fix: Fix a lint
chore: remove the limit on the long-running

fix: Starting sometimes.

fix: Make it so the stop of the main works

fix: Bind local and tor with package.

wip: envs

fix TS error

import config types from sdk

update package.json
2023-07-06 15:08:30 -06:00
Aiden McClelland
f5430f9151 lan port forwarding 2023-07-06 15:08:30 -06:00
Aiden McClelland
4dfdf2f92f v0.3.4
remove health checks from manifest

remove "restarting" bool on "starting" status

remove restarting attr
2023-07-06 15:08:30 -06:00
Matt Hill
e4d283cc99 remove current-dependents, derive instead
remove pointers from current-dependencies

remove pointers and system pointers from FE
2023-07-06 15:08:30 -06:00
Aiden McClelland
8ee64d22b3 add start/stop/restart to effects
fix: add a default always to the get status

chore: Only do updates when the thing is installed.

chore: Make the thing buildable for testing

chore: Add in the debugging

chore: Remove the bluj tracing

chore: Fix the build

Chore: Make these fn's instead of allways ran.

chore: Fix the testing

fix: The stopping/ restarting service

fix: Fix the restarting.
2023-07-06 15:08:30 -06:00
Aiden McClelland
10e3e80042 makefile changes 2023-07-06 15:08:30 -06:00
BluJ
f77a208e2c feat: Add in the ability to get configs
config hooks
2023-07-06 15:07:53 -06:00
BluJ
9366dbb96e wip: Starting down the bind for the effects
todo: complete a ip todo

chore: Fix the result type on something

todo: Address returning

chore: JS with callbacks

chore: Add in the chown and permissions

chore: Add in the binds and unbinds in
2023-07-06 15:07:53 -06:00
Aiden McClelland
550b17552b make OsApi manager-based 2023-07-06 15:07:53 -06:00
Aiden McClelland
bec307d0e9 js effect for subscribing to config
fix errors

chore: Fix some things in the manager for clippy
2023-07-06 15:07:53 -06:00
BluJ
93c751f6eb wip: Refactoring the service
-> Made new skeleton
-> Added service manager
-> Manager Refactored
-> Cleanup
-> Add gid struct
-> remove synchronizer
-> Added backup into manager
-> Fix the configure signal not send
-> Fixes around backup and sync

wip: Moved over the config into the service manager
2023-07-06 15:07:53 -06:00
1575 changed files with 86911 additions and 67756 deletions

118
.github/workflows/start-cli.yaml vendored Normal file
View File

@@ -0,0 +1,118 @@
name: start-cli
on:
workflow_call:
workflow_dispatch:
inputs:
environment:
type: choice
description: Environment
options:
- NONE
- dev
- unstable
- dev-unstable
runner:
type: choice
description: Runner
options:
- standard
- fast
arch:
type: choice
description: Architecture
options:
- ALL
- x86_64
- x86_64-apple
- aarch64
- aarch64-apple
- riscv64
push:
branches:
- master
- next/*
pull_request:
branches:
- master
- next/*
env:
NODEJS_VERSION: "24.11.0"
ENVIRONMENT: '${{ fromJson(format(''["{0}", ""]'', github.event.inputs.environment || ''dev''))[github.event.inputs.environment == ''NONE''] }}'
jobs:
compile:
name: Build Debian Package
strategy:
fail-fast: true
matrix:
triple: >-
${{
fromJson('{
"x86_64": ["x86_64-unknown-linux-musl"],
"x86_64-apple": ["x86_64-apple-darwin"],
"aarch64": ["aarch64-unknown-linux-musl"],
"x86_64-apple": ["aarch64-apple-darwin"],
"riscv64": ["riscv64gc-unknown-linux-musl"],
"ALL": ["x86_64-unknown-linux-musl", "x86_64-apple-darwin", "aarch64-unknown-linux-musl", "aarch64-apple-darwin", "riscv64gc-unknown-linux-musl"]
}')[github.event.inputs.platform || 'ALL']
}}
runs-on: ${{ fromJson('["ubuntu-latest", "buildjet-32vcpu-ubuntu-2204"]')[github.event.inputs.runner == 'fast'] }}
steps:
- name: Cleaning up unnecessary files
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' }}
- uses: actions/checkout@v4
with:
submodules: recursive
- 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
run: TARGET=${{ matrix.triple }} make cli
env:
PLATFORM: ${{ matrix.arch }}
SCCACHE_GHA_ENABLED: on
SCCACHE_GHA_VERSION: 0
- uses: actions/upload-artifact@v4
with:
name: start-cli_${{ matrix.triple }}
path: core/target/${{ matrix.triple }}/release/start-cli

203
.github/workflows/start-registry.yaml vendored Normal file
View File

@@ -0,0 +1,203 @@
name: Start-Registry
on:
workflow_call:
workflow_dispatch:
inputs:
environment:
type: choice
description: Environment
options:
- NONE
- dev
- unstable
- dev-unstable
runner:
type: choice
description: Runner
options:
- standard
- fast
arch:
type: choice
description: Architecture
options:
- ALL
- x86_64
- aarch64
- riscv64
push:
branches:
- master
- next/*
pull_request:
branches:
- master
- next/*
env:
NODEJS_VERSION: "24.11.0"
ENVIRONMENT: '${{ fromJson(format(''["{0}", ""]'', github.event.inputs.environment || ''dev''))[github.event.inputs.environment == ''NONE''] }}'
jobs:
compile:
name: Build Debian Package
strategy:
fail-fast: true
matrix:
arch: >-
${{
fromJson('{
"x86_64": ["x86_64"],
"aarch64": ["aarch64"],
"riscv64": ["riscv64"],
"ALL": ["x86_64", "aarch64", "riscv64"]
}')[github.event.inputs.platform || 'ALL']
}}
runs-on: ${{ fromJson('["ubuntu-latest", "buildjet-32vcpu-ubuntu-2204"]')[github.event.inputs.runner == 'fast'] }}
steps:
- name: Cleaning up unnecessary files
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' }}
- uses: actions/checkout@v4
with:
submodules: recursive
- 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
run: make registry-deb
env:
PLATFORM: ${{ matrix.arch }}
SCCACHE_GHA_ENABLED: on
SCCACHE_GHA_VERSION: 0
- uses: actions/upload-artifact@v4
with:
name: start-registry_${{ matrix.arch }}.deb
path: results/start-registry-*_${{ matrix.arch }}.deb
create-image:
name: Create Docker Image
needs: [compile]
permissions:
contents: read
packages: write
runs-on: ${{ fromJson('["ubuntu-latest", "buildjet-32vcpu-ubuntu-2204"]')[github.event.inputs.runner == 'fast'] }}
steps:
- name: Cleaning up unnecessary files
run: |
sudo apt-get remove --purge -y google-chrome-stable firefox mono-devel
sudo apt-get autoremove -y
sudo apt-get clean
- run: |
sudo mount -t tmpfs tmpfs .
if: ${{ github.event.inputs.runner == 'fast' }}
- name: Set up docker QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: "Login to GitHub Container Registry"
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{github.actor}}
password: ${{secrets.GITHUB_TOKEN}}
- name: Docker meta
id: meta
uses: docker/metadata-action@v5
with:
images: ghcr.io/Start9Labs/startos-registry
tags: |
type=raw,value=${{ github.ref_name }}
- name: Download debian package
uses: actions/download-artifact@v4
with:
pattern: start-registry_*.deb
- name: Map matrix.arch to docker platform
run: |
platforms=""
for deb in *.deb; do
filename=$(basename "$deb" .deb)
arch="${filename#*_}"
case "$arch" in
x86_64)
platform="linux/amd64"
;;
aarch64)
platform="linux/arm64"
;;
riscv64)
platform="linux/riscv64"
;;
*)
echo "Unknown architecture: $arch" >&2
exit 1
;;
esac
if [ -z "$platforms" ]; then
platforms="$platform"
else
platforms="$platforms,$platform"
fi
done
echo "DOCKER_PLATFORM=$platforms" >> "$GITHUB_ENV"
- run: |
cat | docker buildx build --platform "$DOCKER_PLATFORM" --push -t ${{ steps.meta.outputs.tags }} -f - . << 'EOF'
FROM debian:trixie
ADD *.deb .
RUN apt-get install -y ./*_$(uname -m).deb && rm *.deb
VOLUME /var/lib/startos
ENV RUST_LOG=startos=debug
ENTRYPOINT ["start-registryd"]
EOF

114
.github/workflows/start-tunnel.yaml vendored Normal file
View File

@@ -0,0 +1,114 @@
name: Start-Tunnel
on:
workflow_call:
workflow_dispatch:
inputs:
environment:
type: choice
description: Environment
options:
- NONE
- dev
- unstable
- dev-unstable
runner:
type: choice
description: Runner
options:
- standard
- fast
arch:
type: choice
description: Architecture
options:
- ALL
- x86_64
- aarch64
- riscv64
push:
branches:
- master
- next/*
pull_request:
branches:
- master
- next/*
env:
NODEJS_VERSION: "24.11.0"
ENVIRONMENT: '${{ fromJson(format(''["{0}", ""]'', github.event.inputs.environment || ''dev''))[github.event.inputs.environment == ''NONE''] }}'
jobs:
compile:
name: Build Debian Package
strategy:
fail-fast: true
matrix:
arch: >-
${{
fromJson('{
"x86_64": ["x86_64"],
"aarch64": ["aarch64"],
"riscv64": ["riscv64"],
"ALL": ["x86_64", "aarch64", "riscv64"]
}')[github.event.inputs.platform || 'ALL']
}}
runs-on: ${{ fromJson('["ubuntu-latest", "buildjet-32vcpu-ubuntu-2204"]')[github.event.inputs.runner == 'fast'] }}
steps:
- name: Cleaning up unnecessary files
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' }}
- uses: actions/checkout@v4
with:
submodules: recursive
- 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
run: make tunnel-deb
env:
PLATFORM: ${{ matrix.arch }}
SCCACHE_GHA_ENABLED: on
SCCACHE_GHA_VERSION: 0
- uses: actions/upload-artifact@v4
with:
name: start-tunnel_${{ matrix.arch }}.deb
path: results/start-tunnel-*_${{ matrix.arch }}.deb

View File

@@ -28,6 +28,7 @@ on:
- aarch64 - aarch64
- aarch64-nonfree - aarch64-nonfree
- raspberrypi - raspberrypi
- riscv64
deploy: deploy:
type: choice type: choice
description: Deploy description: Deploy
@@ -45,7 +46,7 @@ on:
- next/* - next/*
env: env:
NODEJS_VERSION: "20.16.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''] }}'
jobs: jobs:
@@ -62,11 +63,48 @@ jobs:
"aarch64": ["aarch64"], "aarch64": ["aarch64"],
"aarch64-nonfree": ["aarch64"], "aarch64-nonfree": ["aarch64"],
"raspberrypi": ["aarch64"], "raspberrypi": ["aarch64"],
"ALL": ["x86_64", "aarch64"] "riscv64": ["riscv64"],
"ALL": ["x86_64", "aarch64", "riscv64"]
}')[github.event.inputs.platform || 'ALL'] }')[github.event.inputs.platform || 'ALL']
}} }}
runs-on: ${{ fromJson('["ubuntu-22.04", "buildjet-32vcpu-ubuntu-2204"]')[github.event.inputs.runner == 'fast'] }} runs-on: >-
${{
fromJson(
format(
'["{0}", "{1}"]',
fromJson('{
"x86_64": "ubuntu-latest",
"aarch64": "ubuntu-24.04-arm",
"riscv64": "ubuntu-latest"
}')[matrix.arch],
fromJson('{
"x86_64": "buildjet-32vcpu-ubuntu-2204",
"aarch64": "buildjet-32vcpu-ubuntu-2204-arm",
"riscv64": "buildjet-32vcpu-ubuntu-2204"
}')[matrix.arch]
)
)[github.event.inputs.runner == 'fast']
}}
steps: steps:
- name: Cleaning up unnecessary files
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: | - run: |
sudo mount -t tmpfs tmpfs . sudo mount -t tmpfs tmpfs .
if: ${{ github.event.inputs.runner == 'fast' }} if: ${{ github.event.inputs.runner == 'fast' }}
@@ -93,8 +131,18 @@ jobs:
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3 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
env:
SCCACHE_GHA_ENABLED: on
SCCACHE_GHA_VERSION: 0
- uses: actions/upload-artifact@v4 - uses: actions/upload-artifact@v4
with: with:
@@ -112,7 +160,7 @@ jobs:
format( format(
'[ '[
["{0}"], ["{0}"],
["x86_64", "x86_64-nonfree", "aarch64", "aarch64-nonfree", "raspberrypi"] ["x86_64", "x86_64-nonfree", "aarch64", "aarch64-nonfree", "riscv64", "raspberrypi"]
]', ]',
github.event.inputs.platform || 'ALL' github.event.inputs.platform || 'ALL'
) )
@@ -122,13 +170,22 @@ jobs:
${{ ${{
fromJson( fromJson(
format( format(
'["ubuntu-22.04", "{0}"]', '["{0}", "{1}"]',
fromJson('{
"x86_64": "ubuntu-latest",
"x86_64-nonfree": "ubuntu-latest",
"aarch64": "ubuntu-24.04-arm",
"aarch64-nonfree": "ubuntu-24.04-arm",
"raspberrypi": "ubuntu-24.04-arm",
"riscv64": "ubuntu-24.04-arm",
}')[matrix.platform],
fromJson('{ fromJson('{
"x86_64": "buildjet-8vcpu-ubuntu-2204", "x86_64": "buildjet-8vcpu-ubuntu-2204",
"x86_64-nonfree": "buildjet-8vcpu-ubuntu-2204", "x86_64-nonfree": "buildjet-8vcpu-ubuntu-2204",
"aarch64": "buildjet-8vcpu-ubuntu-2204-arm", "aarch64": "buildjet-8vcpu-ubuntu-2204-arm",
"aarch64-nonfree": "buildjet-8vcpu-ubuntu-2204-arm", "aarch64-nonfree": "buildjet-8vcpu-ubuntu-2204-arm",
"raspberrypi": "buildjet-8vcpu-ubuntu-2204-arm", "raspberrypi": "buildjet-8vcpu-ubuntu-2204-arm",
"riscv64": "buildjet-8vcpu-ubuntu-2204",
}')[matrix.platform] }')[matrix.platform]
) )
)[github.event.inputs.runner == 'fast'] )[github.event.inputs.runner == 'fast']
@@ -142,11 +199,29 @@ jobs:
"aarch64": "aarch64", "aarch64": "aarch64",
"aarch64-nonfree": "aarch64", "aarch64-nonfree": "aarch64",
"raspberrypi": "aarch64", "raspberrypi": "aarch64",
"riscv64": "riscv64",
}')[matrix.platform] }')[matrix.platform]
}} }}
steps: steps:
- name: Free space - name: Free space
run: rm -rf /opt/hostedtoolcache* 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.)
if: ${{ github.event.inputs.runner != 'fast' }} if: ${{ github.event.inputs.runner != 'fast' }}
- uses: actions/checkout@v4 - uses: actions/checkout@v4
@@ -253,7 +328,7 @@ jobs:
index: index:
if: ${{ github.event.inputs.deploy != '' && github.event.inputs.deploy != 'NONE' }} if: ${{ github.event.inputs.deploy != '' && github.event.inputs.deploy != 'NONE' }}
needs: [image] needs: [image]
runs-on: ubuntu-22.04 runs-on: ubuntu-latest
steps: steps:
- run: >- - run: >-
curl "https://${{ curl "https://${{

View File

@@ -11,13 +11,13 @@ on:
- next/* - next/*
env: env:
NODEJS_VERSION: "20.16.0" NODEJS_VERSION: "24.11.0"
ENVIRONMENT: dev-unstable ENVIRONMENT: dev-unstable
jobs: jobs:
test: test:
name: Run Automated Tests name: Run Automated Tests
runs-on: ubuntu-22.04 runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
with: with:

3
.gitignore vendored
View File

@@ -1,8 +1,5 @@
.DS_Store .DS_Store
.idea .idea
system-images/binfmt/binfmt.tar
system-images/compat/compat.tar
system-images/util/util.tar
/*.img /*.img
/*.img.gz /*.img.gz
/*.img.xz /*.img.xz

View File

@@ -1,7 +1,6 @@
# Contributing to StartOS # Contributing to StartOS
This guide is for contributing to the StartOS. If you are interested in packaging a service for StartOS, visit the [service packaging guide](https://docs.start9.com/latest/developer-docs/). If you are interested in promoting, providing technical support, creating tutorials, or helping in other ways, please visit the [Start9 website](https://start9.com/contribute). This guide is for contributing to the StartOS. If you are interested in packaging a service for StartOS, visit the [service packaging guide](https://docs.start9.com/latest/packaging-guide/). If you are interested in promoting, providing technical support, creating tutorials, or helping in other ways, please visit the [Start9 website](https://start9.com/contribute).
## Collaboration ## Collaboration
@@ -13,64 +12,77 @@ This guide is for contributing to the StartOS. If you are interested in packagin
```bash ```bash
/ /
├── assets/ ├── assets/
├── container-runtime/
├── core/ ├── core/
├── build/ ├── build/
├── debian/ ├── debian/
├── web/ ├── web/
├── image-recipe/ ├── image-recipe/
├── patch-db ├── patch-db
└── system-images/ └── sdk/
``` ```
#### assets #### assets
screenshots for the StartOS README screenshots for the StartOS README
#### container-runtime
A NodeJS program that dynamically loads maintainer scripts and communicates with the OS to manage packages
#### core #### core
An API, daemon (startd), CLI (start-cli), and SDK (start-sdk) that together provide the core functionality of StartOS.
An API, daemon (startd), and CLI (start-cli) that together provide the core functionality of StartOS.
#### build #### build
Auxiliary files and scripts to include in deployed StartOS images Auxiliary files and scripts to include in deployed StartOS images
#### debian #### debian
Maintainer scripts for the StartOS Debian package Maintainer scripts for the StartOS Debian package
#### web #### web
Web UIs served under various conditions and used to interact with StartOS APIs. Web UIs served under various conditions and used to interact with StartOS APIs.
#### image-recipe #### image-recipe
Scripts for building StartOS images Scripts for building StartOS images
#### patch-db (submodule) #### patch-db (submodule)
A diff based data store used to synchronize data between the web interfaces and server. A diff based data store used to synchronize data between the web interfaces and server.
#### system-images #### sdk
Docker images that assist with creating backups.
A typescript sdk for building start-os packages
## Environment Setup ## Environment Setup
#### Clone the StartOS repository #### Clone the StartOS repository
```sh ```sh
git clone https://github.com/Start9Labs/start-os.git git clone https://github.com/Start9Labs/start-os.git --recurse-submodules
cd start-os cd start-os
``` ```
#### Load the PatchDB submodule
```sh
git submodule update --init --recursive
```
#### Continue to your project of interest for additional instructions: #### Continue to your project of interest for additional instructions:
- [`core`](core/README.md) - [`core`](core/README.md)
- [`web-interfaces`](web-interfaces/README.md) - [`web-interfaces`](web-interfaces/README.md)
- [`build`](build/README.md) - [`build`](build/README.md)
- [`patch-db`](https://github.com/Start9Labs/patch-db) - [`patch-db`](https://github.com/Start9Labs/patch-db)
## Building ## Building
This project uses [GNU Make](https://www.gnu.org/software/make/) to build its components. To build any specific component, simply run `make <TARGET>` replacing `<TARGET>` with the name of the target you'd like to build This project uses [GNU Make](https://www.gnu.org/software/make/) to build its components. To build any specific component, simply run `make <TARGET>` replacing `<TARGET>` with the name of the target you'd like to build
### Requirements ### Requirements
- [GNU Make](https://www.gnu.org/software/make/) - [GNU Make](https://www.gnu.org/software/make/)
- [Docker](https://docs.docker.com/get-docker/) - [Docker](https://docs.docker.com/get-docker/)
- [NodeJS v18.15.0](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm) - [NodeJS v20.16.0](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm)
- [sed](https://www.gnu.org/software/sed/) - [sed](https://www.gnu.org/software/sed/)
- [grep](https://www.gnu.org/software/grep/) - [grep](https://www.gnu.org/software/grep/)
- [awk](https://www.gnu.org/software/gawk/) - [awk](https://www.gnu.org/software/gawk/)
@@ -79,41 +91,43 @@ This project uses [GNU Make](https://www.gnu.org/software/make/) to build its co
- [brotli](https://github.com/google/brotli) - [brotli](https://github.com/google/brotli)
### Environment variables ### Environment variables
- `PLATFORM`: which platform you would like to build for. Must be one of `x86_64`, `x86_64-nonfree`, `aarch64`, `aarch64-nonfree`, `raspberrypi` - `PLATFORM`: which platform you would like to build for. Must be one of `x86_64`, `x86_64-nonfree`, `aarch64`, `aarch64-nonfree`, `raspberrypi`
- NOTE: `nonfree` images are for including `nonfree` firmware packages in the built ISO - NOTE: `nonfree` images are for including `nonfree` firmware packages in the built ISO
- `ENVIRONMENT`: a hyphen separated set of feature flags to enable - `ENVIRONMENT`: a hyphen separated set of feature flags to enable
- `dev`: enables password ssh (INSECURE!) and does not compress frontends - `dev`: enables password ssh (INSECURE!) and does not compress frontends
- `unstable`: enables assertions that will cause errors on unexpected inconsistencies that are undesirable in production use either for performance or reliability reasons - `unstable`: enables assertions that will cause errors on unexpected inconsistencies that are undesirable in production use either for performance or reliability reasons
- `docker`: use `docker` instead of `podman` - `docker`: use `docker` instead of `podman`
- `GIT_BRANCH_AS_HASH`: set to `1` to use the current git branch name as the git hash so that the project does not need to be rebuilt on each commit - `GIT_BRANCH_AS_HASH`: set to `1` to use the current git branch name as the git hash so that the project does not need to be rebuilt on each commit
### Useful Make Targets ### Useful Make Targets
- `iso`: Create a full `.iso` image - `iso`: Create a full `.iso` image
- Only possible from Debian - Only possible from Debian
- Not available for `PLATFORM=raspberrypi` - Not available for `PLATFORM=raspberrypi`
- Additional Requirements: - Additional Requirements:
- [debspawn](https://github.com/lkhq/debspawn) - [debspawn](https://github.com/lkhq/debspawn)
- `img`: Create a full `.img` image - `img`: Create a full `.img` image
- Only possible from Debian - Only possible from Debian
- Only available for `PLATFORM=raspberrypi` - Only available for `PLATFORM=raspberrypi`
- Additional Requirements: - Additional Requirements:
- [debspawn](https://github.com/lkhq/debspawn) - [debspawn](https://github.com/lkhq/debspawn)
- `format`: Run automatic code formatting for the project - `format`: Run automatic code formatting for the project
- Additional Requirements: - Additional Requirements:
- [rust](https://rustup.rs/) - [rust](https://rustup.rs/)
- `test`: Run automated tests for the project - `test`: Run automated tests for the project
- Additional Requirements: - Additional Requirements:
- [rust](https://rustup.rs/) - [rust](https://rustup.rs/)
- `update`: Deploy the current working project to a device over ssh as if through an over-the-air update - `update`: Deploy the current working project to a device over ssh as if through an over-the-air update
- Requires an argument `REMOTE` which is the ssh address of the device, i.e. `start9@192.168.122.2` - Requires an argument `REMOTE` which is the ssh address of the device, i.e. `start9@192.168.122.2`
- `reflash`: Deploy the current working project to a device over ssh as if using a live `iso` image to reflash it - `reflash`: Deploy the current working project to a device over ssh as if using a live `iso` image to reflash it
- Requires an argument `REMOTE` which is the ssh address of the device, i.e. `start9@192.168.122.2` - Requires an argument `REMOTE` which is the ssh address of the device, i.e. `start9@192.168.122.2`
- `update-overlay`: Deploy the current working project to a device over ssh to the in-memory overlay without restarting it - `update-overlay`: Deploy the current working project to a device over ssh to the in-memory overlay without restarting it
- WARNING: changes will be reverted after the device is rebooted - WARNING: changes will be reverted after the device is rebooted
- WARNING: changes to `init` will not take effect as the device is already initialized - WARNING: changes to `init` will not take effect as the device is already initialized
- Requires an argument `REMOTE` which is the ssh address of the device, i.e. `start9@192.168.122.2` - Requires an argument `REMOTE` which is the ssh address of the device, i.e. `start9@192.168.122.2`
- `wormhole`: Deploy the `startbox` to a device using [magic-wormhole](https://github.com/magic-wormhole/magic-wormhole) - `wormhole`: Deploy the `startbox` to a device using [magic-wormhole](https://github.com/magic-wormhole/magic-wormhole)
- When the build it complete will emit a command to paste into the shell of the device to upgrade it - When the build it complete will emit a command to paste into the shell of the device to upgrade it
- Additional Requirements: - Additional Requirements:
- [magic-wormhole](https://github.com/magic-wormhole/magic-wormhole) - [magic-wormhole](https://github.com/magic-wormhole/magic-wormhole)
- `clean`: Delete all compiled artifacts - `clean`: Delete all compiled artifacts

View File

@@ -25,15 +25,15 @@ docker buildx create --use
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh # proceed with default installation curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh # proceed with default installation
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/master/install.sh | bash curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/master/install.sh | bash
source ~/.bashrc source ~/.bashrc
nvm install 20 nvm install 24
nvm use 20 nvm use 24
nvm alias default 20 # this prevents your machine from reverting back to another version nvm alias default 24 # this prevents your machine from reverting back to another version
``` ```
## Cloning the repository ## Cloning the repository
```sh ```sh
git clone --recursive https://github.com/Start9Labs/start-os.git --branch next/minor git clone --recursive https://github.com/Start9Labs/start-os.git --branch next/major
cd start-os cd start-os
``` ```

228
Makefile
View File

@@ -1,32 +1,45 @@
ls-files = $(shell git ls-files --cached --others --exclude-standard $1)
PROFILE = release
PLATFORM_FILE := $(shell ./check-platform.sh) PLATFORM_FILE := $(shell ./check-platform.sh)
ENVIRONMENT_FILE := $(shell ./check-environment.sh) ENVIRONMENT_FILE := $(shell ./check-environment.sh)
GIT_HASH_FILE := $(shell ./check-git-hash.sh) GIT_HASH_FILE := $(shell ./check-git-hash.sh)
VERSION_FILE := $(shell ./check-version.sh) VERSION_FILE := $(shell ./check-version.sh)
BASENAME := $(shell ./basename.sh) BASENAME := $(shell PROJECT=startos ./basename.sh)
PLATFORM := $(shell if [ -f ./PLATFORM.txt ]; then cat ./PLATFORM.txt; else echo unknown; fi) 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) ARCH := $(shell if [ "$(PLATFORM)" = "raspberrypi" ]; then echo aarch64; else echo $(PLATFORM) | sed 's/-nonfree$$//g'; fi)
RUST_ARCH := $(shell if [ "$(ARCH)" = "riscv64" ]; then echo riscv64gc; else echo $(ARCH); fi)
REGISTRY_BASENAME := $(shell PROJECT=start-registry PLATFORM=$(ARCH) ./basename.sh)
TUNNEL_BASENAME := $(shell PROJECT=start-tunnel PLATFORM=$(ARCH) ./basename.sh)
IMAGE_TYPE=$(shell if [ "$(PLATFORM)" = raspberrypi ]; then echo img; else echo iso; fi) IMAGE_TYPE=$(shell if [ "$(PLATFORM)" = raspberrypi ]; then echo img; else echo iso; fi)
WEB_UIS := web/dist/raw/ui/index.html web/dist/raw/setup-wizard/index.html web/dist/raw/install-wizard/index.html 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 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) 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) BUILD_SRC := $(call ls-files, build) build/lib/depends build/lib/conflicts $(FIRMWARE_ROMS)
DEBIAN_SRC := $(shell git ls-files debian/) IMAGE_RECIPE_SRC := $(call ls-files, image-recipe/)
IMAGE_RECIPE_SRC := $(shell git ls-files image-recipe/)
STARTD_SRC := core/startos/startd.service $(BUILD_SRC) STARTD_SRC := core/startos/startd.service $(BUILD_SRC)
COMPAT_SRC := $(shell git ls-files system-images/compat/) CORE_SRC := $(call ls-files, core) $(shell git ls-files --recurse-submodules patch-db) $(GIT_HASH_FILE)
UTILS_SRC := $(shell git ls-files system-images/utils/) WEB_SHARED_SRC := $(call ls-files, web/projects/shared) $(call ls-files, web/projects/marketplace) $(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 sdk/baseDist/package.json web/patchdb-ui-seed.json sdk/dist/package.json
BINFMT_SRC := $(shell git ls-files system-images/binfmt/) WEB_UI_SRC := $(call ls-files, web/projects/ui)
CORE_SRC := $(shell git ls-files core) $(shell git ls-files --recurse-submodules patch-db) $(GIT_HASH_FILE) WEB_SETUP_WIZARD_SRC := $(call ls-files, web/projects/setup-wizard)
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 sdk/baseDist/package.json web/patchdb-ui-seed.json sdk/dist/package.json WEB_INSTALL_WIZARD_SRC := $(call ls-files, web/projects/install-wizard)
WEB_UI_SRC := $(shell git ls-files web/projects/ui) WEB_START_TUNNEL_SRC := $(call ls-files, web/projects/start-tunnel)
WEB_SETUP_WIZARD_SRC := $(shell git ls-files web/projects/setup-wizard)
WEB_INSTALL_WIZARD_SRC := $(shell git ls-files web/projects/install-wizard)
PATCH_DB_CLIENT_SRC := $(shell git ls-files --recurse-submodules patch-db/client) PATCH_DB_CLIENT_SRC := $(shell git ls-files --recurse-submodules patch-db/client)
GZIP_BIN := $(shell which pigz || which gzip) GZIP_BIN := $(shell which pigz || which gzip)
TAR_BIN := $(shell which gtar || which tar) TAR_BIN := $(shell which gtar || which tar)
COMPILED_TARGETS := core/target/$(ARCH)-unknown-linux-musl/release/startbox core/target/$(ARCH)-unknown-linux-musl/release/containerbox system-images/compat/docker-images/$(ARCH).tar system-images/utils/docker-images/$(ARCH).tar system-images/binfmt/docker-images/$(ARCH).tar container-runtime/rootfs.$(ARCH).squashfs COMPILED_TARGETS := core/target/$(RUST_ARCH)-unknown-linux-musl/$(PROFILE)/startbox core/target/$(RUST_ARCH)-unknown-linux-musl/release/start-container container-runtime/rootfs.$(ARCH).squashfs
ALL_TARGETS := $(STARTD_SRC) $(ENVIRONMENT_FILE) $(GIT_HASH_FILE) $(VERSION_FILE) $(COMPILED_TARGETS) cargo-deps/$(ARCH)-unknown-linux-musl/release/startos-backup-fs $(shell if [ "$(PLATFORM)" = "raspberrypi" ]; then echo cargo-deps/aarch64-unknown-linux-musl/release/pi-beep; fi) $(shell /bin/bash -c 'if [[ "${ENVIRONMENT}" =~ (^|-)unstable($$|-) ]]; then echo cargo-deps/$(ARCH)-unknown-linux-musl/release/tokio-console; fi') $(PLATFORM_FILE) STARTOS_TARGETS := $(STARTD_SRC) $(ENVIRONMENT_FILE) $(GIT_HASH_FILE) $(VERSION_FILE) $(COMPILED_TARGETS) cargo-deps/$(RUST_ARCH)-unknown-linux-musl/release/startos-backup-fs $(PLATFORM_FILE) \
REBUILD_TYPES = 1 $(shell if [ "$(PLATFORM)" = "raspberrypi" ]; then \
echo cargo-deps/aarch64-unknown-linux-musl/release/pi-beep; \
fi) \
$(shell /bin/bash -c 'if [[ "${ENVIRONMENT}" =~ (^|-)unstable($$|-) ]]; then \
echo cargo-deps/$(RUST_ARCH)-unknown-linux-musl/release/flamegraph; \
fi') \
$(shell /bin/bash -c 'if [[ "${ENVIRONMENT}" =~ (^|-)console($$|-) ]]; then \
echo cargo-deps/$(RUST_ARCH)-unknown-linux-musl/release/tokio-console; \
fi')
REGISTRY_TARGETS := core/target/$(RUST_ARCH)-unknown-linux-musl/$(PROFILE)/registrybox core/startos/start-registryd.service
TUNNEL_TARGETS := core/target/$(RUST_ARCH)-unknown-linux-musl/$(PROFILE)/tunnelbox core/startos/start-tunneld.service
ifeq ($(REMOTE),) ifeq ($(REMOTE),)
mkdir = mkdir -p $1 mkdir = mkdir -p $1
@@ -49,21 +62,16 @@ endif
.DELETE_ON_ERROR: .DELETE_ON_ERROR:
.PHONY: all metadata install clean format cli uis ui reflash deb $(IMAGE_TYPE) squashfs sudo wormhole wormhole-deb test test-core test-sdk test-container-runtime registry .PHONY: all metadata install clean format install-cli cli uis ui reflash deb $(IMAGE_TYPE) squashfs wormhole wormhole-deb test test-core test-sdk test-container-runtime registry install-registry tunnel install-tunnel ts-bindings
all: $(ALL_TARGETS) all: $(STARTOS_TARGETS)
touch: touch:
touch $(ALL_TARGETS) touch $(STARTOS_TARGETS)
metadata: $(VERSION_FILE) $(PLATFORM_FILE) $(ENVIRONMENT_FILE) $(GIT_HASH_FILE) metadata: $(VERSION_FILE) $(PLATFORM_FILE) $(ENVIRONMENT_FILE) $(GIT_HASH_FILE)
sudo:
sudo true
clean: clean:
rm -f system-images/**/*.tar
rm -rf system-images/compat/target
rm -rf core/target rm -rf core/target
rm -rf core/startos/bindings rm -rf core/startos/bindings
rm -rf web/.angular rm -rf web/.angular
@@ -98,25 +106,63 @@ test: | test-core test-sdk test-container-runtime
test-core: $(CORE_SRC) $(ENVIRONMENT_FILE) test-core: $(CORE_SRC) $(ENVIRONMENT_FILE)
./core/run-tests.sh ./core/run-tests.sh
test-sdk: $(shell git ls-files sdk) sdk/base/lib/osBindings/index.ts test-sdk: $(call ls-files, sdk) sdk/base/lib/osBindings/index.ts
cd sdk && make test cd sdk && make test
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 test-container-runtime: container-runtime/node_modules/.package-lock.json $(call ls-files, container-runtime/src) container-runtime/package.json container-runtime/tsconfig.json
cd container-runtime && npm test cd container-runtime && npm test
cli: install-cli: $(GIT_HASH_FILE)
cd core && ./install-cli.sh ./core/build-cli.sh --install
registry: cli: $(GIT_HASH_FILE)
cd core && ./build-registrybox.sh ./core/build-cli.sh
registry: core/target/$(RUST_ARCH)-unknown-linux-musl/$(PROFILE)/registrybox
install-registry: $(REGISTRY_TARGETS)
$(call mkdir,$(DESTDIR)/usr/bin)
$(call cp,core/target/$(RUST_ARCH)-unknown-linux-musl/$(PROFILE)/registrybox,$(DESTDIR)/usr/bin/start-registrybox)
$(call ln,/usr/bin/start-registrybox,$(DESTDIR)/usr/bin/start-registryd)
$(call ln,/usr/bin/start-registrybox,$(DESTDIR)/usr/bin/start-registry)
$(call mkdir,$(DESTDIR)/lib/systemd/system)
$(call cp,core/startos/start-registryd.service,$(DESTDIR)/lib/systemd/system/start-registryd.service)
core/target/$(RUST_ARCH)-unknown-linux-musl/$(PROFILE)/registrybox: $(CORE_SRC) $(ENVIRONMENT_FILE)
ARCH=$(ARCH) PROFILE=$(PROFILE) ./core/build-registrybox.sh
tunnel: core/target/$(RUST_ARCH)-unknown-linux-musl/$(PROFILE)/tunnelbox
install-tunnel: core/target/$(RUST_ARCH)-unknown-linux-musl/$(PROFILE)/tunnelbox core/startos/start-tunneld.service
$(call mkdir,$(DESTDIR)/usr/bin)
$(call cp,core/target/$(RUST_ARCH)-unknown-linux-musl/$(PROFILE)/tunnelbox,$(DESTDIR)/usr/bin/start-tunnelbox)
$(call ln,/usr/bin/start-tunnelbox,$(DESTDIR)/usr/bin/start-tunneld)
$(call ln,/usr/bin/start-tunnelbox,$(DESTDIR)/usr/bin/start-tunnel)
$(call mkdir,$(DESTDIR)/lib/systemd/system)
$(call cp,core/startos/start-tunneld.service,$(DESTDIR)/lib/systemd/system/start-tunneld.service)
$(call mkdir,$(DESTDIR)/usr/lib/startos/scripts)
$(call cp,build/lib/scripts/forward-port,$(DESTDIR)/usr/lib/startos/scripts/forward-port)
core/target/$(RUST_ARCH)-unknown-linux-musl/$(PROFILE)/tunnelbox: $(CORE_SRC) $(ENVIRONMENT_FILE) $(GIT_HASH_FILE) web/dist/static/start-tunnel/index.html
ARCH=$(ARCH) PROFILE=$(PROFILE) ./core/build-tunnelbox.sh
deb: results/$(BASENAME).deb deb: results/$(BASENAME).deb
debian/control: build/lib/depends build/lib/conflicts results/$(BASENAME).deb: dpkg-build.sh $(call ls-files,debian/startos) $(STARTOS_TARGETS)
./debuild/control.sh PLATFORM=$(PLATFORM) REQUIRES=debian ./build/os-compat/run-compat.sh ./dpkg-build.sh
results/$(BASENAME).deb: dpkg-build.sh $(DEBIAN_SRC) $(ALL_TARGETS) registry-deb: results/$(REGISTRY_BASENAME).deb
PLATFORM=$(PLATFORM) ./dpkg-build.sh
results/$(REGISTRY_BASENAME).deb: dpkg-build.sh $(call ls-files,debian/start-registry) $(REGISTRY_TARGETS)
PROJECT=start-registry PLATFORM=$(ARCH) REQUIRES=debian ./build/os-compat/run-compat.sh ./dpkg-build.sh
tunnel-deb: results/$(TUNNEL_BASENAME).deb
results/$(TUNNEL_BASENAME).deb: dpkg-build.sh $(call ls-files,debian/start-tunnel) $(TUNNEL_TARGETS) build/lib/scripts/forward-port
PROJECT=start-tunnel PLATFORM=$(ARCH) REQUIRES=debian DEPENDS=wireguard-tools,iptables,conntrack ./build/os-compat/run-compat.sh ./dpkg-build.sh
$(IMAGE_TYPE): results/$(BASENAME).$(IMAGE_TYPE) $(IMAGE_TYPE): results/$(BASENAME).$(IMAGE_TYPE)
@@ -126,16 +172,20 @@ results/$(BASENAME).$(IMAGE_TYPE) results/$(BASENAME).squashfs: $(IMAGE_RECIPE_S
./image-recipe/run-local-build.sh "results/$(BASENAME).deb" ./image-recipe/run-local-build.sh "results/$(BASENAME).deb"
# For creating os images. DO NOT USE # For creating os images. DO NOT USE
install: $(ALL_TARGETS) install: $(STARTOS_TARGETS)
$(call mkdir,$(DESTDIR)/usr/bin) $(call mkdir,$(DESTDIR)/usr/bin)
$(call mkdir,$(DESTDIR)/usr/sbin) $(call mkdir,$(DESTDIR)/usr/sbin)
$(call cp,core/target/$(ARCH)-unknown-linux-musl/release/startbox,$(DESTDIR)/usr/bin/startbox) $(call cp,core/target/$(RUST_ARCH)-unknown-linux-musl/$(PROFILE)/startbox,$(DESTDIR)/usr/bin/startbox)
$(call ln,/usr/bin/startbox,$(DESTDIR)/usr/bin/startd) $(call ln,/usr/bin/startbox,$(DESTDIR)/usr/bin/startd)
$(call ln,/usr/bin/startbox,$(DESTDIR)/usr/bin/start-cli) $(call ln,/usr/bin/startbox,$(DESTDIR)/usr/bin/start-cli)
$(call ln,/usr/bin/startbox,$(DESTDIR)/usr/bin/start-sdk)
if [ "$(PLATFORM)" = "raspberrypi" ]; then $(call cp,cargo-deps/aarch64-unknown-linux-musl/release/pi-beep,$(DESTDIR)/usr/bin/pi-beep); fi if [ "$(PLATFORM)" = "raspberrypi" ]; then $(call cp,cargo-deps/aarch64-unknown-linux-musl/release/pi-beep,$(DESTDIR)/usr/bin/pi-beep); fi
if /bin/bash -c '[[ "${ENVIRONMENT}" =~ (^|-)unstable($$|-) ]]'; then $(call cp,cargo-deps/$(ARCH)-unknown-linux-musl/release/tokio-console,$(DESTDIR)/usr/bin/tokio-console); fi if /bin/bash -c '[[ "${ENVIRONMENT}" =~ (^|-)unstable($$|-) ]]'; then \
$(call cp,cargo-deps/$(ARCH)-unknown-linux-musl/release/startos-backup-fs,$(DESTDIR)/usr/bin/startos-backup-fs) $(call cp,cargo-deps/$(RUST_ARCH)-unknown-linux-musl/release/flamegraph,$(DESTDIR)/usr/bin/flamegraph); \
fi
if /bin/bash -c '[[ "${ENVIRONMENT}" =~ (^|-)console($$|-) ]]'; then \
$(call cp,cargo-deps/$(RUST_ARCH)-unknown-linux-musl/release/tokio-console,$(DESTDIR)/usr/bin/tokio-console); \
fi
$(call cp,cargo-deps/$(RUST_ARCH)-unknown-linux-musl/release/startos-backup-fs,$(DESTDIR)/usr/bin/startos-backup-fs)
$(call ln,/usr/bin/startos-backup-fs,$(DESTDIR)/usr/sbin/mount.backup-fs) $(call ln,/usr/bin/startos-backup-fs,$(DESTDIR)/usr/sbin/mount.backup-fs)
$(call mkdir,$(DESTDIR)/lib/systemd/system) $(call mkdir,$(DESTDIR)/lib/systemd/system)
@@ -152,13 +202,9 @@ install: $(ALL_TARGETS)
$(call cp,GIT_HASH.txt,$(DESTDIR)/usr/lib/startos/GIT_HASH.txt) $(call cp,GIT_HASH.txt,$(DESTDIR)/usr/lib/startos/GIT_HASH.txt)
$(call cp,VERSION.txt,$(DESTDIR)/usr/lib/startos/VERSION.txt) $(call cp,VERSION.txt,$(DESTDIR)/usr/lib/startos/VERSION.txt)
$(call mkdir,$(DESTDIR)/usr/lib/startos/system-images)
$(call cp,system-images/compat/docker-images/$(ARCH).tar,$(DESTDIR)/usr/lib/startos/system-images/compat.tar)
$(call cp,system-images/utils/docker-images/$(ARCH).tar,$(DESTDIR)/usr/lib/startos/system-images/utils.tar)
$(call cp,firmware/$(PLATFORM),$(DESTDIR)/usr/lib/startos/firmware) $(call cp,firmware/$(PLATFORM),$(DESTDIR)/usr/lib/startos/firmware)
update-overlay: $(ALL_TARGETS) update-overlay: $(STARTOS_TARGETS)
@echo "\033[33m!!! THIS WILL ONLY REFLASH YOUR DEVICE IN MEMORY !!!\033[0m" @echo "\033[33m!!! THIS WILL ONLY REFLASH YOUR DEVICE IN MEMORY !!!\033[0m"
@echo "\033[33mALL CHANGES WILL BE REVERTED IF YOU RESTART THE DEVICE\033[0m" @echo "\033[33mALL CHANGES WILL BE REVERTED IF YOU RESTART THE DEVICE\033[0m"
@if [ -z "$(REMOTE)" ]; then >&2 echo "Must specify REMOTE" && false; fi @if [ -z "$(REMOTE)" ]; then >&2 echo "Must specify REMOTE" && false; fi
@@ -167,10 +213,10 @@ update-overlay: $(ALL_TARGETS)
$(MAKE) install REMOTE=$(REMOTE) SSHPASS=$(SSHPASS) PLATFORM=$(PLATFORM) $(MAKE) install REMOTE=$(REMOTE) SSHPASS=$(SSHPASS) PLATFORM=$(PLATFORM)
$(call ssh,"sudo systemctl start startd") $(call ssh,"sudo systemctl start startd")
wormhole: core/target/$(ARCH)-unknown-linux-musl/release/startbox wormhole: core/target/$(RUST_ARCH)-unknown-linux-musl/$(PROFILE)/startbox
@echo "Paste the following command into the shell of your StartOS server:" @echo "Paste the following command into the shell of your StartOS server:"
@echo @echo
@wormhole send core/target/$(ARCH)-unknown-linux-musl/release/startbox 2>&1 | awk -Winteractive '/wormhole receive/ { printf "sudo /usr/lib/startos/scripts/chroot-and-upgrade \"cd /usr/bin && rm startbox && wormhole receive --accept-file %s && chmod +x startbox\"\n", $$3 }' @wormhole send core/target/$(RUST_ARCH)-unknown-linux-musl/$(PROFILE)/startbox 2>&1 | awk -Winteractive '/wormhole receive/ { printf "sudo /usr/lib/startos/scripts/chroot-and-upgrade \"cd /usr/bin && rm startbox && wormhole receive --accept-file %s && chmod +x startbox\"\n", $$3 }'
wormhole-deb: results/$(BASENAME).deb wormhole-deb: results/$(BASENAME).deb
@echo "Paste the following command into the shell of your StartOS server:" @echo "Paste the following command into the shell of your StartOS server:"
@@ -182,18 +228,18 @@ wormhole-squashfs: results/$(BASENAME).squashfs
$(eval SQFS_SIZE := $(shell du -s --bytes results/$(BASENAME).squashfs | awk '{print $$1}')) $(eval SQFS_SIZE := $(shell du -s --bytes results/$(BASENAME).squashfs | awk '{print $$1}'))
@echo "Paste the following command into the shell of your StartOS server:" @echo "Paste the following command into the shell of your StartOS server:"
@echo @echo
@wormhole send results/$(BASENAME).squashfs 2>&1 | awk -Winteractive '/wormhole receive/ { printf "sudo sh -c '"'"'/usr/lib/startos/scripts/prune-images $(SQFS_SIZE) && cd /media/startos/images && wormhole receive --accept-file %s && mv $(BASENAME).squashfs $(SQFS_SUM).rootfs && ln -rsf ./$(SQFS_SUM).rootfs ../config/current.rootfs && sync && reboot'"'"'\n", $$3 }' @wormhole send results/$(BASENAME).squashfs 2>&1 | awk -Winteractive '/wormhole receive/ { printf "sudo sh -c '"'"'/usr/lib/startos/scripts/prune-images $(SQFS_SIZE) && /usr/lib/startos/scripts/prune-boot && cd /media/startos/images && wormhole receive --accept-file %s && CHECKSUM=$(SQFS_SUM) /usr/lib/startos/scripts/upgrade ./$(BASENAME).squashfs'"'"'\n", $$3 }'
update: $(ALL_TARGETS) update: $(STARTOS_TARGETS)
@if [ -z "$(REMOTE)" ]; then >&2 echo "Must specify REMOTE" && false; fi @if [ -z "$(REMOTE)" ]; then >&2 echo "Must specify REMOTE" && false; fi
$(call ssh,'sudo /usr/lib/startos/scripts/chroot-and-upgrade --create') $(call ssh,'sudo /usr/lib/startos/scripts/chroot-and-upgrade --create')
$(MAKE) install REMOTE=$(REMOTE) SSHPASS=$(SSHPASS) DESTDIR=/media/startos/next PLATFORM=$(PLATFORM) $(MAKE) install REMOTE=$(REMOTE) SSHPASS=$(SSHPASS) DESTDIR=/media/startos/next PLATFORM=$(PLATFORM)
$(call ssh,'sudo /media/startos/next/usr/lib/startos/scripts/chroot-and-upgrade --no-sync "apt-get install -y $(shell cat ./build/lib/depends)"') $(call ssh,'sudo /media/startos/next/usr/lib/startos/scripts/chroot-and-upgrade --no-sync "apt-get install -y $(shell cat ./build/lib/depends)"')
update-startbox: core/target/$(ARCH)-unknown-linux-musl/release/startbox # only update binary (faster than full update) update-startbox: core/target/$(RUST_ARCH)-unknown-linux-musl/$(PROFILE)/startbox # only update binary (faster than full update)
@if [ -z "$(REMOTE)" ]; then >&2 echo "Must specify REMOTE" && false; fi @if [ -z "$(REMOTE)" ]; then >&2 echo "Must specify REMOTE" && false; fi
$(call ssh,'sudo /usr/lib/startos/scripts/chroot-and-upgrade --create') $(call ssh,'sudo /usr/lib/startos/scripts/chroot-and-upgrade --create')
$(call cp,core/target/$(ARCH)-unknown-linux-musl/release/startbox,/media/startos/next/usr/bin/startbox) $(call cp,core/target/$(RUST_ARCH)-unknown-linux-musl/$(PROFILE)/startbox,/media/startos/next/usr/bin/startbox)
$(call ssh,'sudo /media/startos/next/usr/lib/startos/scripts/chroot-and-upgrade --no-sync true') $(call ssh,'sudo /media/startos/next/usr/lib/startos/scripts/chroot-and-upgrade --no-sync true')
update-deb: results/$(BASENAME).deb # better than update, but only available from debian update-deb: results/$(BASENAME).deb # better than update, but only available from debian
@@ -208,11 +254,11 @@ update-squashfs: results/$(BASENAME).squashfs
$(eval SQFS_SUM := $(shell b3sum results/$(BASENAME).squashfs)) $(eval SQFS_SUM := $(shell b3sum results/$(BASENAME).squashfs))
$(eval SQFS_SIZE := $(shell du -s --bytes results/$(BASENAME).squashfs | awk '{print $$1}')) $(eval SQFS_SIZE := $(shell du -s --bytes results/$(BASENAME).squashfs | awk '{print $$1}'))
$(call ssh,'/usr/lib/startos/scripts/prune-images $(SQFS_SIZE)') $(call ssh,'/usr/lib/startos/scripts/prune-images $(SQFS_SIZE)')
$(call cp,results/$(BASENAME).squashfs,/media/startos/images/$(SQFS_SUM).rootfs) $(call ssh,'/usr/lib/startos/scripts/prune-boot')
$(call ssh,'sudo ln -rsf /media/startos/images/$(SQFS_SUM).rootfs /media/startos/config/current.rootfs') $(call cp,results/$(BASENAME).squashfs,/media/startos/images/next.rootfs)
$(call ssh,'sudo reboot') $(call ssh,'sudo CHECKSUM=$(SQFS_SUM) /usr/lib/startos/scripts/upgrade /media/startos/images/next.rootfs')
emulate-reflash: $(ALL_TARGETS) emulate-reflash: $(STARTOS_TARGETS)
@if [ -z "$(REMOTE)" ]; then >&2 echo "Must specify REMOTE" && false; fi @if [ -z "$(REMOTE)" ]; then >&2 echo "Must specify REMOTE" && false; fi
$(call ssh,'sudo /usr/lib/startos/scripts/chroot-and-upgrade --create') $(call ssh,'sudo /usr/lib/startos/scripts/chroot-and-upgrade --create')
$(MAKE) install REMOTE=$(REMOTE) SSHPASS=$(SSHPASS) DESTDIR=/media/startos/next PLATFORM=$(PLATFORM) $(MAKE) install REMOTE=$(REMOTE) SSHPASS=$(SSHPASS) DESTDIR=/media/startos/next PLATFORM=$(PLATFORM)
@@ -222,65 +268,63 @@ emulate-reflash: $(ALL_TARGETS)
upload-ota: results/$(BASENAME).squashfs upload-ota: results/$(BASENAME).squashfs
TARGET=$(TARGET) KEY=$(KEY) ./upload-ota.sh TARGET=$(TARGET) KEY=$(KEY) ./upload-ota.sh
container-runtime/debian.$(ARCH).squashfs: container-runtime/debian.$(ARCH).squashfs: ./container-runtime/download-base-image.sh
ARCH=$(ARCH) ./container-runtime/download-base-image.sh ARCH=$(ARCH) ./container-runtime/download-base-image.sh
container-runtime/node_modules/.package-lock.json: container-runtime/package.json container-runtime/package-lock.json sdk/dist/package.json container-runtime/package-lock.json: sdk/dist/package.json
npm --prefix container-runtime i
touch container-runtime/package-lock.json
container-runtime/node_modules/.package-lock.json: container-runtime/package-lock.json
npm --prefix container-runtime ci npm --prefix container-runtime ci
touch container-runtime/node_modules/.package-lock.json touch container-runtime/node_modules/.package-lock.json
sdk/base/lib/osBindings/index.ts: $(shell if [ "$(REBUILD_TYPES)" -ne 0 ]; then echo core/startos/bindings/index.ts; fi) ts-bindings: core/startos/bindings/index.ts
mkdir -p sdk/base/lib/osBindings mkdir -p sdk/base/lib/osBindings
rsync -ac --delete core/startos/bindings/ sdk/base/lib/osBindings/ rsync -ac --delete core/startos/bindings/ sdk/base/lib/osBindings/
touch sdk/base/lib/osBindings/index.ts
core/startos/bindings/index.ts: $(shell git ls-files core) $(ENVIRONMENT_FILE) core/startos/bindings/index.ts: $(call ls-files, core) $(ENVIRONMENT_FILE)
rm -rf core/startos/bindings rm -rf core/startos/bindings
./core/build-ts.sh ./core/build-ts.sh
ls core/startos/bindings/*.ts | sed 's/core\/startos\/bindings\/\([^.]*\)\.ts/export { \1 } from ".\/\1";/g' | grep -v '"./index"' | tee core/startos/bindings/index.ts ls core/startos/bindings/*.ts | sed 's/core\/startos\/bindings\/\([^.]*\)\.ts/export { \1 } from ".\/\1";/g' | grep -v '"./index"' | tee core/startos/bindings/index.ts
npm --prefix sdk exec -- prettier --config ./sdk/base/package.json -w ./core/startos/bindings/*.ts npm --prefix sdk exec -- prettier --config ./sdk/base/package.json -w ./core/startos/bindings/*.ts
touch core/startos/bindings/index.ts touch core/startos/bindings/index.ts
sdk/dist/package.json sdk/baseDist/package.json: $(shell git ls-files sdk) sdk/base/lib/osBindings/index.ts sdk/dist/package.json sdk/baseDist/package.json: $(call ls-files, sdk) sdk/base/lib/osBindings/index.ts
(cd sdk && make bundle) (cd sdk && make bundle)
touch sdk/dist/package.json touch sdk/dist/package.json
touch sdk/baseDist/package.json touch sdk/baseDist/package.json
# TODO: make container-runtime its own makefile? # TODO: make container-runtime its own makefile?
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 container-runtime/dist/index.js: container-runtime/node_modules/.package-lock.json $(call ls-files, container-runtime/src) container-runtime/package.json container-runtime/tsconfig.json
npm --prefix container-runtime run build npm --prefix container-runtime run build
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/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 ./container-runtime/install-dist-deps.sh
touch container-runtime/dist/node_modules/.package-lock.json 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/.package-lock.json 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/$(RUST_ARCH)-unknown-linux-musl/release/start-container
ARCH=$(ARCH) ./container-runtime/update-image.sh ARCH=$(ARCH) REQUIRES=qemu ./build/os-compat/run-compat.sh ./container-runtime/update-image.sh
build/lib/depends build/lib/conflicts: build/dpkg-deps/* build/lib/depends build/lib/conflicts: $(ENVIRONMENT_FILE) $(PLATFORM_FILE) $(shell ls build/dpkg-deps/*)
build/dpkg-deps/generate.sh PLATFORM=$(PLATFORM) ARCH=$(ARCH) build/dpkg-deps/generate.sh
$(FIRMWARE_ROMS): build/lib/firmware.json download-firmware.sh $(PLATFORM_FILE) $(FIRMWARE_ROMS): build/lib/firmware.json download-firmware.sh $(PLATFORM_FILE)
./download-firmware.sh $(PLATFORM) ./download-firmware.sh $(PLATFORM)
system-images/compat/docker-images/$(ARCH).tar: $(COMPAT_SRC) core/target/$(RUST_ARCH)-unknown-linux-musl/$(PROFILE)/startbox: $(CORE_SRC) $(COMPRESSED_WEB_UIS) web/patchdb-ui-seed.json $(ENVIRONMENT_FILE)
cd system-images/compat && make docker-images/$(ARCH).tar && touch docker-images/$(ARCH).tar ARCH=$(ARCH) PROFILE=$(PROFILE) ./core/build-startbox.sh
touch core/target/$(RUST_ARCH)-unknown-linux-musl/$(PROFILE)/startbox
system-images/utils/docker-images/$(ARCH).tar: $(UTILS_SRC) core/target/$(RUST_ARCH)-unknown-linux-musl/release/start-container: $(CORE_SRC) $(ENVIRONMENT_FILE)
cd system-images/utils && make docker-images/$(ARCH).tar && touch docker-images/$(ARCH).tar ARCH=$(ARCH) ./core/build-start-container.sh
touch core/target/$(RUST_ARCH)-unknown-linux-musl/release/start-container
system-images/binfmt/docker-images/$(ARCH).tar: $(BINFMT_SRC) web/package-lock.json: web/package.json sdk/baseDist/package.json
cd system-images/binfmt && make docker-images/$(ARCH).tar && touch docker-images/$(ARCH).tar npm --prefix web i
touch web/package-lock.json
core/target/$(ARCH)-unknown-linux-musl/release/startbox: $(CORE_SRC) $(COMPRESSED_WEB_UIS) web/patchdb-ui-seed.json $(ENVIRONMENT_FILE) web/node_modules/.package-lock.json: web/package-lock.json
ARCH=$(ARCH) ./core/build-startbox.sh
touch core/target/$(ARCH)-unknown-linux-musl/release/startbox
core/target/$(ARCH)-unknown-linux-musl/release/containerbox: $(CORE_SRC) $(ENVIRONMENT_FILE)
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/baseDist/package.json
npm --prefix web ci npm --prefix web ci
touch web/node_modules/.package-lock.json touch web/node_modules/.package-lock.json
@@ -298,11 +342,15 @@ web/dist/raw/setup-wizard/index.html: $(WEB_SETUP_WIZARD_SRC) $(WEB_SHARED_SRC)
touch web/dist/raw/setup-wizard/index.html touch web/dist/raw/setup-wizard/index.html
web/dist/raw/install-wizard/index.html: $(WEB_INSTALL_WIZARD_SRC) $(WEB_SHARED_SRC) web/.angular/.updated web/dist/raw/install-wizard/index.html: $(WEB_INSTALL_WIZARD_SRC) $(WEB_SHARED_SRC) web/.angular/.updated
npm --prefix web run build:install-wiz npm --prefix web run build:install
touch web/dist/raw/install-wizard/index.html touch web/dist/raw/install-wizard/index.html
$(COMPRESSED_WEB_UIS): $(WEB_UIS) $(ENVIRONMENT_FILE) web/dist/raw/start-tunnel/index.html: $(WEB_START_TUNNEL_SRC) $(WEB_SHARED_SRC) web/.angular/.updated
./compress-uis.sh npm --prefix web run build:tunnel
touch web/dist/raw/start-tunnel/index.html
web/dist/static/%/index.html: web/dist/raw/%/index.html
./compress-uis.sh $*
web/config.json: $(GIT_HASH_FILE) web/config-sample.json web/config.json: $(GIT_HASH_FILE) web/config-sample.json
jq '.useMocks = false' web/config-sample.json | jq '.gitHash = "$(shell cat GIT_HASH.txt)"' > web/config.json jq '.useMocks = false' web/config-sample.json | jq '.gitHash = "$(shell cat GIT_HASH.txt)"' > web/config.json
@@ -326,11 +374,17 @@ uis: $(WEB_UIS)
# this is a convenience step to build the UI # this is a convenience step to build the UI
ui: web/dist/raw/ui ui: web/dist/raw/ui
cargo-deps/aarch64-unknown-linux-musl/release/pi-beep: cargo-deps/aarch64-unknown-linux-musl/release/pi-beep: ./build-cargo-dep.sh
ARCH=aarch64 ./build-cargo-dep.sh pi-beep ARCH=aarch64 ./build-cargo-dep.sh pi-beep
cargo-deps/$(ARCH)-unknown-linux-musl/release/tokio-console: cargo-deps/$(RUST_ARCH)-unknown-linux-musl/release/tokio-console: ./build-cargo-dep.sh
ARCH=$(ARCH) PREINSTALL="apk add musl-dev pkgconfig" ./build-cargo-dep.sh tokio-console ARCH=$(ARCH) ./build-cargo-dep.sh tokio-console
touch $@
cargo-deps/$(ARCH)-unknown-linux-musl/release/startos-backup-fs: cargo-deps/$(RUST_ARCH)-unknown-linux-musl/release/startos-backup-fs: ./build-cargo-dep.sh
ARCH=$(ARCH) PREINSTALL="apk add fuse3 fuse3-dev fuse3-static musl-dev pkgconfig" ./build-cargo-dep.sh --git https://github.com/Start9Labs/start-fs.git startos-backup-fs ARCH=$(ARCH) ./build-cargo-dep.sh --git https://github.com/Start9Labs/start-fs.git startos-backup-fs
touch $@
cargo-deps/$(RUST_ARCH)-unknown-linux-musl/release/flamegraph: ./build-cargo-dep.sh
ARCH=$(ARCH) ./build-cargo-dep.sh flamegraph
touch $@

View File

@@ -13,9 +13,6 @@
<a href="https://twitter.com/start9labs"> <a href="https://twitter.com/start9labs">
<img alt="X (formerly Twitter) Follow" src="https://img.shields.io/twitter/follow/start9labs"> <img alt="X (formerly Twitter) Follow" src="https://img.shields.io/twitter/follow/start9labs">
</a> </a>
<a href="https://mastodon.start9labs.com">
<img src="https://img.shields.io/mastodon/follow/000000001?domain=https%3A%2F%2Fmastodon.start9labs.com&label=Follow&style=social">
</a>
<a href="https://matrix.to/#/#community:matrix.start9labs.com"> <a href="https://matrix.to/#/#community:matrix.start9labs.com">
<img alt="Static Badge" src="https://img.shields.io/badge/community-matrix-yellow?logo=matrix"> <img alt="Static Badge" src="https://img.shields.io/badge/community-matrix-yellow?logo=matrix">
</a> </a>

95
START-TUNNEL.md Normal file
View File

@@ -0,0 +1,95 @@
# StartTunnel
A self-hosted WireGuard VPN optimized for creating VLANs and reverse tunneling to personal servers.
You can think of StartTunnel as "virtual router in the cloud".
Use it for private remote access to self-hosted services running on a personal server, or to expose self-hosted services to the public Internet without revealing the host server's IP address.
## Features
- **Create Subnets**: Each subnet creates a private, virtual local area network (VLAN), similar to the LAN created by a home router.
- **Add Devices**: When you add a device (server, phone, laptop) to a subnet, it receives a LAN IP address on that subnet as well as a unique WireGuard config that must be copied, downloaded, or scanned into the device.
- **Forward Ports**: Forwarding a port creates a "reverse tunnel", exposing a specific port on a specific device to the public Internet.
## Installation
1. Rent a low cost VPS. For most use cases, the cheapest option should be enough.
- It must have a dedicated public IP address.
- For compute (CPU), memory (RAM), and storage (disk), choose the minimum spec.
- For transfer (bandwidth), it depends on (1) your use case and (2) your home Internet's _upload_ speed. Even if you intend to serve large files or stream content from your server, there is no reason to pay for speeds that exceed your home Internet's upload speed.
1. Provision the VPS with the latest version of Debian.
1. Access the VPS via SSH.
1. Run the StartTunnel install script:
curl -fsSL https://start9labs.github.io/start-tunnel | sh
1. [Initialize the web interface](#web-interface) (recommended)
## Updating
Simply re-run the install command:
```sh
curl -fsSL https://start9labs.github.io/start-tunnel | sh
```
## CLI
By default, StartTunnel is managed via the `start-tunnel` command line interface, which is self-documented.
```
start-tunnel --help
```
## Web Interface
Enable the web interface (recommended in most cases) to access your StartTunnel from the browser or via API.
1. Initialize the web interface.
start-tunnel web init
1. If your VPS has multiple public IP addresses, you will be prompted to select the IP address at which to host the web interface.
1. When prompted, enter the port at which to host the web interface. The default is 8443, and we recommend using it. If you change the default, choose an uncommon port to avoid future conflicts.
1. To access your StartTunnel web interface securely over HTTPS, you need an SSL certificate. When prompted, select whether to autogenerate a certificate or provide your own. _This is only for accessing your StartTunnel web interface_.
1. You will receive a success message with 3 pieces of information:
- **<https://IP:port>**: the URL where you can reach your personal web interface.
- **Password**: an autogenerated password for your interface. If you lose/forget it, you can reset it using the start-tunnel CLI.
- **Root Certificate Authority**: the Root CA of your StartTunnel instance.
1. If you autogenerated your SSL certificate, visiting the `https://IP:port` URL in the browser will warn you that the website is insecure. This is expected. You have two options for getting past this warning:
- option 1 (recommended): [Trust your StartTunnel Root CA on your connecting device](#trusting-your-starttunnel-root-ca).
- Option 2: bypass the warning in the browser, creating a one-time security exception.
### Trusting your StartTunnel Root CA
1. Copy the contents of your Root CA (starting with -----BEGIN CERTIFICATE----- and ending with -----END CERTIFICATE-----).
2. Open a text editor:
- Linux: gedit, nano, or any editor
- Mac: TextEdit
- Windows: Notepad
3. Paste the contents of your Root CA.
4. Save the file with a `.crt` extension (e.g. `start-tunnel.crt`) (make sure it saves as plain text, not rich text).
5. Trust the Root CA on your client device(s):
- [Linux](https://staging.docs.start9.com/device-guides/linux/ca.html)
- [Mac](https://staging.docs.start9.com/device-guides/mac/ca.html)
- [Windows](https://staging.docs.start9.com/device-guides/windows/ca.html)
- [Android/Graphene](https://staging.docs.start9.com/device-guides/android/ca.html)
- [iOS](https://staging.docs.start9.com/device-guides/ios/ca.html)

201
agents/VERSION_BUMP.md Normal file
View File

@@ -0,0 +1,201 @@
# StartOS Version Bump Guide
This document explains how to bump the StartOS version across the entire codebase.
## Overview
When bumping from version `X.Y.Z-alpha.N` to `X.Y.Z-alpha.N+1`, you need to update files in multiple locations across the repository. The `// VERSION_BUMP` comment markers indicate where changes are needed.
## Files to Update
### 1. Core Rust Crate Version
**File: `core/startos/Cargo.toml`**
Update the version string (line ~18):
```toml
version = "0.4.0-alpha.15" # VERSION_BUMP
```
**File: `core/Cargo.lock`**
This file is auto-generated. After updating `Cargo.toml`, run:
```bash
cd core
cargo check
```
This will update the version in `Cargo.lock` automatically.
### 2. Create New Version Migration Module
**File: `core/startos/src/version/vX_Y_Z_alpha_N+1.rs`**
Create a new version file by copying the previous version and updating:
```rust
use exver::{PreReleaseSegment, VersionRange};
use super::v0_3_5::V0_3_0_COMPAT;
use super::{VersionT, v0_4_0_alpha_14}; // Update to previous version
use crate::prelude::*;
lazy_static::lazy_static! {
static ref V0_4_0_alpha_15: exver::Version = exver::Version::new(
[0, 4, 0],
[PreReleaseSegment::String("alpha".into()), 15.into()] // Update number
);
}
#[derive(Clone, Copy, Debug, Default)]
pub struct Version;
impl VersionT for Version {
type Previous = v0_4_0_alpha_14::Version; // Update to previous version
type PreUpRes = ();
async fn pre_up(self) -> Result<Self::PreUpRes, Error> {
Ok(())
}
fn semver(self) -> exver::Version {
V0_4_0_alpha_15.clone() // Update version name
}
fn compat(self) -> &'static VersionRange {
&V0_3_0_COMPAT
}
#[instrument(skip_all)]
fn up(self, _db: &mut Value, _: Self::PreUpRes) -> Result<Value, Error> {
// Add migration logic here if needed
Ok(Value::Null)
}
fn down(self, _db: &mut Value) -> Result<(), Error> {
// Add rollback logic here if needed
Ok(())
}
}
```
### 3. Update Version Module Registry
**File: `core/startos/src/version/mod.rs`**
Make changes in **5 locations**:
#### Location 1: Module Declaration (~line 57)
Add the new module after the previous version:
```rust
mod v0_4_0_alpha_14;
mod v0_4_0_alpha_15; // Add this
```
#### Location 2: Current Type Alias (~line 59)
Update the `Current` type and move the `// VERSION_BUMP` comment:
```rust
pub type Current = v0_4_0_alpha_15::Version; // VERSION_BUMP
```
#### Location 3: Version Enum (~line 175)
Remove `// VERSION_BUMP` from the previous version, add new variant, add comment:
```rust
V0_4_0_alpha_14(Wrapper<v0_4_0_alpha_14::Version>),
V0_4_0_alpha_15(Wrapper<v0_4_0_alpha_15::Version>), // VERSION_BUMP
Other(exver::Version),
```
#### Location 4: as_version_t() Match (~line 233)
Remove `// VERSION_BUMP`, add new match arm, add comment:
```rust
Self::V0_4_0_alpha_14(v) => DynVersion(Box::new(v.0)),
Self::V0_4_0_alpha_15(v) => DynVersion(Box::new(v.0)), // VERSION_BUMP
Self::Other(v) => {
```
#### Location 5: as_exver() Match (~line 284, inside #[cfg(test)])
Remove `// VERSION_BUMP`, add new match arm, add comment:
```rust
Version::V0_4_0_alpha_14(Wrapper(x)) => x.semver(),
Version::V0_4_0_alpha_15(Wrapper(x)) => x.semver(), // VERSION_BUMP
Version::Other(x) => x.clone(),
```
### 4. SDK TypeScript Version
**File: `sdk/package/lib/StartSdk.ts`**
Update the OSVersion constant (~line 64):
```typescript
export const OSVersion = testTypeVersion("0.4.0-alpha.15");
```
### 5. Web UI Package Version
**File: `web/package.json`**
Update the version field:
```json
{
"name": "startos-ui",
"version": "0.4.0-alpha.15",
...
}
```
**File: `web/package-lock.json`**
This file is auto-generated, but it's faster to update manually. Find all instances of "startos-ui" and update the version field.
## Verification Step
```
make
```
## VERSION_BUMP Comment Pattern
The `// VERSION_BUMP` comment serves as a marker for where to make changes next time:
- Always **remove** it from the old location
- **Add** the new version entry
- **Move** the comment to mark the new location
This pattern helps you quickly find all the places that need updating in the next version bump.
## Summary Checklist
- [ ] Update `core/startos/Cargo.toml` version
- [ ] Create new `core/startos/src/version/vX_Y_Z_alpha_N+1.rs` file
- [ ] Update `core/startos/src/version/mod.rs` in 5 locations
- [ ] Run `cargo check` to update `core/Cargo.lock`
- [ ] Update `sdk/package/lib/StartSdk.ts` OSVersion
- [ ] Update `web/package.json` and `web/package-lock.json` version
- [ ] Verify all changes compile/build successfully
## Migration Logic
The `up()` and `down()` methods in the version file handle database migrations:
- **up()**: Migrates the database from the previous version to this version
- **down()**: Rolls back from this version to the previous version
- **pre_up()**: Runs before migration, useful for pre-migration checks or data gathering
If no migration is needed, return `Ok(Value::Null)` for `up()` and `Ok(())` for `down()`.
For complex migrations, you may need to:
1. Update `type PreUpRes` to pass data between `pre_up()` and `up()`
2. Implement database transformations in the `up()` method
3. Implement reverse transformations in `down()` for rollback support

View File

@@ -1,5 +1,7 @@
#!/bin/bash #!/bin/bash
PROJECT=${PROJECT:-"startos"}
cd "$(dirname "${BASH_SOURCE[0]}")" cd "$(dirname "${BASH_SOURCE[0]}")"
PLATFORM="$(if [ -f ./PLATFORM.txt ]; then cat ./PLATFORM.txt; else echo unknown; fi)" PLATFORM="$(if [ -f ./PLATFORM.txt ]; then cat ./PLATFORM.txt; else echo unknown; fi)"
@@ -16,4 +18,4 @@ if [ -n "$STARTOS_ENV" ]; then
VERSION_FULL="$VERSION_FULL~${STARTOS_ENV}" VERSION_FULL="$VERSION_FULL~${STARTOS_ENV}"
fi fi
echo -n "startos-${VERSION_FULL}_${PLATFORM}" echo -n "${PROJECT}-${VERSION_FULL}_${PLATFORM}"

View File

@@ -8,27 +8,22 @@ if [ "$0" != "./build-cargo-dep.sh" ]; then
exit 1 exit 1
fi fi
USE_TTY=
if tty -s; then
USE_TTY="-it"
fi
if [ -z "$ARCH" ]; then if [ -z "$ARCH" ]; then
ARCH=$(uname -m) ARCH=$(uname -m)
fi fi
DOCKER_PLATFORM="linux/${ARCH}" RUST_ARCH="$ARCH"
if [ "$ARCH" = aarch64 ] || [ "$ARCH" = arm64 ]; then if [ "$ARCH" = "riscv64" ]; then
DOCKER_PLATFORM="linux/arm64" RUST_ARCH="riscv64gc"
elif [ "$ARCH" = x86_64 ]; then
DOCKER_PLATFORM="linux/amd64"
fi fi
mkdir -p cargo-deps mkdir -p cargo-deps
alias 'rust-musl-builder'='docker run $USE_TTY --platform=${DOCKER_PLATFORM} --rm -e "RUSTFLAGS=$RUSTFLAGS" -v "$HOME/.cargo/registry":/root/.cargo/registry -v "$(pwd)"/cargo-deps:/home/rust/src -w /home/rust/src -P rust:alpine'
PREINSTALL=${PREINSTALL:-true} source core/builder-alias.sh
rust-musl-builder sh -c "$PREINSTALL && cargo install $* --target-dir /home/rust/src --target=$ARCH-unknown-linux-musl" RUSTFLAGS="-C target-feature=+crt-static"
sudo chown -R $USER cargo-deps
sudo chown -R $USER ~/.cargo rust-zig-builder cargo-zigbuild install $* --target-dir /workdir/cargo-deps/ --target=$RUST_ARCH-unknown-linux-musl
if [ "$(ls -nd "cargo-deps/$RUST_ARCH-unknown-linux-musl/release/${!#}" | awk '{ print $3 }')" != "$UID" ]; then
rust-zig-builder sh -c "chown -R $UID:$UID cargo-deps && chown -R $UID:$UID /usr/local/cargo"
fi

View File

@@ -7,9 +7,9 @@ bmon
btrfs-progs btrfs-progs
ca-certificates ca-certificates
cifs-utils cifs-utils
conntrack
cryptsetup cryptsetup
curl curl
dnsutils
dmidecode dmidecode
dnsutils dnsutils
dosfstools dosfstools
@@ -19,6 +19,7 @@ exfatprogs
flashrom flashrom
fuse3 fuse3
grub-common grub-common
grub-efi
htop htop
httpdirfs httpdirfs
iotop iotop
@@ -36,13 +37,14 @@ man-db
ncdu ncdu
net-tools net-tools
network-manager network-manager
nfs-common
nvme-cli nvme-cli
nyx nyx
openssh-server openssh-server
podman podman
postgresql
psmisc psmisc
qemu-guest-agent qemu-guest-agent
rfkill
rsync rsync
samba-common-bin samba-common-bin
smartmontools smartmontools

View File

@@ -5,11 +5,15 @@ set -e
cd "$(dirname "${BASH_SOURCE[0]}")" cd "$(dirname "${BASH_SOURCE[0]}")"
IFS="-" read -ra FEATURES <<< "$ENVIRONMENT" IFS="-" read -ra FEATURES <<< "$ENVIRONMENT"
FEATURES+=("${ARCH}")
if [ "$ARCH" != "$PLATFORM" ]; then
FEATURES+=("${PLATFORM}")
fi
feature_file_checker=' feature_file_checker='
/^#/ { next } /^#/ { next }
/^\+ [a-z0-9]+$/ { next } /^\+ [a-z0-9.-]+$/ { next }
/^- [a-z0-9]+$/ { next } /^- [a-z0-9.-]+$/ { next }
{ exit 1 } { exit 1 }
' '
@@ -30,8 +34,8 @@ for type in conflicts depends; do
for feature in ${FEATURES[@]}; do for feature in ${FEATURES[@]}; do
file="$feature.$type" file="$feature.$type"
if [ -f $file ]; then if [ -f $file ]; then
if grep "^- $pkg$" $file; then if grep "^- $pkg$" $file > /dev/null; then
SKIP=1 SKIP=yes
fi fi
fi fi
done done

View File

@@ -0,0 +1,10 @@
- grub-efi
+ parted
+ raspberrypi-net-mods
+ raspberrypi-sys-mods
+ raspi-config
+ raspi-firmware
+ raspi-utils
+ rpi-eeprom
+ rpi-update
+ rpi.gpio-common

View File

@@ -1,2 +1,3 @@
+ gdb + gdb
+ heaptrack + heaptrack
+ linux-perf

View File

@@ -0,0 +1 @@
+ grub-pc-bin

View File

@@ -1,34 +1,123 @@
#!/bin/sh #!/bin/sh
printf "\n"
printf "Welcome to\n"
cat << "ASCII"
███████ parse_essential_db_info() {
█ █ █ DB_DUMP="/tmp/startos_db.json"
█ █ █ █
█ █ █ █
█ █ █ █
█ █ █ █
█ █
███████
_____ __ ___ __ __ if command -v start-cli >/dev/null 2>&1; then
(_ | /\ |__) | / \(_ start-cli db dump > "$DB_DUMP" 2>/dev/null || return 1
__) | / \| \ | \__/__) else
ASCII return 1
printf " v$(cat /usr/lib/startos/VERSION.txt)\n\n" fi
printf " %s (%s %s)\n" "$(uname -o)" "$(uname -r)" "$(uname -m)"
printf " Git Hash: $(cat /usr/lib/startos/GIT_HASH.txt)" if command -v jq >/dev/null 2>&1 && [ -f "$DB_DUMP" ]; then
if [ -n "$(cat /usr/lib/startos/ENVIRONMENT.txt)" ]; then HOSTNAME=$(jq -r '.value.serverInfo.hostname // "unknown"' "$DB_DUMP" 2>/dev/null)
printf " ~ $(cat /usr/lib/startos/ENVIRONMENT.txt)\n" VERSION=$(jq -r '.value.serverInfo.version // "unknown"' "$DB_DUMP" 2>/dev/null)
else RAM_BYTES=$(jq -r '.value.serverInfo.ram // 0' "$DB_DUMP" 2>/dev/null)
printf "\n" WAN_IP=$(jq -r '.value.serverInfo.network.gateways[].ipInfo.wanIp // "unknown"' "$DB_DUMP" 2>/dev/null | head -1)
NTP_SYNCED=$(jq -r '.value.serverInfo.ntpSynced // false' "$DB_DUMP" 2>/dev/null)
if [ "$RAM_BYTES" != "0" ] && [ "$RAM_BYTES" != "null" ]; then
RAM_GB=$(echo "scale=1; $RAM_BYTES / 1073741824" | bc 2>/dev/null || echo "unknown")
else
RAM_GB="unknown"
fi
RUNNING_SERVICES=$(jq -r '[.value.packageData[] | select(.statusInfo.started != null)] | length' "$DB_DUMP" 2>/dev/null)
TOTAL_SERVICES=$(jq -r '.value.packageData | length' "$DB_DUMP" 2>/dev/null)
rm -f "$DB_DUMP"
return 0
else
rm -f "$DB_DUMP" 2>/dev/null
return 1
fi
}
DB_INFO_AVAILABLE=0
if parse_essential_db_info; then
DB_INFO_AVAILABLE=1
fi fi
printf "\n" if [ "$DB_INFO_AVAILABLE" -eq 1 ] && [ "$VERSION" != "unknown" ]; then
printf " * Documentation: https://docs.start9.com\n" version_display="v$VERSION"
printf " * Management: https://%s.local\n" "$(hostname)" else
printf " * Support: https://start9.com/contact\n" version_display="v$(cat /usr/lib/startos/VERSION.txt 2>/dev/null || echo 'unknown')"
printf " * Source Code: https://github.com/Start9Labs/start-os\n" fi
printf " * License: MIT\n"
printf "\n" printf "\n\033[1;37m ▄▄▀▀▀▀▀▄▄\033[0m\n"
printf "\033[1;37m ▄▀ ▄ ▀▄ ▄▄▄▄▄ ▄▄▄▄▄▄▄ ▄ ▄▄▄▄▄ ▄▄▄▄▄▄▄ \033[1;31m▄██████▄ ▄██████\033[0m\n"
printf "\033[1;37m █ █ █ █ █ █ █ █ █ ▀▄ █ \033[1;31m██ ██ ██ \033[0m\n"
printf "\033[1;37m█ █ █ █ ▀▄▄▄▄ █ █ █ █ ▄▄▄▀ █ \033[1;31m██ ██ ▀█████▄\033[0m\n"
printf "\033[1;37m█ █ █ █ █ █ █ █ █ ▀▄ █ \033[1;31m██ ██ ██\033[0m\n"
printf "\033[1;37m █ █ █ █ ▄▄▄▄▄▀ █ █ █ █ ▀▄ █ \033[1;31m▀██████▀ ██████▀\033[0m\n"
printf "\033[1;37m █ █\033[0m\n"
printf "\033[1;37m ▀▀▄▄▄▀▀ $version_display\033[0m\n\n"
uptime_str=$(uptime | awk -F'up ' '{print $2}' | awk -F',' '{print $1}' | sed 's/^ *//')
if [ "$DB_INFO_AVAILABLE" -eq 1 ] && [ "$RAM_GB" != "unknown" ]; then
memory_used=$(free -m | awk 'NR==2{printf "%.0fMB", $3}')
memory_display="$memory_used / ${RAM_GB}GB"
else
memory_display=$(free -m | awk 'NR==2{printf "%.0fMB / %.0fMB", $3, $2}')
fi
root_usage=$(df -h / | awk 'NR==2{printf "%s (%s free)", $5, $4}')
if [ -d "/media/startos/data/package-data" ]; then
data_usage=$(df -h /media/startos/data/package-data | awk 'NR==2{printf "%s (%s free)", $5, $4}')
else
data_usage="N/A"
fi
if [ "$DB_INFO_AVAILABLE" -eq 1 ]; then
services_text="$RUNNING_SERVICES/$TOTAL_SERVICES running"
else
services_text="Unknown"
fi
local_ip=$(ip route get 1.1.1.1 2>/dev/null | awk '{for(i=1;i<=NF;i++) if($i=="src") print $(i+1)}' | head -1)
if [ -z "$local_ip" ]; then local_ip="N/A"; fi
if [ "$DB_INFO_AVAILABLE" -eq 1 ] && [ "$WAN_IP" != "unknown" ]; then
wan_ip="$WAN_IP"
else
wan_ip="N/A"
fi
printf " \033[1;37m┌─ SYSTEM STATUS ───────────────────────────────────────────────────┐\033[0m\n"
printf " \033[1;37m│\033[0m %-8s \033[0;33m%-22s\033[0m %-8s \033[0;33m%-23s\033[0m \033[1;37m│\033[0m\n" "Uptime:" "$uptime_str" "Memory:" "$memory_display"
printf " \033[1;37m│\033[0m %-8s \033[0;33m%-22s\033[0m %-8s \033[0;33m%-23s\033[0m \033[1;37m│\033[0m\n" "Root:" "$root_usage" "Data:" "$data_usage"
if [ "$DB_INFO_AVAILABLE" -eq 1 ]; then
if [ "$RUNNING_SERVICES" -eq "$TOTAL_SERVICES" ] && [ "$TOTAL_SERVICES" -gt 0 ]; then
printf " \033[1;37m│\033[0m %-8s \033[0;32m%-22s\033[0m %-8s \033[0;33m%-23s\033[0m \033[1;37m│\033[0m\n" "Services:" "$services_text" "WAN:" "$wan_ip"
elif [ "$RUNNING_SERVICES" -gt 0 ]; then
printf " \033[1;37m│\033[0m %-8s \033[0;33m%-22s\033[0m %-8s \033[0;33m%-23s\033[0m \033[1;37m│\033[0m\n" "Services:" "$services_text" "WAN:" "$wan_ip"
else
printf " \033[1;37m│\033[0m %-8s \033[0;31m%-22s\033[0m %-8s \033[0;33m%-23s\033[0m \033[1;37m│\033[0m\n" "Services:" "$services_text" "WAN:" "$wan_ip"
fi
else
printf " \033[1;37m│\033[0m %-8s \033[0;37m%-22s\033[0m %-8s \033[0;33m%-23s\033[0m \033[1;37m│\033[0m\n" "Services:" "$services_text" "WAN:" "$wan_ip"
fi
if [ "$DB_INFO_AVAILABLE" -eq 1 ] && [ "$NTP_SYNCED" = "true" ]; then
printf " \033[1;37m│\033[0m %-8s \033[0;33m%-22s\033[0m %-8s \033[0;32m%-23s\033[0m \033[1;37m│\033[0m\n" "Local:" "$local_ip" "NTP:" "Synced"
elif [ "$DB_INFO_AVAILABLE" -eq 1 ] && [ "$NTP_SYNCED" = "false" ]; then
printf " \033[1;37m│\033[0m %-8s \033[0;33m%-22s\033[0m %-8s \033[0;31m%-23s\033[0m \033[1;37m│\033[0m\n" "Local:" "$local_ip" "NTP:" "Not Synced"
else
printf " \033[1;37m│\033[0m %-8s \033[0;33m%-22s\033[0m %-8s \033[0;37m%-23s\033[0m \033[1;37m│\033[0m\n" "Local:" "$local_ip" "NTP:" "Unknown"
fi
printf " \033[1;37m└───────────────────────────────────────────────────────────────────┘\033[0m"
if [ "$DB_INFO_AVAILABLE" -eq 1 ] && [ "$HOSTNAME" != "unknown" ]; then
web_url="https://$HOSTNAME.local"
else
web_url="https://$(hostname).local"
fi
printf "\n \033[1;37m┌──────────────────────────────────────────────────── QUICK ACCESS ─┐\033[0m\n"
printf " \033[1;37m│\033[0m Web Interface: \033[0;36m%-50s\033[0m \033[1;37m│\033[0m\n" "$web_url"
printf " \033[1;37m│\033[0m Documentation: \033[0;36m%-50s\033[0m \033[1;37m│\033[0m\n" "https://staging.docs.start9.com"
printf " \033[1;37m│\033[0m Support: \033[0;36m%-50s\033[0m \033[1;37m│\033[0m\n" "https://start9.com/contact"
printf " \033[1;37m└───────────────────────────────────────────────────────────────────┘\033[0m\n\n"

View File

@@ -1,8 +0,0 @@
#!/bin/sh
if cat /sys/class/drm/*/status | grep -qw connected; then
exit 0
else
exit 1
fi

View File

@@ -1,6 +1,6 @@
#!/bin/bash #!/bin/bash
SOURCE_DIR="$(dirname "${BASH_SOURCE[0]}")" SOURCE_DIR="$(dirname $(realpath "${BASH_SOURCE[0]}"))"
if [ "$UID" -ne 0 ]; then if [ "$UID" -ne 0 ]; then
>&2 echo 'Must be run as root' >&2 echo 'Must be run as root'
@@ -10,24 +10,24 @@ fi
POSITIONAL_ARGS=() POSITIONAL_ARGS=()
while [[ $# -gt 0 ]]; do while [[ $# -gt 0 ]]; do
case $1 in case $1 in
--no-sync) --no-sync)
NO_SYNC=1 NO_SYNC=1
shift shift
;; ;;
--create) --create)
ONLY_CREATE=1 ONLY_CREATE=1
shift shift
;; ;;
-*|--*) -*|--*)
echo "Unknown option $1" echo "Unknown option $1"
exit 1 exit 1
;; ;;
*) *)
POSITIONAL_ARGS+=("$1") # save positional arg POSITIONAL_ARGS+=("$1") # save positional arg
shift # past argument shift # past argument
;; ;;
esac esac
done done
set -- "${POSITIONAL_ARGS[@]}" # restore positional parameters set -- "${POSITIONAL_ARGS[@]}" # restore positional parameters
@@ -35,7 +35,7 @@ set -- "${POSITIONAL_ARGS[@]}" # restore positional parameters
if [ -z "$NO_SYNC" ]; then if [ -z "$NO_SYNC" ]; then
echo 'Syncing...' echo 'Syncing...'
umount -R /media/startos/next 2> /dev/null umount -R /media/startos/next 2> /dev/null
umount -R /media/startos/upper 2> /dev/null umount /media/startos/upper 2> /dev/null
rm -rf /media/startos/upper /media/startos/next rm -rf /media/startos/upper /media/startos/next
mkdir /media/startos/upper mkdir /media/startos/upper
mount -t tmpfs tmpfs /media/startos/upper mount -t tmpfs tmpfs /media/startos/upper
@@ -43,8 +43,6 @@ if [ -z "$NO_SYNC" ]; then
mount -t overlay \ mount -t overlay \
-olowerdir=/media/startos/current,upperdir=/media/startos/upper/data,workdir=/media/startos/upper/work \ -olowerdir=/media/startos/current,upperdir=/media/startos/upper/data,workdir=/media/startos/upper/work \
overlay /media/startos/next overlay /media/startos/next
mkdir -p /media/startos/next/media/startos/root
mount --bind /media/startos/root /media/startos/next/media/startos/root
fi fi
if [ -n "$ONLY_CREATE" ]; then if [ -n "$ONLY_CREATE" ]; then
@@ -56,12 +54,18 @@ mkdir -p /media/startos/next/dev
mkdir -p /media/startos/next/sys mkdir -p /media/startos/next/sys
mkdir -p /media/startos/next/proc mkdir -p /media/startos/next/proc
mkdir -p /media/startos/next/boot mkdir -p /media/startos/next/boot
mkdir -p /media/startos/next/media/startos/root
mount --bind /run /media/startos/next/run mount --bind /run /media/startos/next/run
mount --bind /tmp /media/startos/next/tmp mount --bind /tmp /media/startos/next/tmp
mount --bind /dev /media/startos/next/dev mount --bind /dev /media/startos/next/dev
mount --bind /sys /media/startos/next/sys mount --bind /sys /media/startos/next/sys
mount --bind /proc /media/startos/next/proc mount --bind /proc /media/startos/next/proc
mount --bind /boot /media/startos/next/boot mount --bind /boot /media/startos/next/boot
mount --bind /media/startos/root /media/startos/next/media/startos/root
if mountpoint /sys/firmware/efi/efivars 2> /dev/null; then
mount --bind /sys/firmware/efi/efivars /media/startos/next/sys/firmware/efi/efivars
fi
if [ -z "$*" ]; then if [ -z "$*" ]; then
chroot /media/startos/next chroot /media/startos/next
@@ -71,6 +75,10 @@ else
CHROOT_RES=$? CHROOT_RES=$?
fi fi
if mountpoint /media/startos/next/sys/firmware/efi/efivars 2> /dev/null; then
umount /media/startos/next/sys/firmware/efi/efivars
fi
umount /media/startos/next/run umount /media/startos/next/run
umount /media/startos/next/tmp umount /media/startos/next/tmp
umount /media/startos/next/dev umount /media/startos/next/dev
@@ -87,11 +95,12 @@ if [ "$CHROOT_RES" -eq 0 ]; then
echo 'Upgrading...' echo 'Upgrading...'
rm -f /media/startos/images/next.squashfs
if ! time mksquashfs /media/startos/next /media/startos/images/next.squashfs -b 4096 -comp gzip; then if ! time mksquashfs /media/startos/next /media/startos/images/next.squashfs -b 4096 -comp gzip; then
umount -R /media/startos/next umount -l /media/startos/next
umount -R /media/startos/upper umount -l /media/startos/upper
rm -rf /media/startos/upper /media/startos/next rm -rf /media/startos/upper /media/startos/next
exit 1 exit 1
fi fi
hash=$(b3sum /media/startos/images/next.squashfs | head -c 32) hash=$(b3sum /media/startos/images/next.squashfs | head -c 32)
mv /media/startos/images/next.squashfs /media/startos/images/${hash}.rootfs mv /media/startos/images/next.squashfs /media/startos/images/${hash}.rootfs
@@ -103,5 +112,5 @@ if [ "$CHROOT_RES" -eq 0 ]; then
fi fi
umount -R /media/startos/next umount -R /media/startos/next
umount -R /media/startos/upper umount /media/startos/upper
rm -rf /media/startos/upper /media/startos/next rm -rf /media/startos/upper /media/startos/next

View File

@@ -27,7 +27,6 @@ user_pref("browser.crashReports.unsubmittedCheck.autoSubmit2", false);
user_pref("browser.newtabpage.activity-stream.feeds.asrouterfeed", false); user_pref("browser.newtabpage.activity-stream.feeds.asrouterfeed", false);
user_pref("browser.newtabpage.activity-stream.feeds.topsites", false); user_pref("browser.newtabpage.activity-stream.feeds.topsites", false);
user_pref("browser.newtabpage.activity-stream.showSponsoredTopSites", false); user_pref("browser.newtabpage.activity-stream.showSponsoredTopSites", false);
user_pref("browser.onboarding.enabled", false);
user_pref("browser.ping-centre.telemetry", false); user_pref("browser.ping-centre.telemetry", false);
user_pref("browser.pocket.enabled", false); user_pref("browser.pocket.enabled", false);
user_pref("browser.safebrowsing.blockedURIs.enabled", false); user_pref("browser.safebrowsing.blockedURIs.enabled", false);
@@ -43,7 +42,7 @@ user_pref("browser.startup.homepage_override.mstone", "ignore");
user_pref("browser.theme.content-theme", 0); user_pref("browser.theme.content-theme", 0);
user_pref("browser.theme.toolbar-theme", 0); user_pref("browser.theme.toolbar-theme", 0);
user_pref("browser.urlbar.groupLabels.enabled", false); user_pref("browser.urlbar.groupLabels.enabled", false);
user_pref("browser.urlbar.suggest.searches" false); user_pref("browser.urlbar.suggest.searches", false);
user_pref("datareporting.policy.firstRunURL", ""); user_pref("datareporting.policy.firstRunURL", "");
user_pref("datareporting.healthreport.service.enabled", false); user_pref("datareporting.healthreport.service.enabled", false);
user_pref("datareporting.healthreport.uploadEnabled", false); user_pref("datareporting.healthreport.uploadEnabled", false);
@@ -52,10 +51,9 @@ user_pref("dom.securecontext.allowlist_onions", true);
user_pref("dom.securecontext.whitelist_onions", true); user_pref("dom.securecontext.whitelist_onions", true);
user_pref("experiments.enabled", false); user_pref("experiments.enabled", false);
user_pref("experiments.activeExperiment", false); user_pref("experiments.activeExperiment", false);
user_pref("experiments.supported", false);
user_pref("extensions.activeThemeID", "firefox-compact-dark@mozilla.org"); user_pref("extensions.activeThemeID", "firefox-compact-dark@mozilla.org");
user_pref("extensions.blocklist.enabled", false); user_pref("extensions.blocklist.enabled", false);
user_pref("extensions.getAddons.cache.enabled", false); user_pref("extensions.htmlaboutaddons.recommendations.enabled", false);
user_pref("extensions.pocket.enabled", false); user_pref("extensions.pocket.enabled", false);
user_pref("extensions.update.enabled", false); user_pref("extensions.update.enabled", false);
user_pref("extensions.shield-recipe-client.enabled", false); user_pref("extensions.shield-recipe-client.enabled", false);
@@ -66,9 +64,15 @@ user_pref("messaging-system.rsexperimentloader.enabled", false);
user_pref("network.allow-experiments", false); user_pref("network.allow-experiments", false);
user_pref("network.captive-portal-service.enabled", false); user_pref("network.captive-portal-service.enabled", false);
user_pref("network.connectivity-service.enabled", false); user_pref("network.connectivity-service.enabled", false);
user_pref("network.proxy.autoconfig_url", "file:///usr/lib/startos/proxy.pac"); user_pref("network.proxy.socks", "10.0.3.1");
user_pref("network.proxy.socks_port", 9050);
user_pref("network.proxy.socks_version", 5);
user_pref("network.proxy.socks_remote_dns", true); user_pref("network.proxy.socks_remote_dns", true);
user_pref("network.proxy.type", 2); user_pref("network.proxy.type", 1);
user_pref("privacy.resistFingerprinting", true);
//Enable letterboxing if we want the window size sent to the server to snap to common resolutions:
//user_pref("privacy.resistFingerprinting.letterboxing", true);
user_pref("privacy.trackingprotection.enabled", true);
user_pref("signon.rememberSignons", false); user_pref("signon.rememberSignons", false);
user_pref("toolkit.telemetry.archive.enabled", false); user_pref("toolkit.telemetry.archive.enabled", false);
user_pref("toolkit.telemetry.bhrPing.enabled", false); user_pref("toolkit.telemetry.bhrPing.enabled", false);
@@ -81,6 +85,17 @@ user_pref("toolkit.telemetry.shutdownPingSender.enabled", false);
user_pref("toolkit.telemetry.unified", false); user_pref("toolkit.telemetry.unified", false);
user_pref("toolkit.telemetry.updatePing.enabled", false); user_pref("toolkit.telemetry.updatePing.enabled", false);
user_pref("toolkit.telemetry.cachedClientID", ""); user_pref("toolkit.telemetry.cachedClientID", "");
//Blocking automatic Mozilla CDN server requests
user_pref("extensions.getAddons.showPane", false);
user_pref("extensions.getAddons.cache.enabled", false);
//user_pref("services.settings.server", ""); // Remote settings server (HSTS preload updates and Cerfiticate Revocation Lists are fetched)
user_pref("browser.aboutHomeSnippets.updateUrl", "");
user_pref("browser.newtabpage.activity-stream.feeds.snippets", false);
user_pref("browser.newtabpage.activity-stream.feeds.section.topstories", false);
user_pref("browser.newtabpage.activity-stream.feeds.system.topstories", false);
user_pref("browser.newtabpage.activity-stream.feeds.discoverystreamfeed", false);
user_pref("browser.safebrowsing.provider.mozilla.updateURL", "");
user_pref("browser.safebrowsing.provider.mozilla.gethashURL", "");
EOF EOF
ln -sf /usr/lib/$(uname -m)-linux-gnu/pkcs11/p11-kit-trust.so /usr/lib/firefox-esr/libnssckbi.so ln -sf /usr/lib/$(uname -m)-linux-gnu/pkcs11/p11-kit-trust.so /usr/lib/firefox-esr/libnssckbi.so
@@ -91,15 +106,6 @@ cat > /home/kiosk/kiosk.sh << 'EOF'
while ! curl "http://localhost" > /dev/null; do while ! curl "http://localhost" > /dev/null; do
sleep 1 sleep 1
done done
while ! /usr/lib/startos/scripts/check-monitor; do
sleep 15
done
(
while /usr/lib/startos/scripts/check-monitor; do
sleep 15
done
killall firefox-esr
) &
matchbox-window-manager -use_titlebar no & matchbox-window-manager -use_titlebar no &
cp -r /home/kiosk/fx-profile /home/kiosk/fx-profile-tmp cp -r /home/kiosk/fx-profile /home/kiosk/fx-profile-tmp
firefox-esr http://localhost --profile /home/kiosk/fx-profile-tmp firefox-esr http://localhost --profile /home/kiosk/fx-profile-tmp

51
build/lib/scripts/forward-port Executable file
View File

@@ -0,0 +1,51 @@
#!/bin/bash
if [ -z "$sip" ] || [ -z "$dip" ] || [ -z "$dprefix" ] || [ -z "$sport" ] || [ -z "$dport" ]; then
>&2 echo 'missing required env var'
exit 1
fi
NAME="F$(echo "$sip:$sport -> $dip/$dprefix:$dport" | sha256sum | head -c 15)"
for kind in INPUT FORWARD ACCEPT; do
if ! iptables -C $kind -j "${NAME}_${kind}" 2> /dev/null; then
iptables -N "${NAME}_${kind}" 2> /dev/null
iptables -A $kind -j "${NAME}_${kind}"
fi
done
for kind in PREROUTING INPUT OUTPUT POSTROUTING; do
if ! iptables -t nat -C $kind -j "${NAME}_${kind}" 2> /dev/null; then
iptables -t nat -N "${NAME}_${kind}" 2> /dev/null
iptables -t nat -A $kind -j "${NAME}_${kind}"
fi
done
err=0
trap 'err=1' ERR
for kind in INPUT FORWARD ACCEPT; do
iptables -F "${NAME}_${kind}" 2> /dev/null
done
for kind in PREROUTING INPUT OUTPUT POSTROUTING; do
iptables -t nat -F "${NAME}_${kind}" 2> /dev/null
done
if [ "$UNDO" = 1 ]; then
conntrack -D -p tcp -d $sip --dport $sport || true # conntrack returns exit 1 if no connections are active
conntrack -D -p udp -d $sip --dport $sport || true # conntrack returns exit 1 if no connections are active
exit $err
fi
iptables -t nat -A ${NAME}_PREROUTING -d "$sip" -p tcp --dport "$sport" -j DNAT --to-destination "$dip:$dport"
iptables -t nat -A ${NAME}_PREROUTING -d "$sip" -p udp --dport "$sport" -j DNAT --to-destination "$dip:$dport"
iptables -t nat -A ${NAME}_OUTPUT -d "$sip" -p tcp --dport "$sport" -j DNAT --to-destination "$dip:$dport"
iptables -t nat -A ${NAME}_OUTPUT -d "$sip" -p udp --dport "$sport" -j DNAT --to-destination "$dip:$dport"
iptables -t nat -A ${NAME}_PREROUTING -s "$dip/$dprefix" -d "$sip" -p tcp --dport "$sport" -j DNAT --to-destination "$dip:$dport"
iptables -t nat -A ${NAME}_PREROUTING -s "$dip/$dprefix" -d "$sip" -p udp --dport "$sport" -j DNAT --to-destination "$dip:$dport"
iptables -t nat -A ${NAME}_POSTROUTING -s "$dip/$dprefix" -d "$dip" -p tcp --dport "$dport" -j MASQUERADE
iptables -t nat -A ${NAME}_POSTROUTING -s "$dip/$dprefix" -d "$dip" -p udp --dport "$dport" -j MASQUERADE
iptables -A ${NAME}_FORWARD -d $dip -p tcp --dport $dport -m state --state NEW -j ACCEPT
iptables -A ${NAME}_FORWARD -d $dip -p udp --dport $dport -m state --state NEW -j ACCEPT
exit $err

35
build/lib/scripts/prune-boot Executable file
View File

@@ -0,0 +1,35 @@
#!/bin/bash
set -e
if [ "$UID" -ne 0 ]; then
>&2 echo 'Must be run as root'
exit 1
fi
# Get the current kernel version
current_kernel=$(uname -r)
echo "Current kernel: $current_kernel"
echo "Searching for old kernel files in /boot..."
# Extract base kernel version (without possible suffixes)
current_base=$(echo "$current_kernel" | sed 's/-.*//')
cd /boot || { echo "/boot directory not found!"; exit 1; }
for file in vmlinuz-* initrd.img-* System.map-* config-*; do
# Extract version from filename
version=$(echo "$file" | sed -E 's/^[^0-9]*([0-9][^ ]*).*/\1/')
# Skip if file matches current kernel version
if [[ "$file" == *"$current_kernel"* ]]; then
continue
fi
# Compare versions, delete if less than current
if dpkg --compare-versions "$version" lt "$current_kernel"; then
echo "Deleting $file (version $version is older than $current_kernel)"
sudo rm -f "$file"
fi
done
echo "Old kernel files deleted."

View File

@@ -83,6 +83,7 @@ local_mount_root()
if [ -d "$image" ]; then if [ -d "$image" ]; then
mount -r --bind $image /lower mount -r --bind $image /lower
elif [ -f "$image" ]; then elif [ -f "$image" ]; then
modprobe loop
modprobe squashfs modprobe squashfs
mount -r $image /lower mount -r $image /lower
else else

View File

@@ -1,36 +1,64 @@
#!/bin/bash #!/bin/bash
fail=$(printf " [\033[31m fail \033[0m]") # --- Config ---
pass=$(printf " [\033[32m pass \033[0m]") # Colors (using printf to ensure compatibility)
GRAY=$(printf '\033[90m')
GREEN=$(printf '\033[32m')
RED=$(printf '\033[31m')
NC=$(printf '\033[0m') # No Color
# Proxies to test
proxies=(
"Host Tor|127.0.1.1:9050"
"Startd Tor|10.0.3.1:9050"
)
# Default URLs
onion_list=( onion_list=(
"The Tor Project|http://2gzyxa5ihm7nsggfxnu52rck2vv4rvmdlkiu3zzui5du4xyclen53wid.onion"
"Start9|http://privacy34kn4ez3y3nijweec6w4g54i3g54sdv7r5mr6soma3w4begyd.onion" "Start9|http://privacy34kn4ez3y3nijweec6w4g54i3g54sdv7r5mr6soma3w4begyd.onion"
"Mempool|http://mempoolhqx4isw62xs7abwphsq7ldayuidyx2v2oethdhhj6mlo2r6ad.onion" "Mempool|http://mempoolhqx4isw62xs7abwphsq7ldayuidyx2v2oethdhhj6mlo2r6ad.onion"
"DuckDuckGo|https://duckduckgogg42xjoc72x3sjasowoarfbgcmvfimaftt6twagswzczad.onion" "DuckDuckGo|https://duckduckgogg42xjoc72x3sjasowoarfbgcmvfimaftt6twagswzczad.onion"
"Brave Search|https://search.brave4u7jddbv7cyviptqjc7jusxh72uik7zt6adtckl5f4nwy2v72qd.onion" "Brave Search|https://search.brave4u7jddbv7cyviptqjc7jusxh72uik7zt6adtckl5f4nwy2v72qd.onion"
) )
# Check if ~/.startos/tor-check.list exists and read its contents if available # Load custom list
if [ -f ~/.startos/tor-check.list ]; then [ -f ~/.startos/tor-check.list ] && readarray -t custom_list < <(grep -v '^#' ~/.startos/tor-check.list) && onion_list+=("${custom_list[@]}")
while IFS= read -r line; do
# Check if the line starts with a # # --- Functions ---
if [[ ! "$line" =~ ^# ]]; then print_line() { printf "${GRAY}────────────────────────────────────────${NC}\n"; }
onion_list+=("$line")
# --- Main ---
echo "Testing Onion Connections..."
for proxy_info in "${proxies[@]}"; do
proxy_name="${proxy_info%%|*}"
proxy_addr="${proxy_info#*|}"
print_line
printf "${GRAY}Proxy: %s (%s)${NC}\n" "$proxy_name" "$proxy_addr"
for data in "${onion_list[@]}"; do
name="${data%%|*}"
url="${data#*|}"
# Capture verbose output + http code.
# --no-progress-meter: Suppresses the "0 0 0" stats but keeps -v output
output=$(curl -v --no-progress-meter --max-time 15 --socks5-hostname "$proxy_addr" "$url" 2>&1)
exit_code=$?
if [ $exit_code -eq 0 ]; then
printf " ${GREEN}[pass]${NC} %s (%s)\n" "$name" "$url"
else
printf " ${RED}[fail]${NC} %s (%s)\n" "$name" "$url"
printf " ${RED}↳ Curl Error %s${NC}\n" "$exit_code"
# Print the last 4 lines of verbose log to show the specific handshake error
# We look for lines starting with '*' or '>' or '<' to filter out junk if any remains
echo "$output" | tail -n 4 | sed "s/^/ ${GRAY}/"
fi fi
done < ~/.startos/tor-check.list done
fi
echo "Testing connection to Onion Pages ..."
for data in "${onion_list[@]}"; do
name="${data%%|*}"
url="${data#*|}"
if curl --socks5-hostname localhost:9050 "$url" > /dev/null 2>&1; then
echo " ${pass}: $name ($url) "
else
echo " ${fail}: $name ($url) "
fi
done done
print_line
echo # Reset color just in case
echo "Done." printf "${NC}"

82
build/lib/scripts/upgrade Executable file
View File

@@ -0,0 +1,82 @@
#!/bin/bash
set -e
SOURCE_DIR="$(dirname $(realpath "${BASH_SOURCE[0]}"))"
if [ "$UID" -ne 0 ]; then
>&2 echo 'Must be run as root'
exit 1
fi
if ! [ -f "$1" ]; then
>&2 echo "usage: $0 <SQUASHFS>"
exit 1
fi
echo 'Upgrading...'
hash=$(b3sum $1 | head -c 32)
if [ -n "$2" ] && [ "$hash" != "$CHECKSUM" ]; then
>&2 echo 'Checksum mismatch'
exit 2
fi
unsquashfs -f -d / $1 boot
umount -R /media/startos/next 2> /dev/null || true
umount /media/startos/upper 2> /dev/null || true
umount /media/startos/lower 2> /dev/null || true
mkdir -p /media/startos/upper
mount -t tmpfs tmpfs /media/startos/upper
mkdir -p /media/startos/lower /media/startos/upper/data /media/startos/upper/work /media/startos/next
mount $1 /media/startos/lower
mount -t overlay \
-olowerdir=/media/startos/lower,upperdir=/media/startos/upper/data,workdir=/media/startos/upper/work \
overlay /media/startos/next
mkdir -p /media/startos/next/run
mkdir -p /media/startos/next/dev
mkdir -p /media/startos/next/sys
mkdir -p /media/startos/next/proc
mkdir -p /media/startos/next/boot
mkdir -p /media/startos/next/media/startos/root
mount --bind /run /media/startos/next/run
mount --bind /tmp /media/startos/next/tmp
mount --bind /dev /media/startos/next/dev
mount --bind /sys /media/startos/next/sys
mount --bind /proc /media/startos/next/proc
mount --bind /boot /media/startos/next/boot
mount --bind /media/startos/root /media/startos/next/media/startos/root
if mountpoint /boot/efi 2> /dev/null; then
mkdir -p /media/startos/next/boot/efi
mount --bind /boot/efi /media/startos/next/boot/efi
fi
if mountpoint /sys/firmware/efi/efivars 2> /dev/null; then
mount --bind /sys/firmware/efi/efivars /media/startos/next/sys/firmware/efi/efivars
fi
chroot /media/startos/next bash -e << "EOF"
if [ -f /boot/grub/grub.cfg ]; then
grub-install /dev/$(eval $(lsblk -o MOUNTPOINT,PKNAME -P | grep 'MOUNTPOINT="/media/startos/root"') && echo $PKNAME)
update-grub
fi
EOF
sync
umount -Rl /media/startos/next
umount /media/startos/upper
umount /media/startos/lower
mv $1 /media/startos/images/${hash}.rootfs
ln -rsf /media/startos/images/${hash}.rootfs /media/startos/config/current.rootfs
sync
echo 'System upgrade complete. Reboot to apply changes...'

View File

@@ -0,0 +1,46 @@
FROM debian:forky
RUN apt-get update && \
apt-get install -y \
ca-certificates \
curl \
gpg \
build-essential \
sed \
grep \
gawk \
jq \
gzip \
brotli \
qemu-user-static \
binfmt-support \
squashfs-tools \
git \
debspawn \
rsync \
b3sum \
fuse-overlayfs \
sudo \
systemd \
systemd-container \
systemd-sysv \
dbus \
dbus-user-session \
nodejs
RUN systemctl mask \
systemd-firstboot.service \
systemd-udevd.service \
getty@tty1.service \
console-getty.service
RUN git config --global --add safe.directory /root/start-os
RUN mkdir -p /etc/debspawn && \
echo "AllowUnsafePermissions=true" > /etc/debspawn/global.toml
RUN mkdir -p /root/start-os
WORKDIR /root/start-os
COPY docker-entrypoint.sh /docker-entrypoint.sh
ENTRYPOINT [ "/docker-entrypoint.sh" ]

View File

@@ -0,0 +1,3 @@
#!/bin/bash
exec /lib/systemd/systemd --unit=multi-user.target --show-status=false --log-target=journal

27
build/os-compat/run-compat.sh Executable file
View File

@@ -0,0 +1,27 @@
#!/bin/bash
if [ "$FORCE_COMPAT" = 1 ] || ( [ "$REQUIRES" = "linux" ] && [ "$(uname -s)" != "Linux" ] ) || ( [ "$REQUIRES" = "debian" ] && ! which dpkg > /dev/null ) || ( [ "$REQUIRES" = "qemu" ] && ! which qemu-$ARCH > /dev/null ); then
project_pwd="$(cd "$(dirname "${BASH_SOURCE[0]}")"/../.. && pwd)/"
pwd="$(pwd)/"
if ! [[ "$pwd" = "$project_pwd"* ]]; then
>&2 echo "Must be run from start-os project dir"
exit 1
fi
rel_pwd="${pwd#"$project_pwd"}"
SYSTEMD_TTY="-P"
USE_TTY=
if tty -s; then
USE_TTY="-it"
SYSTEMD_TTY="-t"
fi
docker run -d --rm --name os-compat --privileged --security-opt apparmor=unconfined -v "${project_pwd}:/root/start-os" -v /lib/modules:/lib/modules:ro start9/build-env
while ! docker exec os-compat systemctl is-active --quiet multi-user.target 2> /dev/null; do sleep .5; done
docker exec -eARCH -eENVIRONMENT -ePLATFORM -eGIT_BRANCH_AS_HASH -ePROJECT -eDEPENDS -eCONFLICTS $USE_TTY -w "/root/start-os${rel_pwd}" os-compat $@
code=$?
docker stop os-compat > /dev/null
exit $code
else
exec $@
fi

View File

@@ -7,6 +7,7 @@ else
fi fi
if ! [ -f ./GIT_HASH.txt ] || [ "$(cat ./GIT_HASH.txt)" != "$GIT_HASH" ]; then if ! [ -f ./GIT_HASH.txt ] || [ "$(cat ./GIT_HASH.txt)" != "$GIT_HASH" ]; then
>&2 echo Git hash changed from "$([ -f ./GIT_HASH.txt ] && cat ./GIT_HASH.txt)" to "$GIT_HASH"
echo -n "$GIT_HASH" > ./GIT_HASH.txt echo -n "$GIT_HASH" > ./GIT_HASH.txt
fi fi

View File

@@ -4,13 +4,17 @@ cd "$(dirname "${BASH_SOURCE[0]}")"
set -e set -e
rm -rf web/dist/static STATIC_DIR=web/dist/static/$1
RAW_DIR=web/dist/raw/$1
mkdir -p $STATIC_DIR
rm -rf $STATIC_DIR
if ! [[ "$ENVIRONMENT" =~ (^|-)dev($|-) ]]; then if ! [[ "$ENVIRONMENT" =~ (^|-)dev($|-) ]]; then
find web/dist/raw -type f -not -name '*.gz' -and -not -name '*.br' | xargs -n 1 -P 0 gzip -kf find $RAW_DIR -type f -not -name '*.gz' -and -not -name '*.br' | xargs -n 1 -P 0 gzip -kf
find web/dist/raw -type f -not -name '*.gz' -and -not -name '*.br' | xargs -n 1 -P 0 brotli -kf find $RAW_DIR -type f -not -name '*.gz' -and -not -name '*.br' | xargs -n 1 -P 0 brotli -kf
for file in $(find web/dist/raw -type f -not -name '*.gz' -and -not -name '*.br'); do for file in $(find $RAW_DIR -type f -not -name '*.gz' -and -not -name '*.br'); do
raw_size=$(du $file | awk '{print $1 * 512}') raw_size=$(du $file | awk '{print $1 * 512}')
gz_size=$(du $file.gz | awk '{print $1 * 512}') gz_size=$(du $file.gz | awk '{print $1 * 512}')
br_size=$(du $file.br | awk '{print $1 * 512}') br_size=$(du $file.br | awk '{print $1 * 512}')
@@ -23,4 +27,5 @@ if ! [[ "$ENVIRONMENT" =~ (^|-)dev($|-) ]]; then
done done
fi fi
cp -r web/dist/raw web/dist/static
cp -r $RAW_DIR $STATIC_DIR

View File

@@ -0,0 +1,6 @@
[Unit]
Description=StartOS Container Runtime Failure Handler
[Service]
Type=oneshot
ExecStart=/usr/bin/start-container rebuild

View File

@@ -1,11 +1,12 @@
[Unit] [Unit]
Description=StartOS Container Runtime Description=StartOS Container Runtime
OnFailure=container-runtime-failure.service
[Service] [Service]
Type=simple Type=simple
ExecStart=/usr/bin/node --experimental-detect-module --unhandled-rejections=warn /usr/lib/startos/init/index.js Environment=RUST_LOG=startos=debug
Restart=always ExecStart=/usr/bin/node --experimental-detect-module --trace-warnings --unhandled-rejections=warn /usr/lib/startos/init/index.js
RestartSec=3 Restart=no
[Install] [Install]
WantedBy=multi-user.target WantedBy=multi-user.target

View File

@@ -6,13 +6,9 @@ mkdir -p /run/systemd/resolve
echo "nameserver 8.8.8.8" > /run/systemd/resolve/stub-resolv.conf echo "nameserver 8.8.8.8" > /run/systemd/resolve/stub-resolv.conf
apt-get update apt-get update
apt-get install -y curl rsync qemu-user-static apt-get install -y curl rsync qemu-user-static nodejs
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash
source ~/.bashrc
nvm install 20
ln -s $(which node) /usr/bin/node
sed -i '/\(^\|#\)DNSStubListener=/c\DNSStubListener=no' /etc/systemd/resolved.conf
sed -i '/\(^\|#\)Storage=/c\Storage=persistent' /etc/systemd/journald.conf sed -i '/\(^\|#\)Storage=/c\Storage=persistent' /etc/systemd/journald.conf
sed -i '/\(^\|#\)Compress=/c\Compress=yes' /etc/systemd/journald.conf sed -i '/\(^\|#\)Compress=/c\Compress=yes' /etc/systemd/journald.conf
sed -i '/\(^\|#\)SystemMaxUse=/c\SystemMaxUse=1G' /etc/systemd/journald.conf sed -i '/\(^\|#\)SystemMaxUse=/c\SystemMaxUse=1G' /etc/systemd/journald.conf
@@ -21,3 +17,6 @@ sed -i '/\(^\|#\)ForwardToSyslog=/c\ForwardToSyslog=no' /etc/systemd/journald.co
systemctl enable container-runtime.service systemctl enable container-runtime.service
rm -rf /run/systemd rm -rf /run/systemd
rm -f /etc/resolv.conf
echo "nameserver 10.0.3.1" > /etc/resolv.conf

View File

@@ -1,11 +1,9 @@
#!/bin/bash #!/bin/bash
cd "$(dirname "${BASH_SOURCE[0]}")" cd "$(dirname "${BASH_SOURCE[0]}")"
set -e set -e
DISTRO=debian DISTRO=debian
VERSION=bookworm VERSION=trixie
ARCH=${ARCH:-$(uname -m)} ARCH=${ARCH:-$(uname -m)}
FLAVOR=default FLAVOR=default
@@ -16,8 +14,9 @@ elif [ "$_ARCH" = "aarch64" ]; then
_ARCH=arm64 _ARCH=arm64
fi fi
URL="https://images.linuxcontainers.org/$(curl -fsSL https://images.linuxcontainers.org/meta/1.0/index-system | grep "^$DISTRO;$VERSION;$_ARCH;$FLAVOR;" | head -n1 | sed 's/^.*;//g')/rootfs.squashfs" BASE_URL="https://images.linuxcontainers.org$(curl -fsSL https://images.linuxcontainers.org/meta/1.0/index-system | grep "^$DISTRO;$VERSION;$_ARCH;$FLAVOR;" | head -n1 | sed 's/^.*;//g')"
OUTPUT_FILE="debian.${ARCH}.squashfs"
echo "Downloading $URL to debian.${ARCH}.squashfs" echo "Downloading ${BASE_URL}/rootfs.squashfs to $OUTPUT_FILE"
curl -fsSL "${BASE_URL}/rootfs.squashfs" > "$OUTPUT_FILE"
curl -fsSL "$URL" > debian.${ARCH}.squashfs curl -fsSL "$BASE_URL/SHA256SUMS" | grep 'rootfs\.squashfs' | awk '{print $1" '"$OUTPUT_FILE"'"}' | shasum -a 256 -c

File diff suppressed because it is too large Load Diff

View File

@@ -26,8 +26,9 @@
"isomorphic-fetch": "^3.0.0", "isomorphic-fetch": "^3.0.0",
"jsonpath": "^1.1.1", "jsonpath": "^1.1.1",
"lodash.merge": "^4.6.2", "lodash.merge": "^4.6.2",
"mime": "^4.0.7",
"node-fetch": "^3.1.0", "node-fetch": "^3.1.0",
"ts-matches": "^5.5.1", "ts-matches": "^6.3.2",
"tslib": "^2.5.3", "tslib": "^2.5.3",
"typescript": "^5.1.3", "typescript": "^5.1.3",
"yaml": "^2.3.1" "yaml": "^2.3.1"

View File

@@ -1,4 +1,9 @@
import { types as T, utils } from "@start9labs/start-sdk" import {
ExtendedVersion,
types as T,
utils,
VersionRange,
} from "@start9labs/start-sdk"
import * as net from "net" import * as net from "net"
import { object, string, number, literals, some, unknown } from "ts-matches" import { object, string, number, literals, some, unknown } from "ts-matches"
import { Effects } from "../Models/Effects" import { Effects } from "../Models/Effects"
@@ -6,23 +11,19 @@ import { Effects } from "../Models/Effects"
import { CallbackHolder } from "../Models/CallbackHolder" import { CallbackHolder } from "../Models/CallbackHolder"
import { asError } from "@start9labs/start-sdk/base/lib/util" import { asError } from "@start9labs/start-sdk/base/lib/util"
const matchRpcError = object({ const matchRpcError = object({
error: object( error: object({
{ code: number,
code: number, message: string,
message: string, data: some(
data: some( string,
string, object({
object( details: string,
{ debug: string.nullable().optional(),
details: string, }),
debug: string, )
}, .nullable()
["debug"], .optional(),
), }),
),
},
["data"],
),
}) })
const testRpcError = matchRpcError.test const testRpcError = matchRpcError.test
const testRpcResult = object({ const testRpcResult = object({
@@ -34,13 +35,13 @@ const SOCKET_PATH = "/media/startos/rpc/host.sock"
let hostSystemId = 0 let hostSystemId = 0
export type EffectContext = { export type EffectContext = {
procedureId: string | null eventId: string | null
callbacks?: CallbackHolder callbacks?: CallbackHolder
constRetry?: () => void constRetry?: () => void
} }
const rpcRoundFor = const rpcRoundFor =
(procedureId: string | null) => (eventId: string | null) =>
<K extends T.EffectMethod | "clearCallbacks">( <K extends T.EffectMethod | "clearCallbacks">(
method: K, method: K,
params: Record<string, unknown>, params: Record<string, unknown>,
@@ -51,7 +52,7 @@ const rpcRoundFor =
JSON.stringify({ JSON.stringify({
id, id,
method, method,
params: { ...params, procedureId: procedureId || undefined }, params: { ...params, eventId: eventId ?? undefined },
}) + "\n", }) + "\n",
) )
}) })
@@ -102,9 +103,21 @@ const rpcRoundFor =
} }
export function makeEffects(context: EffectContext): Effects { export function makeEffects(context: EffectContext): Effects {
const rpcRound = rpcRoundFor(context.procedureId) const rpcRound = rpcRoundFor(context.eventId)
const self: Effects = { const self: Effects = {
eventId: context.eventId,
child: (name) =>
makeEffects({ ...context, callbacks: context.callbacks?.child(name) }),
constRetry: context.constRetry, constRetry: context.constRetry,
isInContext: !!context.callbacks,
onLeaveContext:
context.callbacks?.onLeaveContext?.bind(context.callbacks) ||
(() => {
console.warn(
"no context for this effects object",
new Error().stack?.replace(/^Error/, ""),
)
}),
clearCallbacks(...[options]: Parameters<T.Effects["clearCallbacks"]>) { clearCallbacks(...[options]: Parameters<T.Effects["clearCallbacks"]>) {
return rpcRound("clear-callbacks", { return rpcRound("clear-callbacks", {
...options, ...options,
@@ -126,22 +139,20 @@ export function makeEffects(context: EffectContext): Effects {
...options, ...options,
}) as ReturnType<T.Effects["action"]["getInput"]> }) as ReturnType<T.Effects["action"]["getInput"]>
}, },
request(...[options]: Parameters<T.Effects["action"]["request"]>) { createTask(...[options]: Parameters<T.Effects["action"]["createTask"]>) {
return rpcRound("action.request", { return rpcRound("action.create-task", {
...options, ...options,
}) as ReturnType<T.Effects["action"]["request"]> }) as ReturnType<T.Effects["action"]["createTask"]>
}, },
run(...[options]: Parameters<T.Effects["action"]["run"]>) { run(...[options]: Parameters<T.Effects["action"]["run"]>) {
return rpcRound("action.run", { return rpcRound("action.run", {
...options, ...options,
}) as ReturnType<T.Effects["action"]["run"]> }) as ReturnType<T.Effects["action"]["run"]>
}, },
clearRequests( clearTasks(...[options]: Parameters<T.Effects["action"]["clearTasks"]>) {
...[options]: Parameters<T.Effects["action"]["clearRequests"]> return rpcRound("action.clear-tasks", {
) {
return rpcRound("action.clear-requests", {
...options, ...options,
}) as ReturnType<T.Effects["action"]["clearRequests"]> }) as ReturnType<T.Effects["action"]["clearTasks"]>
}, },
}, },
bind(...[options]: Parameters<T.Effects["bind"]>) { bind(...[options]: Parameters<T.Effects["bind"]>) {
@@ -186,13 +197,6 @@ export function makeEffects(context: EffectContext): Effects {
T.Effects["exportServiceInterface"] T.Effects["exportServiceInterface"]
> >
}) as Effects["exportServiceInterface"], }) as Effects["exportServiceInterface"],
exposeForDependents(
...[options]: Parameters<T.Effects["exposeForDependents"]>
) {
return rpcRound("expose-for-dependents", options) as ReturnType<
T.Effects["exposeForDependents"]
>
},
getContainerIp(...[options]: Parameters<T.Effects["getContainerIp"]>) { getContainerIp(...[options]: Parameters<T.Effects["getContainerIp"]>) {
return rpcRound("get-container-ip", options) as ReturnType< return rpcRound("get-container-ip", options) as ReturnType<
T.Effects["getContainerIp"] T.Effects["getContainerIp"]
@@ -254,6 +258,7 @@ export function makeEffects(context: EffectContext): Effects {
return rpcRound("mount", options) as ReturnType<T.Effects["mount"]> return rpcRound("mount", options) as ReturnType<T.Effects["mount"]>
}, },
restart(...[]: Parameters<T.Effects["restart"]>) { restart(...[]: Parameters<T.Effects["restart"]>) {
console.log("Restarting service...")
return rpcRound("restart", {}) as ReturnType<T.Effects["restart"]> return rpcRound("restart", {}) as ReturnType<T.Effects["restart"]>
}, },
setDependencies( setDependencies(
@@ -284,6 +289,7 @@ export function makeEffects(context: EffectContext): Effects {
getStatus(...[o]: Parameters<T.Effects["getStatus"]>) { getStatus(...[o]: Parameters<T.Effects["getStatus"]>) {
return rpcRound("get-status", o) as ReturnType<T.Effects["getStatus"]> return rpcRound("get-status", o) as ReturnType<T.Effects["getStatus"]>
}, },
/// DEPRECATED
setMainStatus(o: { status: "running" | "stopped" }): Promise<null> { setMainStatus(o: { status: "running" | "stopped" }): Promise<null> {
return rpcRound("set-main-status", o) as ReturnType< return rpcRound("set-main-status", o) as ReturnType<
T.Effects["setHealth"] T.Effects["setHealth"]
@@ -293,15 +299,6 @@ export function makeEffects(context: EffectContext): Effects {
shutdown(...[]: Parameters<T.Effects["shutdown"]>) { shutdown(...[]: Parameters<T.Effects["shutdown"]>) {
return rpcRound("shutdown", {}) as ReturnType<T.Effects["shutdown"]> return rpcRound("shutdown", {}) as ReturnType<T.Effects["shutdown"]>
}, },
store: {
get: async (options: any) =>
rpcRound("store.get", {
...options,
callback: context.callbacks?.addCallback(options.callback) || null,
}) as any,
set: async (options: any) =>
rpcRound("store.set", options) as ReturnType<T.Effects["store"]["set"]>,
} as T.Effects["store"],
getDataVersion() { getDataVersion() {
return rpcRound("get-data-version", {}) as ReturnType< return rpcRound("get-data-version", {}) as ReturnType<
T.Effects["getDataVersion"] T.Effects["getDataVersion"]
@@ -313,5 +310,15 @@ export function makeEffects(context: EffectContext): Effects {
> >
}, },
} }
if (context.callbacks?.onLeaveContext)
self.onLeaveContext(() => {
self.isInContext = false
self.onLeaveContext = () => {
console.warn(
"this effects object is already out of context",
new Error().stack?.replace(/^Error/, ""),
)
}
})
return self return self
} }

View File

@@ -12,9 +12,15 @@ import {
any, any,
shape, shape,
anyOf, anyOf,
literals,
} from "ts-matches" } from "ts-matches"
import { types as T, utils } from "@start9labs/start-sdk" import {
ExtendedVersion,
types as T,
utils,
VersionRange,
} from "@start9labs/start-sdk"
import * as fs from "fs" import * as fs from "fs"
import { CallbackHolder } from "../Models/CallbackHolder" import { CallbackHolder } from "../Models/CallbackHolder"
@@ -26,20 +32,16 @@ type MaybePromise<T> = T | Promise<T>
export const matchRpcResult = anyOf( export const matchRpcResult = anyOf(
object({ result: any }), object({ result: any }),
object({ object({
error: object( error: object({
{ code: number,
code: number, message: string,
message: string, data: object({
data: object( details: string.optional(),
{ debug: any.optional(),
details: string, })
debug: any, .nullable()
}, .optional(),
["details", "debug"], }),
),
},
["data"],
),
}), }),
) )
@@ -54,38 +56,26 @@ const isResult = object({ result: any }).test
const idType = some(string, number, literal(null)) const idType = some(string, number, literal(null))
type IdType = null | string | number | undefined type IdType = null | string | number | undefined
const runType = object( const runType = object({
{ id: idType.optional(),
id: idType, method: literal("execute"),
method: literal("execute"), params: object({
params: object( id: string,
{ procedure: string,
id: string, input: any,
procedure: string, timeout: number.nullable().optional(),
input: any, }),
timeout: number, })
}, const sandboxRunType = object({
["timeout"], id: idType.optional(),
), method: literal("sandbox"),
}, params: object({
["id"], id: string,
) procedure: string,
const sandboxRunType = object( input: any,
{ timeout: number.nullable().optional(),
id: idType, }),
method: literal("sandbox"), })
params: object(
{
id: string,
procedure: string,
input: any,
timeout: number,
},
["timeout"],
),
},
["id"],
)
const callbackType = object({ const callbackType = object({
method: literal("callback"), method: literal("callback"),
params: object({ params: object({
@@ -93,44 +83,37 @@ const callbackType = object({
args: array, args: array,
}), }),
}) })
const initType = object( const initType = object({
{ id: idType.optional(),
id: idType, method: literal("init"),
method: literal("init"), params: object({
}, id: string,
["id"], kind: literals("install", "update", "restore").nullable(),
) }),
const startType = object( })
{ const startType = object({
id: idType, id: idType.optional(),
method: literal("start"), method: literal("start"),
}, })
["id"], const stopType = object({
) id: idType.optional(),
const stopType = object( method: literal("stop"),
{ })
id: idType, const exitType = object({
method: literal("stop"), id: idType.optional(),
}, method: literal("exit"),
["id"], params: object({
) id: string,
const exitType = object( target: string.nullable(),
{ }),
id: idType, })
method: literal("exit"), const evalType = object({
}, id: idType.optional(),
["id"], method: literal("eval"),
) params: object({
const evalType = object( script: string,
{ }),
id: idType, })
method: literal("eval"),
params: object({
script: string,
}),
},
["id"],
)
const jsonParse = (x: string) => JSON.parse(x) const jsonParse = (x: string) => JSON.parse(x)
@@ -163,6 +146,7 @@ const handleRpc = (id: IdType, result: Promise<RpcResult>) =>
const hasId = object({ id: idType }).test const hasId = object({ id: idType }).test
export class RpcListener { export class RpcListener {
shouldExit = false
unixSocketServer = net.createServer(async (server) => {}) unixSocketServer = net.createServer(async (server) => {})
private _system: System | undefined private _system: System | undefined
private callbacks: CallbackHolder | undefined private callbacks: CallbackHolder | undefined
@@ -171,8 +155,12 @@ export class RpcListener {
if (!fs.existsSync(SOCKET_PARENT)) { if (!fs.existsSync(SOCKET_PARENT)) {
fs.mkdirSync(SOCKET_PARENT, { recursive: true }) fs.mkdirSync(SOCKET_PARENT, { recursive: true })
} }
if (fs.existsSync(SOCKET_PATH)) fs.rmSync(SOCKET_PATH, { force: true })
this.unixSocketServer.listen(SOCKET_PATH) this.unixSocketServer.listen(SOCKET_PATH)
console.log("Listening on %s", SOCKET_PATH)
this.unixSocketServer.on("connection", (s) => { this.unixSocketServer.on("connection", (s) => {
let id: IdType = null let id: IdType = null
const captureId = <X>(x: X) => { const captureId = <X>(x: X) => {
@@ -223,6 +211,11 @@ export class RpcListener {
.catch(mapError) .catch(mapError)
.then(logData("response")) .then(logData("response"))
.then(writeDataToSocket) .then(writeDataToSocket)
.then((_) => {
if (this.shouldExit) {
process.exit(0)
}
})
.catch((e) => { .catch((e) => {
console.error(`Major error in socket handling: ${e}`) console.error(`Major error in socket handling: ${e}`)
console.debug(`Data in: ${a.toString()}`) console.debug(`Data in: ${a.toString()}`)
@@ -238,21 +231,6 @@ export class RpcListener {
return this._system return this._system
} }
private callbackHolders: Map<string, CallbackHolder> = new Map()
private removeCallbackHolderFor(procedure: string) {
const prev = this.callbackHolders.get(procedure)
if (prev) {
this.callbackHolders.delete(procedure)
this.callbacks?.removeChild(prev)
}
}
private callbackHolderFor(procedure: string): CallbackHolder {
this.removeCallbackHolderFor(procedure)
const callbackHolder = this.callbacks!.child()
this.callbackHolders.set(procedure, callbackHolder)
return callbackHolder
}
callCallback(callback: number, args: any[]): void { callCallback(callback: number, args: any[]): void {
if (this.callbacks) { if (this.callbacks) {
this.callbacks this.callbacks
@@ -272,11 +250,11 @@ export class RpcListener {
.when(runType, async ({ id, params }) => { .when(runType, async ({ id, params }) => {
const system = this.system const system = this.system
const procedure = jsonPath.unsafeCast(params.procedure) const procedure = jsonPath.unsafeCast(params.procedure)
const { input, timeout, id: procedureId } = params const { input, timeout, id: eventId } = params
const result = this.getResult( const result = this.getResult(
procedure, procedure,
system, system,
procedureId, eventId,
timeout, timeout,
input, input,
) )
@@ -286,11 +264,11 @@ export class RpcListener {
.when(sandboxRunType, async ({ id, params }) => { .when(sandboxRunType, async ({ id, params }) => {
const system = this.system const system = this.system
const procedure = jsonPath.unsafeCast(params.procedure) const procedure = jsonPath.unsafeCast(params.procedure)
const { input, timeout, id: procedureId } = params const { input, timeout, id: eventId } = params
const result = this.getResult( const result = this.getResult(
procedure, procedure,
system, system,
procedureId, eventId,
timeout, timeout,
input, input,
) )
@@ -302,9 +280,10 @@ export class RpcListener {
return null return null
}) })
.when(startType, async ({ id }) => { .when(startType, async ({ id }) => {
const callbacks = this.callbackHolderFor("main") const callbacks =
this.callbacks?.getChild("main") || this.callbacks?.child("main")
const effects = makeEffects({ const effects = makeEffects({
procedureId: null, eventId: null,
callbacks, callbacks,
}) })
return handleRpc( return handleRpc(
@@ -313,21 +292,39 @@ export class RpcListener {
) )
}) })
.when(stopType, async ({ id }) => { .when(stopType, async ({ id }) => {
this.removeCallbackHolderFor("main")
return handleRpc( return handleRpc(
id, id,
this.system.stop().then((result) => ({ result })), this.system.stop().then((result) => {
this.callbacks?.removeChild("main")
return { result }
}),
) )
}) })
.when(exitType, async ({ id }) => { .when(exitType, async ({ id, params }) => {
return handleRpc( return handleRpc(
id, id,
(async () => { (async () => {
if (this._system) await this._system.exit() if (this._system) {
let target = null
if (params.target)
try {
target = ExtendedVersion.parse(params.target)
} catch (_) {
target = VersionRange.parse(params.target).normalize()
}
await this._system.exit(
makeEffects({
eventId: params.id,
}),
target,
)
this.shouldExit = true
}
})().then((result) => ({ result })), })().then((result) => ({ result })),
) )
}) })
.when(initType, async ({ id }) => { .when(initType, async ({ id, params }) => {
return handleRpc( return handleRpc(
id, id,
(async () => { (async () => {
@@ -335,16 +332,19 @@ export class RpcListener {
const system = await this.getDependencies.system() const system = await this.getDependencies.system()
this.callbacks = new CallbackHolder( this.callbacks = new CallbackHolder(
makeEffects({ makeEffects({
procedureId: null, eventId: params.id,
}), }),
) )
const callbacks = this.callbackHolderFor("containerInit") const callbacks = this.callbacks.child("init")
await system.containerInit( console.error("Initializing...")
await system.init(
makeEffects({ makeEffects({
procedureId: null, eventId: params.id,
callbacks, callbacks,
}), }),
params.kind,
) )
console.error("Initialization complete.")
this._system = system this._system = system
} }
})().then((result) => ({ result })), })().then((result) => ({ result })),
@@ -377,7 +377,7 @@ export class RpcListener {
) )
}) })
.when( .when(
shape({ id: idType, method: string }, ["id"]), shape({ id: idType.optional(), method: string }),
({ id, method }) => ({ ({ id, method }) => ({
jsonrpc, jsonrpc,
id, id,
@@ -411,8 +411,8 @@ export class RpcListener {
private getResult( private getResult(
procedure: typeof jsonPath._TYPE, procedure: typeof jsonPath._TYPE,
system: System, system: System,
procedureId: string, eventId: string,
timeout: number | undefined, timeout: number | null | undefined,
input: any, input: any,
) { ) {
const ensureResultTypeShape = ( const ensureResultTypeShape = (
@@ -420,9 +420,9 @@ export class RpcListener {
): { result: any } => { ): { result: any } => {
return { result } return { result }
} }
const callbacks = this.callbackHolderFor(procedure) const callbacks = this.callbacks?.child(procedure)
const effects = makeEffects({ const effects = makeEffects({
procedureId, eventId,
callbacks, callbacks,
}) })
@@ -430,16 +430,6 @@ export class RpcListener {
switch (procedure) { switch (procedure) {
case "/backup/create": case "/backup/create":
return system.createBackup(effects, timeout || null) return system.createBackup(effects, timeout || null)
case "/backup/restore":
return system.restoreBackup(effects, timeout || null)
case "/packageInit":
return system.packageInit(effects, timeout || null)
case "/packageUninit":
return system.packageUninit(
effects,
string.optional().unsafeCast(input),
timeout || null,
)
default: default:
const procedures = unNestPath(procedure) const procedures = unNestPath(procedure)
switch (true) { switch (true) {
@@ -461,14 +451,10 @@ export class RpcListener {
})().then(ensureResultTypeShape, (error) => })().then(ensureResultTypeShape, (error) =>
matches(error) matches(error)
.when( .when(
object( object({
{ error: string,
error: string, code: number.defaultTo(0),
code: number, }),
},
["code"],
{ code: 0 },
),
(error) => ({ (error) => ({
error: { error: {
code: error.code, code: error.code,

View File

@@ -7,13 +7,22 @@ import { Volume } from "./matchVolume"
import { import {
CommandOptions, CommandOptions,
ExecOptions, ExecOptions,
ExecSpawnable, SubContainerOwned,
} from "@start9labs/start-sdk/package/lib/util/SubContainer" } from "@start9labs/start-sdk/package/lib/util/SubContainer"
import { Mounts } from "@start9labs/start-sdk/package/lib/mainFn/Mounts"
import { Manifest } from "@start9labs/start-sdk/base/lib/osBindings"
import { BackupEffects } from "@start9labs/start-sdk/package/lib/backup/Backups"
import { Drop } from "@start9labs/start-sdk/package/lib/util"
import { SDKManifest } from "@start9labs/start-sdk/base/lib/types"
export const exec = promisify(cp.exec) export const exec = promisify(cp.exec)
export const execFile = promisify(cp.execFile) export const execFile = promisify(cp.execFile)
export class DockerProcedureContainer { export class DockerProcedureContainer extends Drop {
private constructor(private readonly subcontainer: ExecSpawnable) {} private constructor(
private readonly subcontainer: SubContainer<SDKManifest>,
) {
super()
}
static async of( static async of(
effects: T.Effects, effects: T.Effects,
@@ -21,7 +30,7 @@ export class DockerProcedureContainer {
data: DockerProcedure, data: DockerProcedure,
volumes: { [id: VolumeId]: Volume }, volumes: { [id: VolumeId]: Volume },
name: string, name: string,
options: { subcontainer?: ExecSpawnable } = {}, options: { subcontainer?: SubContainer<SDKManifest> } = {},
) { ) {
const subcontainer = const subcontainer =
options?.subcontainer ?? options?.subcontainer ??
@@ -41,9 +50,10 @@ export class DockerProcedureContainer {
volumes: { [id: VolumeId]: Volume }, volumes: { [id: VolumeId]: Volume },
name: string, name: string,
) { ) {
const subcontainer = await SubContainer.of( const subcontainer = await SubContainerOwned.of(
effects, effects as BackupEffects,
{ imageId: data.image }, { imageId: data.image },
null,
name, name,
) )
@@ -57,13 +67,19 @@ export class DockerProcedureContainer {
const volumeMount = volumes[mount] const volumeMount = volumes[mount]
if (volumeMount.type === "data") { if (volumeMount.type === "data") {
await subcontainer.mount( await subcontainer.mount(
{ type: "volume", id: mount, subpath: null, readonly: false }, Mounts.of().mountVolume({
mounts[mount], volumeId: mount,
subpath: null,
mountpoint: mounts[mount],
readonly: false,
}),
) )
} else if (volumeMount.type === "assets") { } else if (volumeMount.type === "assets") {
await subcontainer.mount( await subcontainer.mount(
{ type: "assets", subpath: mount }, Mounts.of().mountAssets({
mounts[mount], subpath: mount,
mountpoint: mounts[mount],
}),
) )
} else if (volumeMount.type === "certificate") { } else if (volumeMount.type === "certificate") {
const hostnames = [ const hostnames = [
@@ -95,21 +111,22 @@ export class DockerProcedureContainer {
key, key,
) )
} else if (volumeMount.type === "pointer") { } else if (volumeMount.type === "pointer") {
await effects await effects.mount({
.mount({ location: path,
location: path, target: {
target: { packageId: volumeMount["package-id"],
packageId: volumeMount["package-id"], subpath: volumeMount.path,
subpath: volumeMount.path, readonly: volumeMount.readonly,
readonly: volumeMount.readonly, volumeId: volumeMount["volume-id"],
volumeId: volumeMount["volume-id"], idmap: [],
}, },
}) })
.catch(console.warn)
} else if (volumeMount.type === "backup") { } else if (volumeMount.type === "backup") {
await subcontainer.mount( await subcontainer.mount(
{ type: "backup", subpath: null }, Mounts.of().mountBackups({
mounts[mount], subpath: null,
mountpoint: mounts[mount],
}),
) )
} }
} }
@@ -151,7 +168,11 @@ export class DockerProcedureContainer {
} }
} }
async spawn(commands: string[]): Promise<cp.ChildProcess> { // async spawn(commands: string[]): Promise<cp.ChildProcess> {
return await this.subcontainer.spawn(commands) // return await this.subcontainer.spawn(commands)
// }
onDrop(): void {
this.subcontainer.destroy?.()
} }
} }

View File

@@ -6,6 +6,8 @@ import { Daemon } from "@start9labs/start-sdk/package/lib/mainFn/Daemon"
import { Effects } from "../../../Models/Effects" import { Effects } from "../../../Models/Effects"
import { off } from "node:process" import { off } from "node:process"
import { CommandController } from "@start9labs/start-sdk/package/lib/mainFn/CommandController" import { CommandController } from "@start9labs/start-sdk/package/lib/mainFn/CommandController"
import { SDKManifest } from "@start9labs/start-sdk/base/lib/types"
import { SubContainerRc } from "@start9labs/start-sdk/package/lib/util/SubContainer"
const EMBASSY_HEALTH_INTERVAL = 15 * 1000 const EMBASSY_HEALTH_INTERVAL = 15 * 1000
const EMBASSY_PROPERTIES_LOOP = 30 * 1000 const EMBASSY_PROPERTIES_LOOP = 30 * 1000
@@ -15,8 +17,13 @@ const EMBASSY_PROPERTIES_LOOP = 30 * 1000
* Also, this has an ability to clean itself up too if need be. * Also, this has an ability to clean itself up too if need be.
*/ */
export class MainLoop { export class MainLoop {
private subcontainerRc?: SubContainerRc<SDKManifest>
get mainSubContainerHandle() { get mainSubContainerHandle() {
return this.mainEvent?.daemon?.subContainerHandle this.subcontainerRc =
this.subcontainerRc ??
this.mainEvent?.daemon?.subcontainerRc() ??
undefined
return this.subcontainerRc
} }
private healthLoops?: { private healthLoops?: {
name: string name: string
@@ -24,7 +31,7 @@ export class MainLoop {
}[] }[]
private mainEvent?: { private mainEvent?: {
daemon: Daemon daemon: Daemon<SDKManifest>
} }
private constructor( private constructor(
@@ -55,28 +62,20 @@ export class MainLoop {
if (jsMain) { if (jsMain) {
throw new Error("Unreachable") throw new Error("Unreachable")
} }
const daemon = new Daemon(async () => { const subcontainer = await DockerProcedureContainer.createSubContainer(
const subcontainer = await DockerProcedureContainer.createSubContainer( effects,
effects, this.system.manifest.id,
this.system.manifest.id, this.system.manifest.main,
this.system.manifest.main, this.system.manifest.volumes,
this.system.manifest.volumes, `Main - ${currentCommand.join(" ")}`,
`Main - ${currentCommand.join(" ")}`, )
) const daemon = await Daemon.of()(this.effects, subcontainer, {
return CommandController.of()( command: currentCommand,
this.effects, runAsInit: true,
subcontainer, env: {
currentCommand, TINI_SUBREAPER: "true",
{ },
runAsInit: true, sigtermTimeout: utils.inMs(this.system.manifest.main["sigterm-timeout"]),
env: {
TINI_SUBREAPER: "true",
},
sigtermTimeout: utils.inMs(
this.system.manifest.main["sigterm-timeout"],
),
},
)
}) })
daemon.start() daemon.start()
@@ -121,6 +120,7 @@ export class MainLoop {
? { ? {
preferredExternalPort: lanConf.external, preferredExternalPort: lanConf.external,
alpn: { specified: ["http/1.1"] }, alpn: { specified: ["http/1.1"] },
addXForwardedHeaders: false,
} }
: null, : null,
}) })
@@ -134,7 +134,7 @@ export class MainLoop {
delete this.mainEvent delete this.mainEvent
delete this.healthLoops delete this.healthLoops
await main?.daemon await main?.daemon
.stop() .term()
.catch((e: unknown) => console.error(`Main loop error`, utils.asError(e))) .catch((e: unknown) => console.error(`Main loop error`, utils.asError(e)))
this.effects.setMainStatus({ status: "stopped" }) this.effects.setMainStatus({ status: "stopped" })
if (healthLoops) healthLoops.forEach((x) => clearInterval(x.interval)) if (healthLoops) healthLoops.forEach((x) => clearInterval(x.interval))

View File

@@ -0,0 +1,153 @@
export default {
nodes: {
type: "list",
subtype: "union",
name: "Lightning Nodes",
description: "List of Lightning Network node instances to manage",
range: "[1,*)",
default: ["lnd"],
spec: {
type: "string",
"display-as": "{{name}}",
"unique-by": "name",
name: "Node Implementation",
tag: {
id: "type",
name: "Type",
description:
"- LND: Lightning Network Daemon from Lightning Labs\n- CLN: Core Lightning from Blockstream\n",
"variant-names": {
lnd: "Lightning Network Daemon (LND)",
"c-lightning": "Core Lightning (CLN)",
},
},
default: "lnd",
variants: {
lnd: {
name: {
type: "string",
name: "Node Name",
description: "Name of this node in the list",
default: "StartOS LND",
nullable: false,
},
"connection-settings": {
type: "union",
name: "Connection Settings",
description: "The Lightning Network Daemon node to connect to.",
tag: {
id: "type",
name: "Type",
description:
"- Internal: The Lightning Network Daemon service installed to your StartOS server.\n- External: A Lightning Network Daemon instance running on a remote device (advanced).\n",
"variant-names": {
internal: "Internal",
external: "External",
},
},
default: "internal",
variants: {
internal: {},
external: {
address: {
type: "string",
name: "Public Address",
description:
"The public address of your LND REST server\nNOTE: RTL does not support a .onion URL here\n",
nullable: false,
},
"rest-port": {
type: "number",
name: "REST Port",
description:
"The port that your Lightning Network Daemon REST server is bound to",
nullable: false,
range: "[0,65535]",
integral: true,
default: 8080,
},
macaroon: {
type: "string",
name: "Macaroon",
description:
'Your admin.macaroon file, Base64URL encoded. This is the same as the value after "macaroon=" in your lndconnect URL.',
nullable: false,
masked: true,
pattern: "[=A-Za-z0-9_-]+",
"pattern-description":
"Macaroon must be encoded in Base64URL format (only A-Z, a-z, 0-9, _, - and = allowed)",
},
},
},
},
},
"c-lightning": {
name: {
type: "string",
name: "Node Name",
description: "Name of this node in the list",
default: "StartOS CLN",
nullable: false,
},
"connection-settings": {
type: "union",
name: "Connection Settings",
description: "The Core Lightning (CLN) node to connect to.",
tag: {
id: "type",
name: "Type",
description:
"- Internal: The Core Lightning (CLN) service installed to your StartOS server.\n- External: A Core Lightning (CLN) instance running on a remote device (advanced).\n",
"variant-names": {
internal: "Internal",
external: "External",
},
},
default: "internal",
variants: {
internal: {},
external: {
address: {
type: "string",
name: "Public Address",
description:
"The public address of your CLNRest server\nNOTE: RTL does not support a .onion URL here\n",
nullable: false,
},
"rest-port": {
type: "number",
name: "CLNRest Port",
description: "The port that your CLNRest server is bound to",
nullable: false,
range: "[0,65535]",
integral: true,
default: 3010,
},
macaroon: {
type: "string",
name: "Rune",
description:
"Your CLNRest unrestricted Rune, Base64URL encoded.",
nullable: false,
masked: true,
},
},
},
},
},
},
},
},
password: {
type: "string",
name: "Password",
description: "The password for your Ride the Lightning dashboard",
nullable: false,
copyable: true,
masked: true,
default: {
charset: "a-z,A-Z,0-9",
len: 22,
},
},
}

View File

@@ -1,5 +1,30 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`transformConfigSpec transformConfigSpec(RTL) 1`] = `
{
"password": {
"default": {
"charset": "a-z,A-Z,0-9",
"len": 22,
},
"description": "The password for your Ride the Lightning dashboard",
"disabled": false,
"generate": null,
"immutable": false,
"inputmode": "text",
"masked": true,
"maxLength": null,
"minLength": null,
"name": "Password",
"patterns": [],
"placeholder": null,
"required": true,
"type": "text",
"warning": null,
},
}
`;
exports[`transformConfigSpec transformConfigSpec(bitcoind) 1`] = ` exports[`transformConfigSpec transformConfigSpec(bitcoind) 1`] = `
{ {
"advanced": { "advanced": {

View File

@@ -1,5 +1,8 @@
import { import {
ExtendedVersion, ExtendedVersion,
FileHelper,
getDataVersion,
overlaps,
types as T, types as T,
utils, utils,
VersionRange, VersionRange,
@@ -55,8 +58,21 @@ function todo(): never {
const MANIFEST_LOCATION = "/usr/lib/startos/package/embassyManifest.json" const MANIFEST_LOCATION = "/usr/lib/startos/package/embassyManifest.json"
export const EMBASSY_JS_LOCATION = "/usr/lib/startos/package/embassy.js" export const EMBASSY_JS_LOCATION = "/usr/lib/startos/package/embassy.js"
const EMBASSY_POINTER_PATH_PREFIX = "/embassyConfig" as utils.StorePath
const EMBASSY_DEPENDS_ON_PATH_PREFIX = "/embassyDependsOn" as utils.StorePath const configFile = FileHelper.json(
{
volumeId: "embassy",
subpath: "config.json",
},
matches.any,
)
const dependsOnFile = FileHelper.json(
{
volumeId: "embassy",
subpath: "dependsOn.json",
},
dictionary([string, array(string)]),
)
const matchResult = object({ const matchResult = object({
result: any, result: any,
@@ -94,47 +110,48 @@ const fromReturnType = <A>(a: U.ResultType<A>): A => {
return assertNever(a) return assertNever(a)
} }
const matchSetResult = object( const matchSetResult = object({
{ "depends-on": dictionary([string, array(string)])
"depends-on": dictionary([string, array(string)]), .nullable()
dependsOn: dictionary([string, array(string)]), .optional(),
signal: literals( dependsOn: dictionary([string, array(string)])
"SIGTERM", .nullable()
"SIGHUP", .optional(),
"SIGINT", signal: literals(
"SIGQUIT", "SIGTERM",
"SIGILL", "SIGHUP",
"SIGTRAP", "SIGINT",
"SIGABRT", "SIGQUIT",
"SIGBUS", "SIGILL",
"SIGFPE", "SIGTRAP",
"SIGKILL", "SIGABRT",
"SIGUSR1", "SIGBUS",
"SIGSEGV", "SIGFPE",
"SIGUSR2", "SIGKILL",
"SIGPIPE", "SIGUSR1",
"SIGALRM", "SIGSEGV",
"SIGSTKFLT", "SIGUSR2",
"SIGCHLD", "SIGPIPE",
"SIGCONT", "SIGALRM",
"SIGSTOP", "SIGSTKFLT",
"SIGTSTP", "SIGCHLD",
"SIGTTIN", "SIGCONT",
"SIGTTOU", "SIGSTOP",
"SIGURG", "SIGTSTP",
"SIGXCPU", "SIGTTIN",
"SIGXFSZ", "SIGTTOU",
"SIGVTALRM", "SIGURG",
"SIGPROF", "SIGXCPU",
"SIGWINCH", "SIGXFSZ",
"SIGIO", "SIGVTALRM",
"SIGPWR", "SIGPROF",
"SIGSYS", "SIGWINCH",
"SIGINFO", "SIGIO",
), "SIGPWR",
}, "SIGSYS",
["depends-on", "dependsOn"], "SIGINFO",
) ),
})
type OldGetConfigRes = { type OldGetConfigRes = {
config?: null | Record<string, unknown> config?: null | Record<string, unknown>
@@ -174,14 +191,14 @@ export type PackagePropertiesV2 = {
} }
export type PackagePropertyString = { export type PackagePropertyString = {
type: "string" type: "string"
description?: string description?: string | null
value: string value: string
/** Let's the ui make this copyable button */ /** Let's the ui make this copyable button */
copyable?: boolean copyable?: boolean | null
/** Let the ui create a qr for this field */ /** Let the ui create a qr for this field */
qr?: boolean qr?: boolean | null
/** Hiding the value unless toggled off for field */ /** Hiding the value unless toggled off for field */
masked?: boolean masked?: boolean | null
} }
export type PackagePropertyObject = { export type PackagePropertyObject = {
value: PackagePropertiesV2 value: PackagePropertiesV2
@@ -225,17 +242,14 @@ const matchPackagePropertyObject: Parser<unknown, PackagePropertyObject> =
}) })
const matchPackagePropertyString: Parser<unknown, PackagePropertyString> = const matchPackagePropertyString: Parser<unknown, PackagePropertyString> =
object( object({
{ type: literal("string"),
type: literal("string"), description: string.nullable().optional(),
description: string, value: string,
value: string, copyable: boolean.nullable().optional(),
copyable: boolean, qr: boolean.nullable().optional(),
qr: boolean, masked: boolean.nullable().optional(),
masked: boolean, })
},
["copyable", "description", "qr", "masked"],
)
setMatchPackageProperties( setMatchPackageProperties(
dictionary([ dictionary([
string, string,
@@ -275,6 +289,7 @@ function convertProperties(
const DEFAULT_REGISTRY = "https://registry.start9.com" const DEFAULT_REGISTRY = "https://registry.start9.com"
export class SystemForEmbassy implements System { export class SystemForEmbassy implements System {
private version: ExtendedVersion
currentRunning: MainLoop | undefined currentRunning: MainLoop | undefined
static async of(manifestLocation: string = MANIFEST_LOCATION) { static async of(manifestLocation: string = MANIFEST_LOCATION) {
const moduleCode = await import(EMBASSY_JS_LOCATION) const moduleCode = await import(EMBASSY_JS_LOCATION)
@@ -296,11 +311,37 @@ export class SystemForEmbassy implements System {
constructor( constructor(
readonly manifest: Manifest, readonly manifest: Manifest,
readonly moduleCode: Partial<U.ExpectedExports>, readonly moduleCode: Partial<U.ExpectedExports>,
) {} ) {
this.version = ExtendedVersion.parseEmver(manifest.version)
if (
this.manifest.id === "bitcoind" &&
this.manifest.title.toLowerCase().includes("knots")
)
this.version.flavor = "knots"
async containerInit(effects: Effects): Promise<void> { if (
this.manifest.id === "lnd" ||
this.manifest.id === "ride-the-lightning" ||
this.manifest.id === "datum"
) {
this.version.upstream.prerelease = ["beta"]
} else if (
this.manifest.id === "lightning-terminal" ||
this.manifest.id === "robosats"
) {
this.version.upstream.prerelease = ["alpha"]
}
}
async init(
effects: Effects,
kind: "install" | "update" | "restore" | null,
): Promise<void> {
if (kind === "restore") {
await this.restoreBackup(effects, null)
}
for (let depId in this.manifest.dependencies) { for (let depId in this.manifest.dependencies) {
if (this.manifest.dependencies[depId].config) { if (this.manifest.dependencies[depId]?.config) {
await this.dependenciesAutoconfig(effects, depId, null) await this.dependenciesAutoconfig(effects, depId, null)
} }
} }
@@ -308,6 +349,9 @@ export class SystemForEmbassy implements System {
await this.exportActions(effects) await this.exportActions(effects)
await this.exportNetwork(effects) await this.exportNetwork(effects)
await this.containerSetDependencies(effects) await this.containerSetDependencies(effects)
if (kind === "install" || kind === "update") {
await this.packageInit(effects, null)
}
} }
async containerSetDependencies(effects: T.Effects) { async containerSetDependencies(effects: T.Effects) {
const oldDeps: Record<string, string[]> = Object.fromEntries( const oldDeps: Record<string, string[]> = Object.fromEntries(
@@ -347,16 +391,23 @@ export class SystemForEmbassy implements System {
} }
async packageInit(effects: Effects, timeoutMs: number | null): Promise<void> { async packageInit(effects: Effects, timeoutMs: number | null): Promise<void> {
const previousVersion = await effects.getDataVersion() const previousVersion = await getDataVersion(effects)
if (previousVersion) { if (previousVersion) {
if ( const migrationRes = await this.migration(
(await this.migration(effects, { from: previousVersion }, timeoutMs)) effects,
.configured { from: previousVersion },
) { timeoutMs,
await effects.action.clearRequests({ only: ["needs-config"] }) )
if (migrationRes) {
if (migrationRes.configured)
await effects.action.clearTasks({ only: ["needs-config"] })
await configFile.write(
effects,
await this.getConfig(effects, timeoutMs),
)
} }
} else if (this.manifest.config) { } else if (this.manifest.config) {
await effects.action.request({ await effects.action.createTask({
packageId: this.manifest.id, packageId: this.manifest.id,
actionId: "config", actionId: "config",
severity: "critical", severity: "critical",
@@ -364,9 +415,11 @@ export class SystemForEmbassy implements System {
reason: "This service must be configured before it can be run", reason: "This service must be configured before it can be run",
}) })
} }
await effects.setDataVersion({ await effects.setDataVersion({
version: ExtendedVersion.parseEmver(this.manifest.version).toString(), version: this.version.toString(),
}) })
// @FullMetal: package hacks go here
} }
async exportNetwork(effects: Effects) { async exportNetwork(effects: Effects) {
for (const [id, interfaceValue] of Object.entries( for (const [id, interfaceValue] of Object.entries(
@@ -403,6 +456,7 @@ export class SystemForEmbassy implements System {
addSsl = { addSsl = {
preferredExternalPort: lanPortNum, preferredExternalPort: lanPortNum,
alpn: { specified: [] }, alpn: { specified: [] },
addXForwardedHeaders: false,
} }
} }
return [ return [
@@ -441,7 +495,7 @@ export class SystemForEmbassy implements System {
masked: false, masked: false,
path: "", path: "",
schemeOverride: null, schemeOverride: null,
search: {}, query: {},
username: null, username: null,
}), }),
]) ])
@@ -456,13 +510,18 @@ export class SystemForEmbassy implements System {
): Promise<T.ActionInput | null> { ): Promise<T.ActionInput | null> {
if (actionId === "config") { if (actionId === "config") {
const config = await this.getConfig(effects, timeoutMs) const config = await this.getConfig(effects, timeoutMs)
return { spec: config.spec, value: config.config } return {
eventId: effects.eventId!,
spec: config.spec,
value: config.config,
}
} else if (actionId === "properties") { } else if (actionId === "properties") {
return null return null
} else { } else {
const oldSpec = this.manifest.actions?.[actionId]?.["input-spec"] const oldSpec = this.manifest.actions?.[actionId]?.["input-spec"]
if (!oldSpec) return null if (!oldSpec) return null
return { return {
eventId: effects.eventId!,
spec: transformConfigSpec(oldSpec as OldConfigSpec), spec: transformConfigSpec(oldSpec as OldConfigSpec),
value: null, value: null,
} }
@@ -543,14 +602,14 @@ export class SystemForEmbassy implements System {
} }
await effects.action.clear({ except: Object.keys(actions) }) await effects.action.clear({ except: Object.keys(actions) })
} }
async packageUninit( async uninit(
effects: Effects, effects: Effects,
nextVersion: Optional<string>, target: ExtendedVersion | VersionRange | null,
timeoutMs: number | null, timeoutMs?: number | null,
): Promise<void> { ): Promise<void> {
await this.currentRunning?.clean({ timeout: timeoutMs ?? undefined }) await this.currentRunning?.clean({ timeout: timeoutMs ?? undefined })
if (nextVersion) { if (target) {
await this.migration(effects, { to: nextVersion }, timeoutMs) await this.migration(effects, { to: target }, timeoutMs ?? null)
} }
await effects.setMainStatus({ status: "stopped" }) await effects.setMainStatus({ status: "stopped" })
} }
@@ -577,11 +636,21 @@ export class SystemForEmbassy implements System {
const moduleCode = await this.moduleCode const moduleCode = await this.moduleCode
await moduleCode.createBackup?.(polyfillEffects(effects, this.manifest)) await moduleCode.createBackup?.(polyfillEffects(effects, this.manifest))
} }
const dataVersion = await effects.getDataVersion()
if (dataVersion)
await fs.writeFile("/media/startos/backup/dataVersion.txt", dataVersion, {
encoding: "utf-8",
})
} }
async restoreBackup( async restoreBackup(
effects: Effects, effects: Effects,
timeoutMs: number | null, timeoutMs: number | null,
): Promise<void> { ): Promise<void> {
const store = await fs
.readFile("/media/startos/backup/store.json", {
encoding: "utf-8",
})
.catch((_) => null)
const restoreBackup = this.manifest.backup.restore const restoreBackup = this.manifest.backup.restore
if (restoreBackup.type === "docker") { if (restoreBackup.type === "docker") {
const commands = [restoreBackup.entrypoint, ...restoreBackup.args] const commands = [restoreBackup.entrypoint, ...restoreBackup.args]
@@ -600,6 +669,13 @@ export class SystemForEmbassy implements System {
const moduleCode = await this.moduleCode const moduleCode = await this.moduleCode
await moduleCode.restoreBackup?.(polyfillEffects(effects, this.manifest)) await moduleCode.restoreBackup?.(polyfillEffects(effects, this.manifest))
} }
const dataVersion = await fs
.readFile("/media/startos/backup/dataVersion.txt", {
encoding: "utf-8",
})
.catch((_) => null)
if (dataVersion) await effects.setDataVersion({ version: dataVersion })
} }
async getConfig(effects: Effects, timeoutMs: number | null) { async getConfig(effects: Effects, timeoutMs: number | null) {
return this.getConfigUncleaned(effects, timeoutMs).then(convertToNewConfig) return this.getConfigUncleaned(effects, timeoutMs).then(convertToNewConfig)
@@ -649,10 +725,7 @@ export class SystemForEmbassy implements System {
structuredClone(newConfigWithoutPointers as Record<string, unknown>), structuredClone(newConfigWithoutPointers as Record<string, unknown>),
) )
await updateConfig(effects, this.manifest, spec, newConfig) await updateConfig(effects, this.manifest, spec, newConfig)
await effects.store.set({ await configFile.write(effects, newConfig)
path: EMBASSY_POINTER_PATH_PREFIX,
value: newConfig,
})
const setConfigValue = this.manifest.config?.set const setConfigValue = this.manifest.config?.set
if (!setConfigValue) return if (!setConfigValue) return
if (setConfigValue.type === "docker") { if (setConfigValue.type === "docker") {
@@ -706,15 +779,11 @@ export class SystemForEmbassy implements System {
rawDepends: { [x: string]: readonly string[] }, rawDepends: { [x: string]: readonly string[] },
configuring: boolean, configuring: boolean,
) { ) {
const storedDependsOn = (await effects.store.get({ const storedDependsOn = await dependsOnFile.read().once()
packageId: this.manifest.id,
path: EMBASSY_DEPENDS_ON_PATH_PREFIX,
})) as Record<string, readonly string[]>
const requiredDeps = { const requiredDeps = {
...Object.fromEntries( ...Object.fromEntries(
Object.entries(this.manifest.dependencies || {}) Object.entries(this.manifest.dependencies ?? {})
?.filter((x) => x[1].requirement.type === "required") .filter(([k, v]) => v?.requirement.type === "required")
.map((x) => [x[0], []]) || [], .map((x) => [x[0], []]) || [],
), ),
} }
@@ -728,10 +797,7 @@ export class SystemForEmbassy implements System {
? storedDependsOn ? storedDependsOn
: requiredDeps : requiredDeps
await effects.store.set({ await dependsOnFile.write(effects, dependsOn)
path: EMBASSY_DEPENDS_ON_PATH_PREFIX,
value: dependsOn,
})
await effects.setDependencies({ await effects.setDependencies({
dependencies: Object.entries(dependsOn).flatMap( dependencies: Object.entries(dependsOn).flatMap(
@@ -755,31 +821,33 @@ export class SystemForEmbassy implements System {
async migration( async migration(
effects: Effects, effects: Effects,
version: { from: string } | { to: string }, version:
| { from: VersionRange | ExtendedVersion }
| { to: VersionRange | ExtendedVersion },
timeoutMs: number | null, timeoutMs: number | null,
): Promise<{ configured: boolean }> { ): Promise<{ configured: boolean } | null> {
let migration let migration
let args: [string, ...string[]] let args: [string, ...string[]]
if ("from" in version) { if ("from" in version) {
args = [version.from, "from"] if (overlaps(this.version, version.from)) return null
const fromExver = ExtendedVersion.parse(version.from) args = [version.from.toString(), "from"]
if (!this.manifest.migrations) return { configured: true } if (!this.manifest.migrations) return { configured: true }
migration = Object.entries(this.manifest.migrations.from) migration = Object.entries(this.manifest.migrations.from)
.map( .map(
([version, procedure]) => ([version, procedure]) =>
[VersionRange.parseEmver(version), procedure] as const, [VersionRange.parseEmver(version), procedure] as const,
) )
.find(([versionEmver, _]) => versionEmver.satisfiedBy(fromExver)) .find(([versionEmver, _]) => overlaps(versionEmver, version.from))
} else { } else {
args = [version.to, "to"] if (overlaps(this.version, version.to)) return null
const toExver = ExtendedVersion.parse(version.to) args = [version.to.toString(), "to"]
if (!this.manifest.migrations) return { configured: true } if (!this.manifest.migrations) return { configured: true }
migration = Object.entries(this.manifest.migrations.to) migration = Object.entries(this.manifest.migrations.to)
.map( .map(
([version, procedure]) => ([version, procedure]) =>
[VersionRange.parseEmver(version), procedure] as const, [VersionRange.parseEmver(version), procedure] as const,
) )
.find(([versionEmver, _]) => versionEmver.satisfiedBy(toExver)) .find(([versionEmver, _]) => overlaps(versionEmver, version.to))
} }
if (migration) { if (migration) {
@@ -815,13 +883,12 @@ export class SystemForEmbassy implements System {
})) as any })) as any
} }
} }
return { configured: true } return null
} }
async properties( async properties(
effects: Effects, effects: Effects,
timeoutMs: number | null, timeoutMs: number | null,
): Promise<PropertiesReturn> { ): Promise<PropertiesReturn> {
// TODO BLU-J set the properties ever so often
const setConfigValue = this.manifest.properties const setConfigValue = this.manifest.properties
if (!setConfigValue) throw new Error("There is no properties") if (!setConfigValue) throw new Error("There is no properties")
if (setConfigValue.type === "docker") { if (setConfigValue.type === "docker") {
@@ -969,43 +1036,52 @@ export class SystemForEmbassy implements System {
timeoutMs: number | null, timeoutMs: number | null,
): Promise<void> { ): Promise<void> {
// TODO: docker // TODO: docker
const oldConfig = (await effects.store.get({ await effects.mount({
packageId: id, location: `/media/embassy/${id}`,
path: EMBASSY_POINTER_PATH_PREFIX, target: {
callback: () => {
this.dependenciesAutoconfig(effects, id, timeoutMs)
},
})) as U.Config
if (!oldConfig) return
const moduleCode = await this.moduleCode
const method = moduleCode?.dependencies?.[id]?.autoConfigure
if (!method) return
const newConfig = (await method(
polyfillEffects(effects, this.manifest),
JSON.parse(JSON.stringify(oldConfig)),
).then((x) => {
if ("result" in x) return x.result
if ("error" in x) throw new Error("Error getting config: " + x.error)
throw new Error("Error getting config: " + x["error-code"][1])
})) as any
const diff = partialDiff(oldConfig, newConfig)
if (diff) {
await effects.action.request({
actionId: "config",
packageId: id, packageId: id,
replayId: `${id}/config`, volumeId: "embassy",
severity: "important", subpath: null,
reason: `Configure this dependency for the needs of ${this.manifest.title}`, readonly: true,
input: { idmap: [],
kind: "partial", },
value: diff.diff, })
}, configFile
when: { .withPath(`/media/embassy/${id}/config.json`)
condition: "input-not-matches", .read()
once: false, .onChange(effects, async (oldConfig: U.Config) => {
}, if (!oldConfig) return { cancel: false }
const moduleCode = await this.moduleCode
const method = moduleCode?.dependencies?.[id]?.autoConfigure
if (!method) return { cancel: true }
const newConfig = (await method(
polyfillEffects(effects, this.manifest),
JSON.parse(JSON.stringify(oldConfig)),
).then((x) => {
if ("result" in x) return x.result
if ("error" in x) throw new Error("Error getting config: " + x.error)
throw new Error("Error getting config: " + x["error-code"][1])
})) as any
const diff = partialDiff(oldConfig, newConfig)
if (diff) {
await effects.action.createTask({
actionId: "config",
packageId: id,
replayId: `${id}/config`,
severity: "important",
reason: `Configure this dependency for the needs of ${this.manifest.title}`,
input: {
kind: "partial",
value: diff.diff,
},
when: {
condition: "input-not-matches",
once: false,
},
})
}
return { cancel: false }
}) })
}
} }
} }
@@ -1107,11 +1183,21 @@ async function updateConfig(
) { ) {
if (specValue.target === "config") { if (specValue.target === "config") {
const jp = require("jsonpath") const jp = require("jsonpath")
const remoteConfig = await effects.store.get({ const depId = specValue["package-id"]
packageId: specValue["package-id"], await effects.mount({
callback: () => effects.restart(), location: `/media/embassy/${depId}`,
path: EMBASSY_POINTER_PATH_PREFIX, target: {
packageId: depId,
volumeId: "embassy",
subpath: null,
readonly: true,
idmap: [],
},
}) })
const remoteConfig = configFile
.withPath(`/media/embassy/${depId}/config.json`)
.read()
.once()
console.debug(remoteConfig) console.debug(remoteConfig)
const configValue = specValue.multi const configValue = specValue.multi
? jp.query(remoteConfig, specValue.selector) ? jp.query(remoteConfig, specValue.selector)
@@ -1152,14 +1238,14 @@ async function updateConfig(
const url: string = const url: string =
filled === null || filled.addressInfo === null filled === null || filled.addressInfo === null
? "" ? ""
: catchFn(() => : catchFn(
utils.hostnameInfoToAddress( () =>
specValue.target === "lan-address" (specValue.target === "lan-address"
? filled.addressInfo!.localHostnames[0] || ? filled.addressInfo!.filter({ kind: "mdns" }) ||
filled.addressInfo!.onionHostnames[0] filled.addressInfo!.onion
: filled.addressInfo!.onionHostnames[0] || : filled.addressInfo!.onion ||
filled.addressInfo!.localHostnames[0], filled.addressInfo!.filter({ kind: "mdns" })
), ).hostnames[0].hostname.value,
) || "" ) || ""
mutConfigValue[key] = url mutConfigValue[key] = url
} }

View File

@@ -14,123 +14,113 @@ import {
import { matchVolume } from "./matchVolume" import { matchVolume } from "./matchVolume"
import { matchDockerProcedure } from "../../../Models/DockerProcedure" import { matchDockerProcedure } from "../../../Models/DockerProcedure"
const matchJsProcedure = object( const matchJsProcedure = object({
{ type: literal("script"),
type: literal("script"), args: array(unknown).nullable().optional().defaultTo([]),
args: array(unknown), })
},
["args"],
{
args: [],
},
)
const matchProcedure = some(matchDockerProcedure, matchJsProcedure) const matchProcedure = some(matchDockerProcedure, matchJsProcedure)
export type Procedure = typeof matchProcedure._TYPE export type Procedure = typeof matchProcedure._TYPE
const matchAction = object( const matchAction = object({
{ name: string,
name: string, description: string,
description: string, warning: string.nullable().optional(),
warning: string, implementation: matchProcedure,
implementation: matchProcedure, "allowed-statuses": array(literals("running", "stopped")),
"allowed-statuses": array(literals("running", "stopped")), "input-spec": unknown.nullable().optional(),
"input-spec": unknown, })
}, export const matchManifest = object({
["warning", "input-spec", "input-spec"], id: string,
) title: string,
export const matchManifest = object( version: string,
{ main: matchDockerProcedure,
id: string, assets: object({
title: string, assets: string.nullable().optional(),
version: string, scripts: string.nullable().optional(),
main: matchDockerProcedure, })
assets: object( .nullable()
{ .optional(),
assets: string, "health-checks": dictionary([
scripts: string, string,
}, every(
["assets", "scripts"], matchProcedure,
object({
name: string,
["success-message"]: string.nullable().optional(),
}),
), ),
"health-checks": dictionary([ ]),
string, config: object({
every( get: matchProcedure,
matchProcedure, set: matchProcedure,
object( })
{ .nullable()
name: string, .optional(),
["success-message"]: string, properties: matchProcedure.nullable().optional(),
}, volumes: dictionary([string, matchVolume]),
["success-message"], interfaces: dictionary([
), string,
), object({
]), name: string,
config: object({ description: string,
get: matchProcedure, "tor-config": object({
set: matchProcedure, "port-mapping": dictionary([string, string]),
})
.nullable()
.optional(),
"lan-config": dictionary([
string,
object({
ssl: boolean,
internal: number,
}),
])
.nullable()
.optional(),
ui: boolean,
protocols: array(string),
}), }),
properties: matchProcedure, ]),
volumes: dictionary([string, matchVolume]), backup: object({
interfaces: dictionary([ create: matchProcedure,
string, restore: matchProcedure,
object( }),
{ migrations: object({
name: string, to: dictionary([string, matchProcedure]),
description: string, from: dictionary([string, matchProcedure]),
"tor-config": object({ })
"port-mapping": dictionary([string, string]), .nullable()
}), .optional(),
"lan-config": dictionary([ dependencies: dictionary([
string, string,
object({ object({
ssl: boolean, version: string,
internal: number, requirement: some(
}), object({
]), type: literal("opt-in"),
ui: boolean, how: string,
protocols: array(string), }),
}, object({
["lan-config", "tor-config"], type: literal("opt-out"),
how: string,
}),
object({
type: literal("required"),
}),
), ),
]), description: string.nullable().optional(),
backup: object({ config: object({
create: matchProcedure, check: matchProcedure,
restore: matchProcedure, "auto-configure": matchProcedure,
}), })
migrations: object({ .nullable()
to: dictionary([string, matchProcedure]), .optional(),
from: dictionary([string, matchProcedure]), })
}), .nullable()
dependencies: dictionary([ .optional(),
string, ]),
object(
{
version: string,
requirement: some(
object({
type: literal("opt-in"),
how: string,
}),
object({
type: literal("opt-out"),
how: string,
}),
object({
type: literal("required"),
}),
),
description: string,
config: object({
check: matchProcedure,
"auto-configure": matchProcedure,
}),
},
["description", "config"],
),
]),
actions: dictionary([string, matchAction]), actions: dictionary([string, matchAction]),
}, })
["config", "actions", "properties", "migrations", "dependencies"],
)
export type Manifest = typeof matchManifest._TYPE export type Manifest = typeof matchManifest._TYPE

View File

@@ -1,12 +1,9 @@
import { object, literal, string, boolean, some } from "ts-matches" import { object, literal, string, boolean, some } from "ts-matches"
const matchDataVolume = object( const matchDataVolume = object({
{ type: literal("data"),
type: literal("data"), readonly: boolean.optional(),
readonly: boolean, })
},
["readonly"],
)
const matchAssetVolume = object({ const matchAssetVolume = object({
type: literal("assets"), type: literal("assets"),
}) })

View File

@@ -135,12 +135,9 @@ export const polyfillEffects = (
[input.command, ...(input.args || [])].join(" "), [input.command, ...(input.args || [])].join(" "),
) )
const daemon = promiseSubcontainer.then((subcontainer) => const daemon = promiseSubcontainer.then((subcontainer) =>
daemons.runCommand()( daemons.runCommand()(effects, subcontainer, {
effects, command: [input.command, ...(input.args || [])],
subcontainer, }),
[input.command, ...(input.args || [])],
{},
),
) )
return { return {
wait: () => wait: () =>
@@ -169,12 +166,12 @@ export const polyfillEffects = (
{ imageId: manifest.main.image }, { imageId: manifest.main.image },
commands, commands,
{ {
mounts: Mounts.of().addVolume( mounts: Mounts.of().mountVolume({
input.volumeId, volumeId: input.volumeId,
null, subpath: null,
"/drive", mountpoint: "/drive",
false, readonly: false,
), }),
}, },
commands.join(" "), commands.join(" "),
) )
@@ -206,12 +203,12 @@ export const polyfillEffects = (
{ imageId: manifest.main.image }, { imageId: manifest.main.image },
commands, commands,
{ {
mounts: Mounts.of().addVolume( mounts: Mounts.of().mountVolume({
input.volumeId, volumeId: input.volumeId,
null, subpath: null,
"/drive", mountpoint: "/drive",
false, readonly: false,
), }),
}, },
commands.join(" "), commands.join(" "),
) )

View File

@@ -1,5 +1,10 @@
import { matchOldConfigSpec, transformConfigSpec } from "./transformConfigSpec" import {
import fixtureEmbasyPagesConfig from "./__fixtures__/embasyPagesConfig" matchOldConfigSpec,
matchOldValueSpecList,
transformConfigSpec,
} from "./transformConfigSpec"
import fixtureEmbassyPagesConfig from "./__fixtures__/embassyPagesConfig"
import fixtureRTLConfig from "./__fixtures__/rtlConfig"
import searNXG from "./__fixtures__/searNXG" import searNXG from "./__fixtures__/searNXG"
import bitcoind from "./__fixtures__/bitcoind" import bitcoind from "./__fixtures__/bitcoind"
import nostr from "./__fixtures__/nostr" import nostr from "./__fixtures__/nostr"
@@ -8,14 +13,25 @@ import nostrConfig2 from "./__fixtures__/nostrConfig2"
describe("transformConfigSpec", () => { describe("transformConfigSpec", () => {
test("matchOldConfigSpec(embassyPages.homepage.variants[web-page])", () => { test("matchOldConfigSpec(embassyPages.homepage.variants[web-page])", () => {
matchOldConfigSpec.unsafeCast( matchOldConfigSpec.unsafeCast(
fixtureEmbasyPagesConfig.homepage.variants["web-page"], fixtureEmbassyPagesConfig.homepage.variants["web-page"],
) )
}) })
test("matchOldConfigSpec(embassyPages)", () => { test("matchOldConfigSpec(embassyPages)", () => {
matchOldConfigSpec.unsafeCast(fixtureEmbasyPagesConfig) matchOldConfigSpec.unsafeCast(fixtureEmbassyPagesConfig)
}) })
test("transformConfigSpec(embassyPages)", () => { test("transformConfigSpec(embassyPages)", () => {
const spec = matchOldConfigSpec.unsafeCast(fixtureEmbasyPagesConfig) const spec = matchOldConfigSpec.unsafeCast(fixtureEmbassyPagesConfig)
expect(transformConfigSpec(spec)).toMatchSnapshot()
})
test("matchOldConfigSpec(RTL.nodes)", () => {
matchOldValueSpecList.unsafeCast(fixtureRTLConfig.nodes)
})
test("matchOldConfigSpec(RTL)", () => {
matchOldConfigSpec.unsafeCast(fixtureRTLConfig)
})
test("transformConfigSpec(RTL)", () => {
const spec = matchOldConfigSpec.unsafeCast(fixtureRTLConfig)
expect(transformConfigSpec(spec)).toMatchSnapshot() expect(transformConfigSpec(spec)).toMatchSnapshot()
}) })

View File

@@ -47,6 +47,7 @@ export function transformConfigSpec(oldSpec: OldConfigSpec): IST.InputSpec {
immutable: false, immutable: false,
} }
} else if (oldVal.type === "list") { } else if (oldVal.type === "list") {
if (isUnionList(oldVal)) return inputSpec
newVal = getListSpec(oldVal) newVal = getListSpec(oldVal)
} else if (oldVal.type === "number") { } else if (oldVal.type === "number") {
const range = Range.from(oldVal.range) const range = Range.from(oldVal.range)
@@ -177,15 +178,17 @@ export function transformOldConfigToNew(
} }
} }
if (isList(val) && isObjectList(val)) { if (isList(val)) {
if (!config[key]) return obj if (!config[key]) return obj
newVal = (config[key] as object[]).map((obj) => if (isObjectList(val)) {
transformOldConfigToNew( newVal = (config[key] as object[]).map((obj) =>
matchOldConfigSpec.unsafeCast(val.spec.spec), transformOldConfigToNew(
obj, matchOldConfigSpec.unsafeCast(val.spec.spec),
), obj,
) ),
)
} else if (isUnionList(val)) return obj
} }
if (isPointer(val)) { if (isPointer(val)) {
@@ -203,6 +206,7 @@ export function transformNewConfigToOld(
spec: OldConfigSpec, spec: OldConfigSpec,
config: Record<string, any>, config: Record<string, any>,
): Record<string, any> { ): Record<string, any> {
if (!config) return config
return Object.entries(spec).reduce((obj, [key, val]) => { return Object.entries(spec).reduce((obj, [key, val]) => {
let newVal = config[key] let newVal = config[key]
@@ -223,13 +227,15 @@ export function transformNewConfigToOld(
} }
} }
if (isList(val) && isObjectList(val)) { if (isList(val)) {
newVal = (config[key] as object[]).map((obj) => if (isObjectList(val)) {
transformNewConfigToOld( newVal = (config[key] as object[]).map((obj) =>
matchOldConfigSpec.unsafeCast(val.spec.spec), transformNewConfigToOld(
obj, matchOldConfigSpec.unsafeCast(val.spec.spec),
), obj,
) ),
)
} else if (isUnionList(val)) return obj
} }
return { return {
@@ -375,15 +381,17 @@ function isNumberList(
): val is OldValueSpecList & { subtype: "number" } { ): val is OldValueSpecList & { subtype: "number" } {
return val.subtype === "number" return val.subtype === "number"
} }
function isObjectList( function isObjectList(
val: OldValueSpecList, val: OldValueSpecList,
): val is OldValueSpecList & { subtype: "object" } { ): val is OldValueSpecList & { subtype: "object" } {
if (["union"].includes(val.subtype)) {
throw new Error("Invalid list subtype. enum, string, and object permitted.")
}
return val.subtype === "object" return val.subtype === "object"
} }
function isUnionList(
val: OldValueSpecList,
): val is OldValueSpecList & { subtype: "union" } {
return val.subtype === "union"
}
export type OldConfigSpec = Record<string, OldValueSpec> export type OldConfigSpec = Record<string, OldValueSpec>
const [_matchOldConfigSpec, setMatchOldConfigSpec] = deferred<unknown>() const [_matchOldConfigSpec, setMatchOldConfigSpec] = deferred<unknown>()
export const matchOldConfigSpec = _matchOldConfigSpec as Parser< export const matchOldConfigSpec = _matchOldConfigSpec as Parser<
@@ -396,100 +404,71 @@ export const matchOldDefaultString = anyOf(
) )
type OldDefaultString = typeof matchOldDefaultString._TYPE type OldDefaultString = typeof matchOldDefaultString._TYPE
export const matchOldValueSpecString = object( export const matchOldValueSpecString = object({
{ type: literals("string"),
type: literals("string"), name: string,
name: string, masked: boolean.nullable().optional(),
masked: boolean, copyable: boolean.nullable().optional(),
copyable: boolean, nullable: boolean.nullable().optional(),
nullable: boolean, placeholder: string.nullable().optional(),
placeholder: string, pattern: string.nullable().optional(),
pattern: string, "pattern-description": string.nullable().optional(),
"pattern-description": string, default: matchOldDefaultString.nullable().optional(),
default: matchOldDefaultString, textarea: boolean.nullable().optional(),
textarea: boolean, description: string.nullable().optional(),
description: string, warning: string.nullable().optional(),
warning: string, })
},
[
"masked",
"copyable",
"nullable",
"placeholder",
"pattern",
"pattern-description",
"default",
"textarea",
"description",
"warning",
],
)
export const matchOldValueSpecNumber = object( export const matchOldValueSpecNumber = object({
{ type: literals("number"),
type: literals("number"), nullable: boolean,
nullable: boolean, name: string,
name: string, range: string,
range: string, integral: boolean,
integral: boolean, default: number.nullable().optional(),
default: number, description: string.nullable().optional(),
description: string, warning: string.nullable().optional(),
warning: string, units: string.nullable().optional(),
units: string, placeholder: anyOf(number, string).nullable().optional(),
placeholder: anyOf(number, string), })
},
["default", "description", "warning", "units", "placeholder"],
)
type OldValueSpecNumber = typeof matchOldValueSpecNumber._TYPE type OldValueSpecNumber = typeof matchOldValueSpecNumber._TYPE
export const matchOldValueSpecBoolean = object( export const matchOldValueSpecBoolean = object({
{ type: literals("boolean"),
type: literals("boolean"), default: boolean,
default: boolean, name: string,
name: string, description: string.nullable().optional(),
description: string, warning: string.nullable().optional(),
warning: string, })
},
["description", "warning"],
)
type OldValueSpecBoolean = typeof matchOldValueSpecBoolean._TYPE type OldValueSpecBoolean = typeof matchOldValueSpecBoolean._TYPE
const matchOldValueSpecObject = object( const matchOldValueSpecObject = object({
{ type: literals("object"),
type: literals("object"), spec: _matchOldConfigSpec,
spec: _matchOldConfigSpec, name: string,
name: string, description: string.nullable().optional(),
description: string, warning: string.nullable().optional(),
warning: string, })
},
["description", "warning"],
)
type OldValueSpecObject = typeof matchOldValueSpecObject._TYPE type OldValueSpecObject = typeof matchOldValueSpecObject._TYPE
const matchOldValueSpecEnum = object( const matchOldValueSpecEnum = object({
{ values: array(string),
values: array(string), "value-names": dictionary([string, string]),
"value-names": dictionary([string, string]), type: literals("enum"),
type: literals("enum"), default: string,
default: string, name: string,
name: string, description: string.nullable().optional(),
description: string, warning: string.nullable().optional(),
warning: string, })
},
["description", "warning"],
)
type OldValueSpecEnum = typeof matchOldValueSpecEnum._TYPE type OldValueSpecEnum = typeof matchOldValueSpecEnum._TYPE
const matchOldUnionTagSpec = object( const matchOldUnionTagSpec = object({
{ id: string, // The name of the field containing one of the union variants
id: string, // The name of the field containing one of the union variants "variant-names": dictionary([string, string]), // The name of each variant
"variant-names": dictionary([string, string]), // The name of each variant name: string,
name: string, description: string.nullable().optional(),
description: string, warning: string.nullable().optional(),
warning: string, })
},
["description", "warning"],
)
const matchOldValueSpecUnion = object({ const matchOldValueSpecUnion = object({
type: literals("union"), type: literals("union"),
tag: matchOldUnionTagSpec, tag: matchOldUnionTagSpec,
@@ -514,57 +493,51 @@ setOldUniqueBy(
), ),
) )
const matchOldListValueSpecObject = object( const matchOldListValueSpecObject = object({
{ spec: _matchOldConfigSpec, // this is a mapped type of the config object at this level, replacing the object's values with specs on those values
spec: _matchOldConfigSpec, // this is a mapped type of the config object at this level, replacing the object's values with specs on those values "unique-by": matchOldUniqueBy.nullable().optional(), // indicates whether duplicates can be permitted in the list
"unique-by": matchOldUniqueBy, // indicates whether duplicates can be permitted in the list "display-as": string.nullable().optional(), // this should be a handlebars template which can make use of the entire config which corresponds to 'spec'
"display-as": string, // this should be a handlebars template which can make use of the entire config which corresponds to 'spec' })
}, const matchOldListValueSpecUnion = object({
["display-as", "unique-by"], "unique-by": matchOldUniqueBy.nullable().optional(),
) "display-as": string.nullable().optional(),
const matchOldListValueSpecString = object( tag: matchOldUnionTagSpec,
{ variants: dictionary([string, _matchOldConfigSpec]),
masked: boolean, })
copyable: boolean, const matchOldListValueSpecString = object({
pattern: string, masked: boolean.nullable().optional(),
"pattern-description": string, copyable: boolean.nullable().optional(),
placeholder: string, pattern: string.nullable().optional(),
}, "pattern-description": string.nullable().optional(),
["pattern", "pattern-description", "placeholder", "copyable", "masked"], placeholder: string.nullable().optional(),
) })
const matchOldListValueSpecEnum = object({ const matchOldListValueSpecEnum = object({
values: array(string), values: array(string),
"value-names": dictionary([string, string]), "value-names": dictionary([string, string]),
}) })
const matchOldListValueSpecNumber = object( const matchOldListValueSpecNumber = object({
{ range: string,
range: string, integral: boolean,
integral: boolean, units: string.nullable().optional(),
units: string, placeholder: anyOf(number, string).nullable().optional(),
placeholder: anyOf(number, string), })
},
["units", "placeholder"],
)
// represents a spec for a list // represents a spec for a list
const matchOldValueSpecList = every( export const matchOldValueSpecList = every(
object( object({
{ type: literals("list"),
type: literals("list"), range: string, // '[0,1]' (inclusive) OR '[0,*)' (right unbounded), normal math rules
range: string, // '[0,1]' (inclusive) OR '[0,*)' (right unbounded), normal math rules default: anyOf(
default: anyOf( array(string),
array(string), array(number),
array(number), array(matchOldDefaultString),
array(matchOldDefaultString), array(object),
array(object), ),
), name: string,
name: string, description: string.nullable().optional(),
description: string, warning: string.nullable().optional(),
warning: string, }),
},
["description", "warning"],
),
anyOf( anyOf(
object({ object({
subtype: literals("string"), subtype: literals("string"),
@@ -582,6 +555,10 @@ const matchOldValueSpecList = every(
subtype: literals("number"), subtype: literals("number"),
spec: matchOldListValueSpecNumber, spec: matchOldListValueSpecNumber,
}), }),
object({
subtype: literals("union"),
spec: matchOldListValueSpecUnion,
}),
), ),
) )
type OldValueSpecList = typeof matchOldValueSpecList._TYPE type OldValueSpecList = typeof matchOldValueSpecList._TYPE

View File

@@ -1,7 +1,6 @@
import { System } from "../../Interfaces/System" import { System } from "../../Interfaces/System"
import { Effects } from "../../Models/Effects" import { Effects } from "../../Models/Effects"
import { T, utils } from "@start9labs/start-sdk" import { ExtendedVersion, T, utils, VersionRange } from "@start9labs/start-sdk"
import { Optional } from "ts-matches/lib/parsers/interfaces"
export const STARTOS_JS_LOCATION = "/usr/lib/startos/package/index.js" export const STARTOS_JS_LOCATION = "/usr/lib/startos/package/index.js"
@@ -11,6 +10,7 @@ type RunningMain = {
export class SystemForStartOs implements System { export class SystemForStartOs implements System {
private runningMain: RunningMain | undefined private runningMain: RunningMain | undefined
private starting: boolean = false
static of() { static of() {
return new SystemForStartOs(require(STARTOS_JS_LOCATION)) return new SystemForStartOs(require(STARTOS_JS_LOCATION))
@@ -19,22 +19,23 @@ export class SystemForStartOs implements System {
constructor(readonly abi: T.ABI) { constructor(readonly abi: T.ABI) {
this this
} }
async containerInit(effects: Effects): Promise<void> {
return void (await this.abi.containerInit({ effects })) async init(
}
async packageInit(
effects: Effects, effects: Effects,
kind: "install" | "update" | "restore" | null,
): Promise<void> {
return void (await this.abi.init({ effects, kind }))
}
async exit(
effects: Effects,
target: ExtendedVersion | VersionRange | null,
timeoutMs: number | null = null, timeoutMs: number | null = null,
): Promise<void> { ): Promise<void> {
return void (await this.abi.packageInit({ effects })) await this.stop()
} return void (await this.abi.uninit({ effects, target }))
async packageUninit(
effects: Effects,
nextVersion: Optional<string> = null,
timeoutMs: number | null = null,
): Promise<void> {
return void (await this.abi.packageUninit({ effects, nextVersion }))
} }
async createBackup( async createBackup(
effects: T.Effects, effects: T.Effects,
timeoutMs: number | null, timeoutMs: number | null,
@@ -43,14 +44,6 @@ export class SystemForStartOs implements System {
effects, effects,
})) }))
} }
async restoreBackup(
effects: T.Effects,
timeoutMs: number | null,
): Promise<void> {
return void (await this.abi.restoreBackup({
effects,
}))
}
getActionInput( getActionInput(
effects: Effects, effects: Effects,
id: string, id: string,
@@ -71,28 +64,27 @@ export class SystemForStartOs implements System {
return action.run({ effects, input }) return action.run({ effects, input })
} }
async exit(): Promise<void> {}
async start(effects: Effects): Promise<void> { async start(effects: Effects): Promise<void> {
if (this.runningMain) return try {
effects.constRetry = utils.once(() => effects.restart()) if (this.runningMain || this.starting) return
let mainOnTerm: () => Promise<void> | undefined this.starting = true
const started = async (onTerm: () => Promise<void>) => { effects.constRetry = utils.once(() => {
await effects.setMainStatus({ status: "running" }) console.debug(".const() triggered")
mainOnTerm = onTerm effects.restart()
return null
}
const daemons = await (
await this.abi.main({
effects,
started,
}) })
).build() let mainOnTerm: () => Promise<void> | undefined
this.runningMain = { const daemons = await (
stop: async () => { await this.abi.main({
if (mainOnTerm) await mainOnTerm() effects,
await daemons.term() })
}, ).build()
this.runningMain = {
stop: async () => {
await daemons.term()
},
}
} finally {
this.starting = false
} }
} }

View File

@@ -1,13 +1,13 @@
import { types as T } from "@start9labs/start-sdk" import {
ExtendedVersion,
types as T,
VersionRange,
} from "@start9labs/start-sdk"
import { Effects } from "../Models/Effects" import { Effects } from "../Models/Effects"
import { CallbackHolder } from "../Models/CallbackHolder" import { CallbackHolder } from "../Models/CallbackHolder"
import { Optional } from "ts-matches/lib/parsers/interfaces"
export type Procedure = export type Procedure =
| "/packageInit"
| "/packageUninit"
| "/backup/create" | "/backup/create"
| "/backup/restore"
| `/actions/${string}/getInput` | `/actions/${string}/getInput`
| `/actions/${string}/run` | `/actions/${string}/run`
@@ -15,20 +15,15 @@ export type ExecuteResult =
| { ok: unknown } | { ok: unknown }
| { err: { code: number; message: string } } | { err: { code: number; message: string } }
export type System = { export type System = {
containerInit(effects: T.Effects): Promise<void> init(
effects: T.Effects,
kind: "install" | "update" | "restore" | null,
): Promise<void>
start(effects: T.Effects): Promise<void> start(effects: T.Effects): Promise<void>
stop(): Promise<void> stop(): Promise<void>
packageInit(effects: Effects, timeoutMs: number | null): Promise<void>
packageUninit(
effects: Effects,
nextVersion: Optional<string>,
timeoutMs: number | null,
): Promise<void>
createBackup(effects: T.Effects, timeoutMs: number | null): Promise<void> createBackup(effects: T.Effects, timeoutMs: number | null): Promise<void>
restoreBackup(effects: T.Effects, timeoutMs: number | null): Promise<void>
runAction( runAction(
effects: Effects, effects: Effects,
actionId: string, actionId: string,
@@ -41,7 +36,10 @@ export type System = {
timeoutMs: number | null, timeoutMs: number | null,
): Promise<T.ActionInput | null> ): Promise<T.ActionInput | null>
exit(): Promise<void> exit(
effects: Effects,
target: ExtendedVersion | VersionRange | null,
): Promise<void>
} }
export type RunningMain = { export type RunningMain = {

View File

@@ -14,7 +14,8 @@ export class CallbackHolder {
constructor(private effects?: T.Effects) {} constructor(private effects?: T.Effects) {}
private callbacks = new Map<number, Function>() private callbacks = new Map<number, Function>()
private children: WeakRef<CallbackHolder>[] = [] private onLeaveContextCallbacks: Function[] = []
private children: Map<string, CallbackHolder> = new Map()
private newId() { private newId() {
return CallbackIdCell.inc++ return CallbackIdCell.inc++
} }
@@ -32,23 +33,30 @@ export class CallbackHolder {
}) })
return id return id
} }
child(): CallbackHolder { child(name: string): CallbackHolder {
const child = new CallbackHolder() this.removeChild(name)
this.children.push(new WeakRef(child)) const child = new CallbackHolder(this.effects)
this.children.set(name, child)
return child return child
} }
removeChild(child: CallbackHolder) {
this.children = this.children.filter((c) => { getChild(name: string): CallbackHolder | null {
const ref = c.deref() return this.children.get(name) || null
return ref && ref !== child }
})
removeChild(name: string) {
const child = this.children.get(name)
if (child) {
child.leaveContext()
this.children.delete(name)
}
} }
private getCallback(index: number): Function | undefined { private getCallback(index: number): Function | undefined {
let callback = this.callbacks.get(index) let callback = this.callbacks.get(index)
if (callback) this.callbacks.delete(index) if (callback) this.callbacks.delete(index)
else { else {
for (let i = 0; i < this.children.length; i++) { for (let [_, child] of this.children) {
callback = this.children[i].deref()?.getCallback(index) callback = child.getCallback(index)
if (callback) return callback if (callback) return callback
} }
} }
@@ -57,6 +65,25 @@ export class CallbackHolder {
callCallback(index: number, args: any[]): Promise<unknown> { callCallback(index: number, args: any[]): Promise<unknown> {
const callback = this.getCallback(index) const callback = this.getCallback(index)
if (!callback) return Promise.resolve() if (!callback) return Promise.resolve()
return Promise.resolve().then(() => callback(...args)) return Promise.resolve()
.then(() => callback(...args))
.catch((e) => console.error("callback failed", e))
}
onLeaveContext(fn: Function) {
this.onLeaveContextCallbacks.push(fn)
}
leaveContext() {
for (let [_, child] of this.children) {
child.leaveContext()
}
this.children = new Map()
for (let fn of this.onLeaveContextCallbacks) {
try {
fn()
} catch (e) {
console.warn(e)
}
}
this.onLeaveContextCallbacks = []
} }
} }

View File

@@ -17,31 +17,25 @@ const Path = string
export type VolumeId = string export type VolumeId = string
export type Path = string export type Path = string
export const matchDockerProcedure = object( export const matchDockerProcedure = object({
{ type: literal("docker"),
type: literal("docker"), image: string,
image: string, system: boolean.optional(),
system: boolean, entrypoint: string,
entrypoint: string, args: array(string).defaultTo([]),
args: array(string), mounts: dictionary([VolumeId, Path]).optional(),
mounts: dictionary([VolumeId, Path]), "io-format": literals(
"io-format": literals( "json",
"json", "json-pretty",
"json-pretty", "yaml",
"yaml", "cbor",
"cbor", "toml",
"toml", "toml-pretty",
"toml-pretty", )
), .nullable()
"sigterm-timeout": some(number, matchDuration), .optional(),
inject: boolean, "sigterm-timeout": some(number, matchDuration).onMismatch(30),
}, inject: boolean.defaultTo(false),
["io-format", "sigterm-timeout", "system", "args", "inject", "mounts"], })
{
"sigterm-timeout": 30,
inject: false,
args: [],
},
)
export type DockerProcedure = typeof matchDockerProcedure._TYPE export type DockerProcedure = typeof matchDockerProcedure._TYPE

View File

@@ -7,6 +7,12 @@ const getDependencies: AllGetDependencies = {
system: getSystem, system: getSystem,
} }
for (let s of ["SIGTERM", "SIGINT", "SIGHUP"]) {
process.on(s, (s) => {
console.log(`Caught ${s}`)
})
}
new RpcListener(getDependencies) new RpcListener(getDependencies)
/** /**

View File

@@ -4,8 +4,13 @@ cd "$(dirname "${BASH_SOURCE[0]}")"
set -e set -e
if mountpoint tmp/combined; then sudo umount -R tmp/combined; fi RUST_ARCH="$ARCH"
if mountpoint tmp/lower; then sudo umount tmp/lower; fi if [ "$ARCH" = "riscv64" ]; then
RUST_ARCH="riscv64gc"
fi
if mountpoint -q tmp/combined; then sudo umount -l tmp/combined; fi
if mountpoint -q tmp/lower; then sudo umount tmp/lower; fi
sudo rm -rf tmp sudo rm -rf tmp
mkdir -p tmp/lower tmp/upper tmp/work tmp/combined mkdir -p tmp/lower tmp/upper tmp/work tmp/combined
if which squashfuse > /dev/null; then if which squashfuse > /dev/null; then
@@ -13,19 +18,23 @@ if which squashfuse > /dev/null; then
else else
sudo mount debian.${ARCH}.squashfs tmp/lower sudo mount debian.${ARCH}.squashfs tmp/lower
fi fi
sudo mount -t overlay -olowerdir=tmp/lower,upperdir=tmp/upper,workdir=tmp/work overlay tmp/combined if which fuse-overlayfs > /dev/null; then
sudo fuse-overlayfs -olowerdir=tmp/lower,upperdir=tmp/upper,workdir=tmp/work overlay tmp/combined
else
sudo mount -t overlay -olowerdir=tmp/lower,upperdir=tmp/upper,workdir=tmp/work overlay tmp/combined
fi
QEMU= QEMU=
if [ "$ARCH" != "$(uname -m)" ]; then if [ "$ARCH" != "$(uname -m)" ]; then
QEMU=/usr/bin/qemu-${ARCH}-static QEMU=/usr/bin/qemu-${ARCH}
if ! which qemu-$ARCH-static > /dev/null; then if ! which qemu-$ARCH > /dev/null; then
>&2 echo qemu-user-static is required for cross-platform builds >&2 echo qemu-user is required for cross-platform builds
sudo umount tmp/combined sudo umount tmp/combined
sudo umount tmp/lower sudo umount tmp/lower
sudo rm -rf tmp sudo rm -rf tmp
exit 1 exit 1
fi fi
sudo cp $(which qemu-$ARCH-static) tmp/combined${QEMU} sudo cp $(which qemu-$ARCH) tmp/combined${QEMU}
fi fi
sudo mkdir -p tmp/combined/usr/lib/startos/ sudo mkdir -p tmp/combined/usr/lib/startos/
@@ -33,8 +42,12 @@ sudo rsync -a --copy-unsafe-links dist/ tmp/combined/usr/lib/startos/init/
sudo chown -R 0:0 tmp/combined/usr/lib/startos/ sudo chown -R 0:0 tmp/combined/usr/lib/startos/
sudo cp container-runtime.service tmp/combined/lib/systemd/system/container-runtime.service sudo cp container-runtime.service tmp/combined/lib/systemd/system/container-runtime.service
sudo chown 0:0 tmp/combined/lib/systemd/system/container-runtime.service sudo chown 0:0 tmp/combined/lib/systemd/system/container-runtime.service
sudo cp ../core/target/$ARCH-unknown-linux-musl/release/containerbox tmp/combined/usr/bin/start-cli sudo cp container-runtime-failure.service tmp/combined/lib/systemd/system/container-runtime-failure.service
sudo chown 0:0 tmp/combined/usr/bin/start-cli sudo chown 0:0 tmp/combined/lib/systemd/system/container-runtime-failure.service
sudo cp ../core/target/${RUST_ARCH}-unknown-linux-musl/release/start-container tmp/combined/usr/bin/start-container
echo -e '#!/bin/bash\nexec start-container "$@"' | sudo tee tmp/combined/usr/bin/start-cli # TODO: remove
sudo chmod +x tmp/combined/usr/bin/start-cli
sudo chown 0:0 tmp/combined/usr/bin/start-container
echo container-runtime | sha256sum | head -c 32 | cat - <(echo) | sudo tee tmp/combined/etc/machine-id echo container-runtime | sha256sum | head -c 32 | cat - <(echo) | sudo tee tmp/combined/etc/machine-id
cat deb-install.sh | sudo systemd-nspawn --console=pipe -D tmp/combined $QEMU /bin/bash cat deb-install.sh | sudo systemd-nspawn --console=pipe -D tmp/combined $QEMU /bin/bash
sudo truncate -s 0 tmp/combined/etc/machine-id sudo truncate -s 0 tmp/combined/etc/machine-id

7346
core/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,3 +1,3 @@
[workspace] [workspace]
members = ["helpers", "models", "startos"] members = ["startos"]

2
core/Cross.toml Normal file
View File

@@ -0,0 +1,2 @@
[build]
pre-build = ["apt-get update && apt-get install -y rsync"]

View File

@@ -14,7 +14,7 @@
## Artifacts ## Artifacts
The StartOS backend is packed into a single binary `startbox` that is symlinked under The StartOS backend is packed into a single binary `startbox` that is symlinked under
several different names for different behaviour: several different names for different behavior:
- `startd`: This is the main daemon of StartOS - `startd`: This is the main daemon of StartOS
- `start-cli`: This is a CLI tool that will allow you to issue commands to - `start-cli`: This is a CLI tool that will allow you to issue commands to

View File

@@ -2,53 +2,78 @@
cd "$(dirname "${BASH_SOURCE[0]}")" cd "$(dirname "${BASH_SOURCE[0]}")"
source ./builder-alias.sh
set -ea set -ea
INSTALL=false
while [[ $# -gt 0 ]]; do
case $1 in
--install)
INSTALL=true
shift
;;
*)
>&2 echo "Unknown option: $1"
exit 1
;;
esac
done
shopt -s expand_aliases shopt -s expand_aliases
if [ -z "$ARCH" ]; then PROFILE=${PROFILE:-release}
ARCH=$(uname -m) if [ "${PROFILE}" = "release" ]; then
BUILD_FLAGS="--release"
else
if [ "$PROFILE" != "debug"]; then
>&2 echo "Unknown profile $PROFILE: falling back to debug..."
PROFILE=debug
fi
fi fi
if [ -z "${ARCH:-}" ]; then
ARCH=$(uname -m)
fi
if [ "$ARCH" = "arm64" ]; then if [ "$ARCH" = "arm64" ]; then
ARCH="aarch64" ARCH="aarch64"
fi fi
if [ -z "$KERNEL_NAME" ]; then RUST_ARCH="$ARCH"
KERNEL_NAME=$(uname -s) if [ "$ARCH" = "riscv64" ]; then
RUST_ARCH="riscv64gc"
fi fi
if [ -z "$TARGET" ]; then if [ -z "${KERNEL_NAME:-}" ]; then
if [ "$KERNEL_NAME" = "Linux" ]; then KERNEL_NAME=$(uname -s)
TARGET="$ARCH-unknown-linux-musl"
elif [ "$KERNEL_NAME" = "Darwin" ]; then
TARGET="$ARCH-apple-darwin"
else
>&2 echo "unknown kernel $KERNEL_NAME"
exit 1
fi
fi fi
USE_TTY= if [ -z "${TARGET:-}" ]; then
if tty -s; then if [ "$KERNEL_NAME" = "Linux" ]; then
USE_TTY="-it" TARGET="$RUST_ARCH-unknown-linux-musl"
elif [ "$KERNEL_NAME" = "Darwin" ]; then
TARGET="$RUST_ARCH-apple-darwin"
else
>&2 echo "unknown kernel $KERNEL_NAME"
exit 1
fi
fi fi
cd .. cd ..
FEATURES="$(echo $ENVIRONMENT | sed 's/-/,/g')" FEATURES="$(echo "${ENVIRONMENT:-}" | sed 's/-/,/g')"
RUSTFLAGS="" RUSTFLAGS=""
if [[ "${ENVIRONMENT:-}" =~ (^|-)console($|-) ]]; then
if [[ "${ENVIRONMENT}" =~ (^|-)unstable($|-) ]]; then RUSTFLAGS="--cfg tokio_unstable"
RUSTFLAGS="--cfg tokio_unstable"
fi fi
if which zig > /dev/null && [ "$ENFORCE_USE_DOCKER" != 1 ]; do echo "FEATURES=\"$FEATURES\""
echo "FEATURES=\"$FEATURES\"" echo "RUSTFLAGS=\"$RUSTFLAGS\""
echo "RUSTFLAGS=\"$RUSTFLAGS\"" rust-zig-builder cargo zigbuild --manifest-path=./core/Cargo.toml $BUILD_FLAGS --features=$FEATURES --locked --bin start-cli --target=$TARGET
RUSTFLAGS=$RUSTFLAGS sh -c "cd core && cargo zigbuild --release --no-default-features --features cli,$FEATURES --locked --bin start-cli --target=$TARGET" if [ "$(ls -nd "core/target/$TARGET/$PROFILE/start-cli" | awk '{ print $3 }')" != "$UID" ]; then
else rust-zig-builder sh -c "cd core && chown -R $UID:$UID target && chown -R $UID:$UID /usr/local/cargo"
alias 'rust-zig-builder'='docker run $USE_TTY --rm -e "RUSTFLAGS=$RUSTFLAGS" -v "$HOME/.cargo/registry":/root/.cargo/registry -v "$HOME/.cargo/git":/root/.cargo/git -v "$(pwd)":/home/rust/src -w /home/rust/src -P messense/cargo-zigbuild' fi
RUSTFLAGS=$RUSTFLAGS rust-zig-builder sh -c "cd core && cargo zigbuild --release --no-default-features --features cli,$FEATURES --locked --bin start-cli --target=$TARGET"
if [ "$INSTALL" = "true" ]; then
if [ "$(ls -nd core/target/$TARGET/release/start-cli | awk '{ print $3 }')" != "$UID" ]; then cp "core/target/$TARGET/$PROFILE/start-cli" ~/.cargo/bin/start-cli
rust-zig-builder sh -c "cd core && chown -R $UID:$UID target && chown -R $UID:$UID /root/.cargo"
fi
fi fi

View File

@@ -1,36 +0,0 @@
#!/bin/bash
cd "$(dirname "${BASH_SOURCE[0]}")"
set -ea
shopt -s expand_aliases
if [ -z "$ARCH" ]; then
ARCH=$(uname -m)
fi
if [ "$ARCH" = "arm64" ]; then
ARCH="aarch64"
fi
USE_TTY=
if tty -s; then
USE_TTY="-it"
fi
cd ..
FEATURES="$(echo $ENVIRONMENT | sed 's/-/,/g')"
RUSTFLAGS=""
if [[ "${ENVIRONMENT}" =~ (^|-)unstable($|-) ]]; then
RUSTFLAGS="--cfg tokio_unstable"
fi
alias 'rust-musl-builder'='docker run $USE_TTY --rm -e "RUSTFLAGS=$RUSTFLAGS" -v "$HOME/.cargo/registry":/root/.cargo/registry -v "$HOME/.cargo/git":/root/.cargo/git -v "$(pwd)":/home/rust/src -w /home/rust/src -P messense/rust-musl-cross:$ARCH-musl'
echo "FEATURES=\"$FEATURES\""
echo "RUSTFLAGS=\"$RUSTFLAGS\""
rust-musl-builder sh -c "cd core && cargo build --release --no-default-features --features container-runtime,$FEATURES --locked --bin containerbox --target=$ARCH-unknown-linux-musl"
if [ "$(ls -nd core/target/$ARCH-unknown-linux-musl/release/containerbox | awk '{ print $3 }')" != "$UID" ]; then
rust-musl-builder sh -c "cd core && chown -R $UID:$UID target && chown -R $UID:$UID /root/.cargo"
fi

View File

@@ -2,9 +2,21 @@
cd "$(dirname "${BASH_SOURCE[0]}")" cd "$(dirname "${BASH_SOURCE[0]}")"
source ./builder-alias.sh
set -ea set -ea
shopt -s expand_aliases shopt -s expand_aliases
PROFILE=${PROFILE:-release}
if [ "${PROFILE}" = "release" ]; then
BUILD_FLAGS="--release"
else
if [ "$PROFILE" != "debug"]; then
>&2 echo "Unknown profile $PROFILE: falling back to debug..."
PROFILE=debug
fi
fi
if [ -z "$ARCH" ]; then if [ -z "$ARCH" ]; then
ARCH=$(uname -m) ARCH=$(uname -m)
fi fi
@@ -13,24 +25,22 @@ if [ "$ARCH" = "arm64" ]; then
ARCH="aarch64" ARCH="aarch64"
fi fi
USE_TTY= RUST_ARCH="$ARCH"
if tty -s; then if [ "$ARCH" = "riscv64" ]; then
USE_TTY="-it" RUST_ARCH="riscv64gc"
fi fi
cd .. cd ..
FEATURES="$(echo $ENVIRONMENT | sed 's/-/,/g')" FEATURES="$(echo $ENVIRONMENT | sed 's/-/,/g')"
RUSTFLAGS="" RUSTFLAGS=""
if [[ "${ENVIRONMENT}" =~ (^|-)unstable($|-) ]]; then if [[ "${ENVIRONMENT}" =~ (^|-)console($|-) ]]; then
RUSTFLAGS="--cfg tokio_unstable" RUSTFLAGS="--cfg tokio_unstable"
fi fi
alias 'rust-musl-builder'='docker run $USE_TTY --rm -e "RUSTFLAGS=$RUSTFLAGS" -v "$HOME/.cargo/registry":/root/.cargo/registry -v "$HOME/.cargo/git":/root/.cargo/git -v "$(pwd)":/home/rust/src -w /home/rust/src -P messense/rust-musl-cross:$ARCH-musl'
echo "FEATURES=\"$FEATURES\"" echo "FEATURES=\"$FEATURES\""
echo "RUSTFLAGS=\"$RUSTFLAGS\"" echo "RUSTFLAGS=\"$RUSTFLAGS\""
rust-musl-builder sh -c "cd core && cargo build --release --no-default-features --features cli,registry,$FEATURES --locked --bin registrybox --target=$ARCH-unknown-linux-musl" rust-zig-builder cargo zigbuild --manifest-path=./core/Cargo.toml $BUILD_FLAGS --features=$FEATURES --locked --bin registrybox --target=$RUST_ARCH-unknown-linux-musl
if [ "$(ls -nd core/target/$ARCH-unknown-linux-musl/release/registrybox | awk '{ print $3 }')" != "$UID" ]; then if [ "$(ls -nd "core/target/$RUST_ARCH-unknown-linux-musl/$PROFILE/registrybox" | awk '{ print $3 }')" != "$UID" ]; then
rust-musl-builder sh -c "cd core && chown -R $UID:$UID target && chown -R $UID:$UID /root/.cargo" rust-zig-builder sh -c "chown -R $UID:$UID core/target && chown -R $UID:$UID /usr/local/cargo"
fi fi

46
core/build-start-container.sh Executable file
View File

@@ -0,0 +1,46 @@
#!/bin/bash
cd "$(dirname "${BASH_SOURCE[0]}")"
source ./builder-alias.sh
set -ea
shopt -s expand_aliases
PROFILE=${PROFILE:-release}
if [ "${PROFILE}" = "release" ]; then
BUILD_FLAGS="--release"
else
if [ "$PROFILE" != "debug"]; then
>&2 echo "Unknown profile $PROFILE: falling back to debug..."
PROFILE=debug
fi
fi
if [ -z "$ARCH" ]; then
ARCH=$(uname -m)
fi
if [ "$ARCH" = "arm64" ]; then
ARCH="aarch64"
fi
RUST_ARCH="$ARCH"
if [ "$ARCH" = "riscv64" ]; then
RUST_ARCH="riscv64gc"
fi
cd ..
FEATURES="$(echo $ENVIRONMENT | sed 's/-/,/g')"
RUSTFLAGS=""
if [[ "${ENVIRONMENT}" =~ (^|-)console($|-) ]]; then
RUSTFLAGS="--cfg tokio_unstable"
fi
echo "FEATURES=\"$FEATURES\""
echo "RUSTFLAGS=\"$RUSTFLAGS\""
rust-zig-builder cargo zigbuild --manifest-path=./core/Cargo.toml $BUILD_FLAGS --features=$FEATURES --locked --bin start-container --target=$RUST_ARCH-unknown-linux-musl
if [ "$(ls -nd "core/target/$RUST_ARCH-unknown-linux-musl/$PROFILE/start-container" | awk '{ print $3 }')" != "$UID" ]; then
rust-zig-builder sh -c "chown -R $UID:$UID core/target && chown -R $UID:$UID /usr/local/cargo"
fi

View File

@@ -2,9 +2,21 @@
cd "$(dirname "${BASH_SOURCE[0]}")" cd "$(dirname "${BASH_SOURCE[0]}")"
source ./builder-alias.sh
set -ea set -ea
shopt -s expand_aliases shopt -s expand_aliases
PROFILE=${PROFILE:-release}
if [ "${PROFILE}" = "release" ]; then
BUILD_FLAGS="--release"
else
if [ "$PROFILE" != "debug"]; then
>&2 echo "Unknown profile $PROFILE: falling back to debug..."
PROFILE=debug
fi
fi
if [ -z "$ARCH" ]; then if [ -z "$ARCH" ]; then
ARCH=$(uname -m) ARCH=$(uname -m)
fi fi
@@ -13,24 +25,22 @@ if [ "$ARCH" = "arm64" ]; then
ARCH="aarch64" ARCH="aarch64"
fi fi
USE_TTY= RUST_ARCH="$ARCH"
if tty -s; then if [ "$ARCH" = "riscv64" ]; then
USE_TTY="-it" RUST_ARCH="riscv64gc"
fi fi
cd .. cd ..
FEATURES="$(echo $ENVIRONMENT | sed 's/-/,/g')" FEATURES="$(echo $ENVIRONMENT | sed 's/-/,/g')"
RUSTFLAGS="" RUSTFLAGS=""
if [[ "${ENVIRONMENT}" =~ (^|-)unstable($|-) ]]; then if [[ "${ENVIRONMENT}" =~ (^|-)console($|-) ]]; then
RUSTFLAGS="--cfg tokio_unstable" RUSTFLAGS="--cfg tokio_unstable"
fi fi
alias 'rust-musl-builder'='docker run $USE_TTY --rm -e "RUSTFLAGS=$RUSTFLAGS" -v "$HOME/.cargo/registry":/root/.cargo/registry -v "$HOME/.cargo/git":/root/.cargo/git -v "$(pwd)":/home/rust/src -w /home/rust/src -P messense/rust-musl-cross:$ARCH-musl'
echo "FEATURES=\"$FEATURES\"" echo "FEATURES=\"$FEATURES\""
echo "RUSTFLAGS=\"$RUSTFLAGS\"" echo "RUSTFLAGS=\"$RUSTFLAGS\""
rust-musl-builder sh -c "cd core && cargo build --release --no-default-features --features cli,daemon,$FEATURES --locked --bin startbox --target=$ARCH-unknown-linux-musl" rust-zig-builder cargo zigbuild --manifest-path=./core/Cargo.toml $BUILD_FLAGS --features=$FEATURES --locked --bin startbox --target=$RUST_ARCH-unknown-linux-musl
if [ "$(ls -nd core/target/$ARCH-unknown-linux-musl/release/startbox | awk '{ print $3 }')" != "$UID" ]; then if [ "$(ls -nd "core/target/$RUST_ARCH-unknown-linux-musl/$PROFILE/startbox" | awk '{ print $3 }')" != "$UID" ]; then
rust-musl-builder sh -c "cd core && chown -R $UID:$UID target && chown -R $UID:$UID /root/.cargo" rust-zig-builder sh -c "chown -R $UID:$UID core/target && chown -R $UID:$UID /usr/local/cargo"
fi fi

View File

@@ -2,35 +2,43 @@
cd "$(dirname "${BASH_SOURCE[0]}")" cd "$(dirname "${BASH_SOURCE[0]}")"
source ./builder-alias.sh
set -ea set -ea
shopt -s expand_aliases shopt -s expand_aliases
PROFILE=${PROFILE:-release}
if [ "${PROFILE}" = "release" ]; then
BUILD_FLAGS="--release"
else
if [ "$PROFILE" != "debug"]; then
>&2 echo "Unknown profile $PROFILE: falling back to debug..."
PROFILE=debug
fi
fi
if [ -z "$ARCH" ]; then if [ -z "$ARCH" ]; then
ARCH=$(uname -m) ARCH=$(uname -m)
fi fi
if [ "$ARCH" = "arm64" ]; then if [ "$ARCH" = "arm64" ]; then
ARCH="aarch64" ARCH="aarch64"
fi fi
USE_TTY= RUST_ARCH="$ARCH"
if tty -s; then if [ "$ARCH" = "riscv64" ]; then
USE_TTY="-it" RUST_ARCH="riscv64gc"
fi fi
cd .. cd ..
FEATURES="$(echo $ENVIRONMENT | sed 's/-/,/g')" FEATURES="$(echo $ENVIRONMENT | sed 's/-/,/g')"
RUSTFLAGS="" RUSTFLAGS=""
if [[ "${ENVIRONMENT}" =~ (^|-)console($|-) ]]; then
if [[ "${ENVIRONMENT}" =~ (^|-)unstable($|-) ]]; then
RUSTFLAGS="--cfg tokio_unstable" RUSTFLAGS="--cfg tokio_unstable"
fi fi
alias 'rust-musl-builder'='docker run $USE_TTY --rm -e "RUSTFLAGS=$RUSTFLAGS" -v "$HOME/.cargo/registry":/root/.cargo/registry -v "$HOME/.cargo/git":/root/.cargo/git -v "$(pwd)":/home/rust/src -w /home/rust/src -P messense/rust-musl-cross:$ARCH-musl'
echo "FEATURES=\"$FEATURES\"" echo "FEATURES=\"$FEATURES\""
echo "RUSTFLAGS=\"$RUSTFLAGS\"" echo "RUSTFLAGS=\"$RUSTFLAGS\""
rust-musl-builder sh -c "cd core && cargo test --release --features=test,$FEATURES 'export_bindings_' && chown \$UID:\$UID startos/bindings" rust-zig-builder cargo test --manifest-path=./core/Cargo.toml $BUILD_FLAGS --features test,$FEATURES --locked 'export_bindings_'
if [ "$(ls -nd core/startos/bindings | awk '{ print $3 }')" != "$UID" ]; then if [ "$(ls -nd "core/startos/bindings" | awk '{ print $3 }')" != "$UID" ]; then
rust-musl-builder sh -c "cd core && chown -R $UID:$UID startos/bindings && chown -R $UID:$UID target && chown -R $UID:$UID /root/.cargo" rust-zig-builder sh -c "chown -R $UID:$UID core/target && chown -R $UID:$UID core/startos/bindings && chown -R $UID:$UID /usr/local/cargo"
fi fi

46
core/build-tunnelbox.sh Executable file
View File

@@ -0,0 +1,46 @@
#!/bin/bash
cd "$(dirname "${BASH_SOURCE[0]}")"
source ./builder-alias.sh
set -ea
shopt -s expand_aliases
PROFILE=${PROFILE:-release}
if [ "${PROFILE}" = "release" ]; then
BUILD_FLAGS="--release"
else
if [ "$PROFILE" != "debug"]; then
>&2 echo "Unknown profile $PROFILE: falling back to debug..."
PROFILE=debug
fi
fi
if [ -z "$ARCH" ]; then
ARCH=$(uname -m)
fi
if [ "$ARCH" = "arm64" ]; then
ARCH="aarch64"
fi
RUST_ARCH="$ARCH"
if [ "$ARCH" = "riscv64" ]; then
RUST_ARCH="riscv64gc"
fi
cd ..
FEATURES="$(echo $ENVIRONMENT | sed 's/-/,/g')"
RUSTFLAGS=""
if [[ "${ENVIRONMENT}" =~ (^|-)console($|-) ]]; then
RUSTFLAGS="--cfg tokio_unstable"
fi
echo "FEATURES=\"$FEATURES\""
echo "RUSTFLAGS=\"$RUSTFLAGS\""
rust-zig-builder cargo zigbuild --manifest-path=./core/Cargo.toml $BUILD_FLAGS --features=$FEATURES --locked --bin tunnelbox --target=$RUST_ARCH-unknown-linux-musl
if [ "$(ls -nd "core/target/$RUST_ARCH-unknown-linux-musl/$PROFILE/tunnelbox" | awk '{ print $3 }')" != "$UID" ]; then
rust-zig-builder sh -c "chown -R $UID:$UID core/target && chown -R $UID:$UID /usr/local/cargo"
fi

8
core/builder-alias.sh Normal file
View File

@@ -0,0 +1,8 @@
#!/bin/bash
USE_TTY=
if tty -s; then
USE_TTY="-it"
fi
alias 'rust-zig-builder'='docker run '"$USE_TTY"' --rm -e "RUSTFLAGS=$RUSTFLAGS" -e "PKG_CONFIG_SYSROOT_DIR=/opt/sysroot/$ARCH" -e PKG_CONFIG_PATH="" -e PKG_CONFIG_LIBDIR="/opt/sysroot/$ARCH/usr/lib/pkgconfig" -e "AWS_LC_SYS_CMAKE_TOOLCHAIN_FILE_riscv64gc_unknown_linux_musl=/root/cmake-overrides/toolchain-riscv64-musl-clang.cmake" -e SCCACHE_GHA_ENABLED -e SCCACHE_GHA_VERSION -e ACTIONS_RESULTS_URL -e ACTIONS_RUNTIME_TOKEN -v "$HOME/.cargo/registry":/usr/local/cargo/registry -v "$HOME/.cargo/git":/usr/local/cargo/git -v "$HOME/.cache/sccache":/root/.cache/sccache -v "$HOME/.cache/cargo-zigbuild:/root/.cache/cargo-zigbuild" -v "$(pwd)":/workdir -w /workdir start9/cargo-zigbuild'

View File

@@ -1,19 +0,0 @@
[package]
name = "helpers"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
color-eyre = "0.6.2"
futures = "0.3.28"
lazy_async_pool = "0.3.3"
models = { path = "../models" }
pin-project = "1.1.3"
rpc-toolkit = { git = "https://github.com/Start9Labs/rpc-toolkit.git", branch = "master" }
serde = { version = "1.0", features = ["derive", "rc"] }
serde_json = "1.0"
tokio = { version = "1", features = ["full"] }
tokio-stream = { version = "0.1.14", features = ["io-util", "sync"] }
tracing = "0.1.39"

View File

@@ -1,31 +0,0 @@
use std::task::Poll;
use tokio::io::{AsyncRead, ReadBuf};
#[pin_project::pin_project]
pub struct ByteReplacementReader<R> {
pub replace: u8,
pub with: u8,
#[pin]
pub inner: R,
}
impl<R: AsyncRead> AsyncRead for ByteReplacementReader<R> {
fn poll_read(
self: std::pin::Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
buf: &mut ReadBuf<'_>,
) -> std::task::Poll<std::io::Result<()>> {
let this = self.project();
match this.inner.poll_read(cx, buf) {
Poll::Ready(Ok(())) => {
for idx in 0..buf.filled().len() {
if buf.filled()[idx] == *this.replace {
buf.filled_mut()[idx] = *this.with;
}
}
Poll::Ready(Ok(()))
}
a => a,
}
}
}

View File

@@ -1,262 +0,0 @@
use std::future::Future;
use std::ops::{Deref, DerefMut};
use std::path::{Path, PathBuf};
use std::time::Duration;
use color_eyre::eyre::{eyre, Context, Error};
use futures::future::BoxFuture;
use futures::FutureExt;
use models::ResultExt;
use tokio::fs::File;
use tokio::sync::oneshot;
use tokio::task::{JoinError, JoinHandle, LocalSet};
mod byte_replacement_reader;
mod rsync;
mod script_dir;
pub use byte_replacement_reader::*;
pub use rsync::*;
pub use script_dir::*;
pub fn const_true() -> bool {
true
}
pub fn to_tmp_path(path: impl AsRef<Path>) -> Result<PathBuf, Error> {
let path = path.as_ref();
if let (Some(parent), Some(file_name)) =
(path.parent(), path.file_name().and_then(|f| f.to_str()))
{
Ok(parent.join(format!(".{}.tmp", file_name)))
} else {
Err(eyre!("invalid path: {}", path.display()))
}
}
pub async fn canonicalize(
path: impl AsRef<Path> + Send + Sync,
create_parent: bool,
) -> Result<PathBuf, Error> {
fn create_canonical_folder<'a>(
path: impl AsRef<Path> + Send + Sync + 'a,
) -> BoxFuture<'a, Result<PathBuf, Error>> {
async move {
let path = canonicalize(path, true).await?;
tokio::fs::create_dir(&path)
.await
.with_context(|| path.display().to_string())?;
Ok(path)
}
.boxed()
}
let path = path.as_ref();
if tokio::fs::metadata(path).await.is_err() {
let parent = path.parent().unwrap_or(Path::new("."));
if let Some(file_name) = path.file_name() {
if create_parent && tokio::fs::metadata(parent).await.is_err() {
return Ok(create_canonical_folder(parent).await?.join(file_name));
} else {
return Ok(tokio::fs::canonicalize(parent)
.await
.with_context(|| parent.display().to_string())?
.join(file_name));
}
}
}
tokio::fs::canonicalize(&path)
.await
.with_context(|| path.display().to_string())
}
#[pin_project::pin_project(PinnedDrop)]
pub struct NonDetachingJoinHandle<T>(#[pin] JoinHandle<T>);
impl<T> NonDetachingJoinHandle<T> {
pub async fn wait_for_abort(self) -> Result<T, JoinError> {
self.abort();
self.await
}
}
impl<T> From<JoinHandle<T>> for NonDetachingJoinHandle<T> {
fn from(t: JoinHandle<T>) -> Self {
NonDetachingJoinHandle(t)
}
}
impl<T> Deref for NonDetachingJoinHandle<T> {
type Target = JoinHandle<T>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl<T> DerefMut for NonDetachingJoinHandle<T> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
#[pin_project::pinned_drop]
impl<T> PinnedDrop for NonDetachingJoinHandle<T> {
fn drop(self: std::pin::Pin<&mut Self>) {
let this = self.project();
this.0.into_ref().get_ref().abort()
}
}
impl<T> Future for NonDetachingJoinHandle<T> {
type Output = Result<T, JoinError>;
fn poll(
self: std::pin::Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
) -> std::task::Poll<Self::Output> {
let this = self.project();
this.0.poll(cx)
}
}
pub struct AtomicFile {
tmp_path: PathBuf,
path: PathBuf,
file: Option<File>,
}
impl AtomicFile {
pub async fn new(
path: impl AsRef<Path> + Send + Sync,
tmp_path: Option<impl AsRef<Path> + Send + Sync>,
) -> Result<Self, Error> {
let path = canonicalize(&path, true).await?;
let tmp_path = if let Some(tmp_path) = tmp_path {
canonicalize(&tmp_path, true).await?
} else {
to_tmp_path(&path)?
};
let file = File::create(&tmp_path)
.await
.with_context(|| tmp_path.display().to_string())?;
Ok(Self {
tmp_path,
path,
file: Some(file),
})
}
pub async fn rollback(mut self) -> Result<(), Error> {
drop(self.file.take());
tokio::fs::remove_file(&self.tmp_path)
.await
.with_context(|| format!("rm {}", self.tmp_path.display()))?;
Ok(())
}
pub async fn save(mut self) -> Result<(), Error> {
use tokio::io::AsyncWriteExt;
if let Some(file) = self.file.as_mut() {
file.flush().await?;
file.shutdown().await?;
file.sync_all().await?;
}
drop(self.file.take());
tokio::fs::rename(&self.tmp_path, &self.path)
.await
.with_context(|| {
format!("mv {} -> {}", self.tmp_path.display(), self.path.display())
})?;
Ok(())
}
}
impl std::ops::Deref for AtomicFile {
type Target = File;
fn deref(&self) -> &Self::Target {
self.file.as_ref().unwrap()
}
}
impl std::ops::DerefMut for AtomicFile {
fn deref_mut(&mut self) -> &mut Self::Target {
self.file.as_mut().unwrap()
}
}
impl Drop for AtomicFile {
fn drop(&mut self) {
if let Some(file) = self.file.take() {
drop(file);
let path = std::mem::take(&mut self.tmp_path);
tokio::spawn(async move { tokio::fs::remove_file(path).await.log_err() });
}
}
}
pub struct TimedResource<T: 'static + Send> {
handle: NonDetachingJoinHandle<Option<T>>,
ready: oneshot::Sender<()>,
}
impl<T: 'static + Send> TimedResource<T> {
pub fn new(resource: T, timer: Duration) -> Self {
let (send, recv) = oneshot::channel();
let handle = tokio::spawn(async move {
tokio::select! {
_ = tokio::time::sleep(timer) => {
drop(resource);
None
},
_ = recv => Some(resource),
}
});
Self {
handle: handle.into(),
ready: send,
}
}
pub fn new_with_destructor<
Fn: FnOnce(T) -> Fut + Send + 'static,
Fut: Future<Output = ()> + Send,
>(
resource: T,
timer: Duration,
destructor: Fn,
) -> Self {
let (send, recv) = oneshot::channel();
let handle = tokio::spawn(async move {
tokio::select! {
_ = tokio::time::sleep(timer) => {
destructor(resource).await;
None
},
_ = recv => Some(resource),
}
});
Self {
handle: handle.into(),
ready: send,
}
}
pub async fn get(self) -> Option<T> {
let _ = self.ready.send(());
self.handle.await.unwrap()
}
pub fn is_timed_out(&self) -> bool {
self.ready.is_closed()
}
}
pub async fn spawn_local<
T: 'static + Send,
F: FnOnce() -> Fut + Send + 'static,
Fut: Future<Output = T> + 'static,
>(
fut: F,
) -> NonDetachingJoinHandle<T> {
let (send, recv) = tokio::sync::oneshot::channel();
std::thread::spawn(move || {
tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.unwrap()
.block_on(async move {
let set = LocalSet::new();
send.send(set.spawn_local(fut()).into())
.unwrap_or_else(|_| unreachable!());
set.await
})
});
recv.await.unwrap()
}

View File

@@ -1,17 +0,0 @@
use std::path::{Path, PathBuf};
use models::{PackageId, VersionString};
pub const PKG_SCRIPT_DIR: &str = "package-data/scripts";
pub fn script_dir<P: AsRef<Path>>(
datadir: P,
pkg_id: &PackageId,
version: &VersionString,
) -> PathBuf {
datadir
.as_ref()
.join(&*PKG_SCRIPT_DIR)
.join(pkg_id)
.join(version.as_str())
}

View File

@@ -1,19 +0,0 @@
#!/bin/bash
cd "$(dirname "${BASH_SOURCE[0]}")"
set -ea
shopt -s expand_aliases
web="../web/dist/static"
[ -d "$web" ] || mkdir -p "$web"
if [ -z "$PLATFORM" ]; then
PLATFORM=$(uname -m)
fi
if [ "$PLATFORM" = "arm64" ]; then
PLATFORM="aarch64"
fi
cargo install --path=./startos --no-default-features --features=cli,docker,registry --bin start-cli --locked

View File

@@ -1,43 +0,0 @@
[package]
name = "models"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
axum = "0.7.5"
base64 = "0.21.4"
color-eyre = "0.6.2"
ed25519-dalek = { version = "2.0.0", features = ["serde"] }
lazy_static = "1.4"
mbrman = "0.5.2"
exver = { version = "0.2.0", git = "https://github.com/Start9Labs/exver-rs.git", features = [
"serde",
] }
ipnet = "2.8.0"
num_enum = "0.7.1"
openssl = { version = "0.10.57", features = ["vendored"] }
patch-db = { version = "*", path = "../../patch-db/patch-db", features = [
"trace",
] }
rand = "0.8.5"
regex = "1.10.2"
reqwest = "0.12"
rpc-toolkit = { git = "https://github.com/Start9Labs/rpc-toolkit.git", branch = "master" }
rustls = "0.23"
serde = { version = "1.0", features = ["derive", "rc"] }
serde_json = "1.0"
sqlx = { version = "0.7.2", features = [
"chrono",
"runtime-tokio-rustls",
"postgres",
] }
ssh-key = "0.6.2"
ts-rs = { git = "https://github.com/dr-bonez/ts-rs.git", branch = "feature/top-level-as" } # "8"
thiserror = "1.0"
tokio = { version = "1", features = ["full"] }
torut = { git = "https://github.com/Start9Labs/torut.git", branch = "update/dependencies" }
tracing = "0.1.39"
yasi = "0.1.5"
zbus = "5"

View File

@@ -1,612 +0,0 @@
use std::fmt::{Debug, Display};
use axum::http::uri::InvalidUri;
use axum::http::StatusCode;
use color_eyre::eyre::eyre;
use num_enum::TryFromPrimitive;
use patch_db::Revision;
use rpc_toolkit::reqwest;
use rpc_toolkit::yajrc::{
RpcError, INVALID_PARAMS_ERROR, INVALID_REQUEST_ERROR, METHOD_NOT_FOUND_ERROR, PARSE_ERROR,
};
use serde::{Deserialize, Serialize};
use crate::InvalidId;
#[derive(Debug, Clone, Copy, PartialEq, Eq, TryFromPrimitive)]
#[repr(i32)]
pub enum ErrorKind {
Unknown = 1,
Filesystem = 2,
Docker = 3,
ConfigSpecViolation = 4,
ConfigRulesViolation = 5,
NotFound = 6,
IncorrectPassword = 7,
VersionIncompatible = 8,
Network = 9,
Registry = 10,
Serialization = 11,
Deserialization = 12,
Utf8 = 13,
ParseVersion = 14,
IncorrectDisk = 15,
// Nginx = 16,
Dependency = 17,
ParseS9pk = 18,
ParseUrl = 19,
DiskNotAvailable = 20,
BlockDevice = 21,
InvalidOnionAddress = 22,
Pack = 23,
ValidateS9pk = 24,
DiskCorrupted = 25, // Remove
Tor = 26,
ConfigGen = 27,
ParseNumber = 28,
Database = 29,
InvalidId = 30,
InvalidSignature = 31,
Backup = 32,
Restore = 33,
Authorization = 34,
AutoConfigure = 35,
Action = 36,
RateLimited = 37,
InvalidRequest = 38,
MigrationFailed = 39,
Uninitialized = 40,
ParseNetAddress = 41,
ParseSshKey = 42,
SoundError = 43,
ParseTimestamp = 44,
ParseSysInfo = 45,
Wifi = 46,
Journald = 47,
DiskManagement = 48,
OpenSsl = 49,
PasswordHashGeneration = 50,
DiagnosticMode = 51,
ParseDbField = 52,
Duplicate = 53,
MultipleErrors = 54,
Incoherent = 55,
InvalidBackupTargetId = 56,
ProductKeyMismatch = 57,
LanPortConflict = 58,
Javascript = 59,
Pem = 60,
TLSInit = 61,
Ascii = 62,
MissingHeader = 63,
Grub = 64,
Systemd = 65,
OpenSsh = 66,
Zram = 67,
Lshw = 68,
CpuSettings = 69,
Firmware = 70,
Timeout = 71,
Lxc = 72,
Cancelled = 73,
Git = 74,
DBus = 75,
}
impl ErrorKind {
pub fn as_str(&self) -> &'static str {
use ErrorKind::*;
match self {
Unknown => "Unknown Error",
Filesystem => "Filesystem I/O Error",
Docker => "Docker Error",
ConfigSpecViolation => "Config Spec Violation",
ConfigRulesViolation => "Config Rules Violation",
NotFound => "Not Found",
IncorrectPassword => "Incorrect Password",
VersionIncompatible => "Version Incompatible",
Network => "Network Error",
Registry => "Registry Error",
Serialization => "Serialization Error",
Deserialization => "Deserialization Error",
Utf8 => "UTF-8 Parse Error",
ParseVersion => "Version Parsing Error",
IncorrectDisk => "Incorrect Disk",
// Nginx => "Nginx Error",
Dependency => "Dependency Error",
ParseS9pk => "S9PK Parsing Error",
ParseUrl => "URL Parsing Error",
DiskNotAvailable => "Disk Not Available",
BlockDevice => "Block Device Error",
InvalidOnionAddress => "Invalid Onion Address",
Pack => "Pack Error",
ValidateS9pk => "S9PK Validation Error",
DiskCorrupted => "Disk Corrupted", // Remove
Tor => "Tor Daemon Error",
ConfigGen => "Config Generation Error",
ParseNumber => "Number Parsing Error",
Database => "Database Error",
InvalidId => "Invalid ID",
InvalidSignature => "Invalid Signature",
Backup => "Backup Error",
Restore => "Restore Error",
Authorization => "Unauthorized",
AutoConfigure => "Auto-Configure Error",
Action => "Action Failed",
RateLimited => "Rate Limited",
InvalidRequest => "Invalid Request",
MigrationFailed => "Migration Failed",
Uninitialized => "Uninitialized",
ParseNetAddress => "Net Address Parsing Error",
ParseSshKey => "SSH Key Parsing Error",
SoundError => "Sound Interface Error",
ParseTimestamp => "Timestamp Parsing Error",
ParseSysInfo => "System Info Parsing Error",
Wifi => "WiFi Internal Error",
Journald => "Journald Error",
DiskManagement => "Disk Management Error",
OpenSsl => "OpenSSL Internal Error",
PasswordHashGeneration => "Password Hash Generation Error",
DiagnosticMode => "Server is in Diagnostic Mode",
ParseDbField => "Database Field Parse Error",
Duplicate => "Duplication Error",
MultipleErrors => "Multiple Errors",
Incoherent => "Incoherent",
InvalidBackupTargetId => "Invalid Backup Target ID",
ProductKeyMismatch => "Incompatible Product Keys",
LanPortConflict => "Incompatible LAN Port Configuration",
Javascript => "Javascript Engine Error",
Pem => "PEM Encoding Error",
TLSInit => "TLS Backend Initialization Error",
Ascii => "ASCII Parse Error",
MissingHeader => "Missing Header",
Grub => "Grub Error",
Systemd => "Systemd Error",
OpenSsh => "OpenSSH Error",
Zram => "Zram Error",
Lshw => "LSHW Error",
CpuSettings => "CPU Settings Error",
Firmware => "Firmware Error",
Timeout => "Timeout Error",
Lxc => "LXC Error",
Cancelled => "Cancelled",
Git => "Git Error",
DBus => "DBus Error",
}
}
}
impl Display for ErrorKind {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.as_str())
}
}
#[derive(Debug)]
pub struct Error {
pub source: color_eyre::eyre::Error,
pub kind: ErrorKind,
pub revision: Option<Revision>,
}
impl Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}: {}", self.kind.as_str(), self.source)
}
}
impl Error {
pub fn new<E: Into<color_eyre::eyre::Error>>(source: E, kind: ErrorKind) -> Self {
Error {
source: source.into(),
kind,
revision: None,
}
}
pub fn clone_output(&self) -> Self {
Error {
source: ErrorData {
details: format!("{}", self.source),
debug: format!("{:?}", self.source),
}
.into(),
kind: self.kind,
revision: self.revision.clone(),
}
}
}
impl axum::response::IntoResponse for Error {
fn into_response(self) -> axum::response::Response {
let mut res = axum::Json(RpcError::from(self)).into_response();
*res.status_mut() = StatusCode::INTERNAL_SERVER_ERROR;
res
}
}
impl From<std::convert::Infallible> for Error {
fn from(value: std::convert::Infallible) -> Self {
match value {}
}
}
impl From<InvalidId> for Error {
fn from(err: InvalidId) -> Self {
Error::new(err, ErrorKind::InvalidId)
}
}
impl From<std::io::Error> for Error {
fn from(e: std::io::Error) -> Self {
Error::new(e, ErrorKind::Filesystem)
}
}
impl From<std::str::Utf8Error> for Error {
fn from(e: std::str::Utf8Error) -> Self {
Error::new(e, ErrorKind::Utf8)
}
}
impl From<std::string::FromUtf8Error> for Error {
fn from(e: std::string::FromUtf8Error) -> Self {
Error::new(e, ErrorKind::Utf8)
}
}
impl From<exver::ParseError> for Error {
fn from(e: exver::ParseError) -> Self {
Error::new(e, ErrorKind::ParseVersion)
}
}
impl From<rpc_toolkit::url::ParseError> for Error {
fn from(e: rpc_toolkit::url::ParseError) -> Self {
Error::new(e, ErrorKind::ParseUrl)
}
}
impl From<std::num::ParseIntError> for Error {
fn from(e: std::num::ParseIntError) -> Self {
Error::new(e, ErrorKind::ParseNumber)
}
}
impl From<std::num::ParseFloatError> for Error {
fn from(e: std::num::ParseFloatError) -> Self {
Error::new(e, ErrorKind::ParseNumber)
}
}
impl From<patch_db::Error> for Error {
fn from(e: patch_db::Error) -> Self {
Error::new(e, ErrorKind::Database)
}
}
impl From<sqlx::Error> for Error {
fn from(e: sqlx::Error) -> Self {
Error::new(e, ErrorKind::Database)
}
}
impl From<ed25519_dalek::SignatureError> for Error {
fn from(e: ed25519_dalek::SignatureError) -> Self {
Error::new(e, ErrorKind::InvalidSignature)
}
}
impl From<std::net::AddrParseError> for Error {
fn from(e: std::net::AddrParseError) -> Self {
Error::new(e, ErrorKind::ParseNetAddress)
}
}
impl From<torut::control::ConnError> for Error {
fn from(e: torut::control::ConnError) -> Self {
Error::new(e, ErrorKind::Tor)
}
}
impl From<ipnet::AddrParseError> for Error {
fn from(e: ipnet::AddrParseError) -> Self {
Error::new(e, ErrorKind::ParseNetAddress)
}
}
impl From<openssl::error::ErrorStack> for Error {
fn from(e: openssl::error::ErrorStack) -> Self {
Error::new(eyre!("{}", e), ErrorKind::OpenSsl)
}
}
impl From<mbrman::Error> for Error {
fn from(e: mbrman::Error) -> Self {
Error::new(e, ErrorKind::DiskManagement)
}
}
impl From<InvalidUri> for Error {
fn from(e: InvalidUri) -> Self {
Error::new(eyre!("{}", e), ErrorKind::ParseUrl)
}
}
impl From<ssh_key::Error> for Error {
fn from(e: ssh_key::Error) -> Self {
Error::new(e, ErrorKind::OpenSsh)
}
}
impl From<reqwest::Error> for Error {
fn from(e: reqwest::Error) -> Self {
let kind = match e {
_ if e.is_builder() => ErrorKind::ParseUrl,
_ if e.is_decode() => ErrorKind::Deserialization,
_ => ErrorKind::Network,
};
Error::new(e, kind)
}
}
impl From<torut::onion::OnionAddressParseError> for Error {
fn from(e: torut::onion::OnionAddressParseError) -> Self {
Error::new(e, ErrorKind::Tor)
}
}
impl From<zbus::Error> for Error {
fn from(e: zbus::Error) -> Self {
Error::new(e, ErrorKind::DBus)
}
}
impl From<rustls::Error> for Error {
fn from(e: rustls::Error) -> Self {
Error::new(e, ErrorKind::OpenSsl)
}
}
impl From<patch_db::value::Error> for Error {
fn from(value: patch_db::value::Error) -> Self {
match value.kind {
patch_db::value::ErrorKind::Serialization => {
Error::new(value.source, ErrorKind::Serialization)
}
patch_db::value::ErrorKind::Deserialization => {
Error::new(value.source, ErrorKind::Deserialization)
}
}
}
}
#[derive(Clone, Deserialize, Serialize)]
pub struct ErrorData {
pub details: String,
pub debug: String,
}
impl Display for ErrorData {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
Display::fmt(&self.details, f)
}
}
impl Debug for ErrorData {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
Display::fmt(&self.debug, f)
}
}
impl std::error::Error for ErrorData {}
impl From<Error> for ErrorData {
fn from(value: Error) -> Self {
Self {
details: value.to_string(),
debug: format!("{:?}", value),
}
}
}
impl From<&RpcError> for ErrorData {
fn from(value: &RpcError) -> Self {
Self {
details: value
.data
.as_ref()
.and_then(|d| {
d.as_object()
.and_then(|d| {
d.get("details")
.and_then(|d| 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()),
debug: value
.data
.as_ref()
.and_then(|d| {
d.as_object()
.and_then(|d| {
d.get("debug")
.and_then(|d| 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()),
}
}
}
impl From<Error> for RpcError {
fn from(e: Error) -> Self {
let mut data_object = serde_json::Map::with_capacity(3);
data_object.insert("details".to_owned(), format!("{}", e.source).into());
data_object.insert("debug".to_owned(), format!("{:?}", e.source).into());
data_object.insert(
"revision".to_owned(),
match serde_json::to_value(&e.revision) {
Ok(a) => a,
Err(e) => {
tracing::warn!("Error serializing revision for Error object: {}", e);
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
}
},
),
}
}
}
impl From<RpcError> for Error {
fn from(e: RpcError) -> Self {
Error::new(
ErrorData::from(&e),
if let Ok(kind) = e.code.try_into() {
kind
} else if e.code == METHOD_NOT_FOUND_ERROR.code {
ErrorKind::NotFound
} else if e.code == PARSE_ERROR.code
|| e.code == INVALID_PARAMS_ERROR.code
|| e.code == INVALID_REQUEST_ERROR.code
{
ErrorKind::Deserialization
} else {
ErrorKind::Unknown
},
)
}
}
#[derive(Debug, Default)]
pub struct ErrorCollection(Vec<Error>);
impl ErrorCollection {
pub fn new() -> Self {
Self::default()
}
pub fn handle<T, E: Into<Error>>(&mut self, result: Result<T, E>) -> Option<T> {
match result {
Ok(a) => Some(a),
Err(e) => {
self.0.push(e.into());
None
}
}
}
pub fn into_result(self) -> Result<(), Error> {
if self.0.is_empty() {
Ok(())
} else {
Err(Error::new(eyre!("{}", self), ErrorKind::MultipleErrors))
}
}
}
impl From<ErrorCollection> for Result<(), Error> {
fn from(e: ErrorCollection) -> Self {
e.into_result()
}
}
impl<T, E: Into<Error>> Extend<Result<T, E>> for ErrorCollection {
fn extend<I: IntoIterator<Item = Result<T, E>>>(&mut self, iter: I) {
for item in iter {
self.handle(item);
}
}
}
impl std::fmt::Display for ErrorCollection {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
for (idx, e) in self.0.iter().enumerate() {
if idx > 0 {
write!(f, "; ")?;
}
write!(f, "{}", e)?;
}
Ok(())
}
}
pub trait ResultExt<T, E>
where
Self: Sized,
{
fn with_kind(self, kind: ErrorKind) -> Result<T, Error>;
fn with_ctx<F: FnOnce(&E) -> (ErrorKind, D), D: Display>(self, f: F) -> Result<T, Error>;
fn log_err(self) -> Option<T>;
}
impl<T, E> ResultExt<T, E> for Result<T, E>
where
color_eyre::eyre::Error: From<E>,
{
fn with_kind(self, kind: ErrorKind) -> Result<T, Error> {
self.map_err(|e| Error {
source: e.into(),
kind,
revision: None,
})
}
fn with_ctx<F: FnOnce(&E) -> (ErrorKind, D), D: Display>(self, f: F) -> Result<T, Error> {
self.map_err(|e| {
let (kind, ctx) = f(&e);
let source = color_eyre::eyre::Error::from(e);
let ctx = format!("{}: {}", ctx, source);
let source = source.wrap_err(ctx);
Error {
kind,
source,
revision: None,
}
})
}
fn log_err(self) -> Option<T> {
match self {
Ok(a) => Some(a),
Err(e) => {
let e: color_eyre::eyre::Error = e.into();
tracing::error!("{e}");
tracing::debug!("{e:?}");
None
}
}
}
}
impl<T> ResultExt<T, Error> for Result<T, Error> {
fn with_kind(self, kind: ErrorKind) -> Result<T, Error> {
self.map_err(|e| Error {
source: e.source,
kind,
revision: e.revision,
})
}
fn with_ctx<F: FnOnce(&Error) -> (ErrorKind, D), D: Display>(self, f: F) -> Result<T, Error> {
self.map_err(|e| {
let (kind, ctx) = f(&e);
let source = e.source;
let ctx = format!("{}: {}", ctx, source);
let source = source.wrap_err(ctx);
Error {
kind,
source,
revision: e.revision,
}
})
}
fn log_err(self) -> Option<T> {
match self {
Ok(a) => Some(a),
Err(e) => {
tracing::error!("{e}");
tracing::debug!("{e:?}");
None
}
}
}
}
pub trait OptionExt<T>
where
Self: Sized,
{
fn or_not_found(self, message: impl std::fmt::Display) -> Result<T, Error>;
}
impl<T> OptionExt<T> for Option<T> {
fn or_not_found(self, message: impl std::fmt::Display) -> Result<T, Error> {
self.ok_or_else(|| Error::new(eyre!("{}", message), ErrorKind::NotFound))
}
}
#[macro_export]
macro_rules! ensure_code {
($x:expr, $c:expr, $fmt:expr $(, $arg:expr)*) => {
if !($x) {
return Err(Error::new(color_eyre::eyre::eyre!($fmt, $($arg, )*), $c));
}
};
}

View File

@@ -1,164 +0,0 @@
use std::future::Future;
use std::pin::Pin;
use std::sync::Arc;
use std::time::Duration;
use color_eyre::eyre::bail;
use container_init::{Input, Output, ProcessId, RpcId};
use tokio::sync::mpsc::{UnboundedReceiver, UnboundedSender};
use tokio::sync::Mutex;
/// Used by the js-executor, it is the ability to just create a command in an already running exec
pub type ExecCommand = Arc<
dyn Fn(
String,
Vec<String>,
UnboundedSender<container_init::Output>,
Option<Duration>,
) -> Pin<Box<dyn Future<Output = Result<RpcId, String>> + 'static>>
+ Send
+ Sync
+ 'static,
>;
/// Used by the js-executor, it is the ability to just create a command in an already running exec
pub type SendKillSignal = Arc<
dyn Fn(RpcId, u32) -> Pin<Box<dyn Future<Output = Result<(), String>> + 'static>>
+ Send
+ Sync
+ 'static,
>;
pub trait CommandInserter {
fn insert_command(
&self,
command: String,
args: Vec<String>,
sender: UnboundedSender<container_init::Output>,
timeout: Option<Duration>,
) -> Pin<Box<dyn Future<Output = Option<RpcId>>>>;
fn send_signal(&self, id: RpcId, command: u32) -> Pin<Box<dyn Future<Output = ()>>>;
}
pub type ArcCommandInserter = Arc<Mutex<Option<Box<dyn CommandInserter>>>>;
pub struct ExecutingCommand {
rpc_id: RpcId,
/// Will exist until killed
command_inserter: Arc<Mutex<Option<ArcCommandInserter>>>,
owned_futures: Arc<Mutex<Vec<Pin<Box<dyn Future<Output = ()>>>>>>,
}
impl ExecutingCommand {
pub async fn new(
command_inserter: ArcCommandInserter,
command: String,
args: Vec<String>,
timeout: Option<Duration>,
) -> Result<ExecutingCommand, color_eyre::Report> {
let (sender, receiver) = tokio::sync::mpsc::unbounded_channel::<Output>();
let rpc_id = {
let locked_command_inserter = command_inserter.lock().await;
let locked_command_inserter = match &*locked_command_inserter {
Some(a) => a,
None => bail!("Expecting containers.main in the package manifest".to_string()),
};
match locked_command_inserter
.insert_command(command, args, sender, timeout)
.await
{
Some(a) => a,
None => bail!("Couldn't get command started ".to_string()),
}
};
let executing_commands = ExecutingCommand {
rpc_id,
command_inserter: Arc::new(Mutex::new(Some(command_inserter.clone()))),
owned_futures: Default::default(),
};
// let waiting = self.wait()
Ok(executing_commands)
}
async fn wait(
rpc_id: RpcId,
mut outputs: UnboundedReceiver<Output>,
) -> Result<String, (Option<i32>, String)> {
let (process_id_send, process_id_recv) = tokio::sync::oneshot::channel::<ProcessId>();
let mut answer = String::new();
let mut command_error = String::new();
let mut status: Option<i32> = None;
let mut process_id_send = Some(process_id_send);
while let Some(output) = outputs.recv().await {
match output {
Output::ProcessId(process_id) => {
if let Some(process_id_send) = process_id_send.take() {
if let Err(err) = process_id_send.send(process_id) {
tracing::error!(
"Could not get a process id {process_id:?} sent for {rpc_id:?}"
);
tracing::debug!("{err:?}");
}
}
}
Output::Line(value) => {
answer.push_str(&value);
answer.push('\n');
}
Output::Error(error) => {
command_error.push_str(&error);
command_error.push('\n');
}
Output::Done(error_code) => {
status = error_code;
break;
}
}
}
if !command_error.is_empty() {
return Err((status, command_error));
}
Ok(answer)
}
async fn send_signal(&self, signal: u32) {
let locked = self.command_inserter.lock().await;
let inner = match &*locked {
Some(a) => a,
None => return,
};
let locked = inner.lock().await;
let command_inserter = match &*locked {
Some(a) => a,
None => return,
};
command_inserter.send_signal(self.rpc_id, signal);
}
/// Should only be called when output::done
async fn killed(&self) {
*self.owned_futures.lock().await = Default::default();
*self.command_inserter.lock().await = Default::default();
}
pub fn rpc_id(&self) -> RpcId {
self.rpc_id
}
}
impl Drop for ExecutingCommand {
fn drop(&mut self) {
let command_inserter = self.command_inserter.clone();
let rpc_id = self.rpc_id.clone();
tokio::spawn(async move {
let command_inserter_lock = command_inserter.lock().await;
let command_inserter = match &*command_inserter_lock {
Some(a) => a,
None => {
return;
}
};
command_inserter.send_kill_command(rpc_id, 9).await;
});
}
}

View File

@@ -1,15 +0,0 @@
mod clap;
mod data_url;
mod errors;
mod id;
mod mime;
mod procedure_name;
mod version;
pub use clap::*;
pub use data_url::*;
pub use errors::*;
pub use id::*;
pub use mime::*;
pub use procedure_name::*;
pub use version::*;

View File

@@ -2,9 +2,21 @@
cd "$(dirname "${BASH_SOURCE[0]}")" cd "$(dirname "${BASH_SOURCE[0]}")"
source ./builder-alias.sh
set -ea set -ea
shopt -s expand_aliases shopt -s expand_aliases
PROFILE=${PROFILE:-release}
if [ "${PROFILE}" = "release" ]; then
BUILD_FLAGS="--release"
else
if [ "$PROFILE" != "debug"]; then
>&2 echo "Unknown profile $PROFILE: falling back to debug..."
PROFILE=debug
fi
fi
if [ -z "$ARCH" ]; then if [ -z "$ARCH" ]; then
ARCH=$(uname -m) ARCH=$(uname -m)
fi fi
@@ -22,15 +34,12 @@ cd ..
FEATURES="$(echo $ENVIRONMENT | sed 's/-/,/g')" FEATURES="$(echo $ENVIRONMENT | sed 's/-/,/g')"
RUSTFLAGS="" RUSTFLAGS=""
if [[ "${ENVIRONMENT}" =~ (^|-)unstable($|-) ]]; then if [[ "${ENVIRONMENT}" =~ (^|-)console($|-) ]]; then
RUSTFLAGS="--cfg tokio_unstable" RUSTFLAGS="--cfg tokio_unstable"
fi fi
alias 'rust-musl-builder'='docker run $USE_TTY --rm -e "RUSTFLAGS=$RUSTFLAGS" -v "$HOME/.cargo/registry":/root/.cargo/registry -v "$HOME/.cargo/git":/root/.cargo/git -v "$(pwd)":/home/rust/src -w /home/rust/src -P messense/rust-musl-cross:$ARCH-musl'
echo "FEATURES=\"$FEATURES\"" echo "FEATURES=\"$FEATURES\""
echo "RUSTFLAGS=\"$RUSTFLAGS\"" echo "RUSTFLAGS=\"$RUSTFLAGS\""
rust-musl-builder sh -c "apt-get update && apt-get install -y rsync && cd core && cargo test --release --features=test,$FEATURES --workspace --locked --target=$ARCH-unknown-linux-musl -- --skip export_bindings_ && chown \$UID:\$UID target" rust-zig-builder cargo test --manifest-path=./core/Cargo.toml $BUILD_FLAGS --features=test,$FEATURES --workspace --locked --lib -- --skip export_bindings_
if [ "$(ls -nd core/target | awk '{ print $3 }')" != "$UID" ]; then rust-zig-builder sh -c "chown -R $UID:$UID core/target && chown -R $UID:$UID /usr/local/cargo"
rust-musl-builder sh -c "cd core && chown -R $UID:$UID target && chown -R $UID:$UID /root/.cargo"
fi

View File

@@ -2,20 +2,20 @@
authors = ["Aiden McClelland <me@drbonez.dev>"] authors = ["Aiden McClelland <me@drbonez.dev>"]
description = "The core of StartOS" description = "The core of StartOS"
documentation = "https://docs.rs/start-os" documentation = "https://docs.rs/start-os"
edition = "2021" edition = "2024"
keywords = [ keywords = [
"self-hosted",
"raspberry-pi",
"privacy",
"bitcoin", "bitcoin",
"full-node", "full-node",
"lightning", "lightning",
"privacy",
"raspberry-pi",
"self-hosted",
] ]
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.3.6-alpha.18" # VERSION_BUMP version = "0.4.0-alpha.16" # VERSION_BUMP
license = "MIT"
[lib] [lib]
name = "startos" name = "startos"
@@ -23,47 +23,68 @@ path = "src/lib.rs"
[[bin]] [[bin]]
name = "startbox" name = "startbox"
path = "src/main.rs" path = "src/main/startbox.rs"
[[bin]] [[bin]]
name = "start-cli" name = "start-cli"
path = "src/main.rs" path = "src/main/start-cli.rs"
[[bin]] [[bin]]
name = "containerbox" name = "start-container"
path = "src/main.rs" path = "src/main/start-container.rs"
[[bin]] [[bin]]
name = "registrybox" name = "registrybox"
path = "src/main.rs" path = "src/main/registrybox.rs"
[[bin]]
name = "tunnelbox"
path = "src/main/tunnelbox.rs"
[features] [features]
cli = [] arti = [
container-runtime = ["procfs", "tty-spawn"] "arti-client",
daemon = ["mail-send"] "safelog",
registry = [] "tor-cell",
default = ["cli", "daemon", "registry", "container-runtime"] "tor-hscrypto",
"tor-hsservice",
"tor-keymgr",
"tor-llcrypto",
"tor-proto",
"tor-rtcompat",
]
console = ["console-subscriber", "tokio/tracing"]
default = []
dev = [] dev = []
unstable = ["console-subscriber", "tokio/tracing"]
docker = []
test = [] test = []
unstable = ["backtrace-on-stack-overflow"]
[dependencies] [dependencies]
aes = { version = "0.7.5", features = ["ctr"] } aes = { version = "0.7.5", features = ["ctr"] }
arti-client = { version = "0.33", features = [
"compression",
"ephemeral-keystore",
"experimental-api",
"onion-service-client",
"onion-service-service",
"rustls",
"static",
"tokio",
], default-features = false, git = "https://github.com/Start9Labs/arti.git", branch = "patch/disable-exit", optional = true }
async-acme = { version = "0.6.0", git = "https://github.com/dr-bonez/async-acme.git", features = [ async-acme = { version = "0.6.0", git = "https://github.com/dr-bonez/async-acme.git", features = [
"use_rustls", "use_rustls",
"use_tokio", "use_tokio",
] } ] }
async-compression = { version = "0.4.4", features = [ async-compression = { version = "0.4.32", features = [
"gzip",
"brotli", "brotli",
"gzip",
"tokio", "tokio",
"zstd",
] } ] }
async-stream = "0.3.5" async-stream = "0.3.5"
async-trait = "0.1.74" async-trait = "0.1.74"
axum = { version = "0.7.3", features = ["ws"] } axum = { version = "0.8.4", features = ["ws", "http2"] }
barrage = "0.2.3" backtrace-on-stack-overflow = { version = "0.3.0", optional = true }
backhand = "0.18.0"
base32 = "0.5.0" base32 = "0.5.0"
base64 = "0.22.1" base64 = "0.22.1"
base64ct = "1.6.0" base64ct = "1.6.0"
@@ -73,21 +94,24 @@ bytes = "1"
chrono = { version = "0.4.31", features = ["serde"] } chrono = { version = "0.4.31", features = ["serde"] }
clap = { version = "4.4.12", features = ["string"] } clap = { version = "4.4.12", features = ["string"] }
color-eyre = "0.6.2" color-eyre = "0.6.2"
console = "0.15.7" console = "0.16.2"
console-subscriber = { version = "0.3.0", optional = true } console-subscriber = { version = "0.5.0", optional = true }
const_format = "0.2.34" const_format = "0.2.34"
cookie = "0.18.0" cookie = "0.18.0"
cookie_store = "0.21.0" cookie_store = "0.22.0"
curve25519-dalek = "4.1.3"
der = { version = "0.7.9", features = ["derive", "pem"] } der = { version = "0.7.9", features = ["derive", "pem"] }
digest = "0.10.7" digest = "0.10.7"
divrem = "1.0.0" divrem = "1.0.0"
ed25519 = { version = "2.2.3", features = ["pkcs8", "pem", "alloc"] } dns-lookup = "3.0.1"
ed25519-dalek = { version = "2.1.1", features = [ ed25519 = { version = "2.2.3", features = ["alloc", "pem", "pkcs8"] }
ed25519-dalek = { version = "2.2.0", features = [
"digest",
"hazmat",
"pkcs8",
"rand_core",
"serde", "serde",
"zeroize", "zeroize",
"rand_core",
"digest",
"pkcs8",
] } ] }
ed25519-dalek-v1 = { package = "ed25519-dalek", version = "1" } ed25519-dalek-v1 = { package = "ed25519-dalek", version = "1" }
exver = { version = "0.2.0", git = "https://github.com/Start9Labs/exver-rs.git", features = [ exver = { version = "0.2.0", git = "https://github.com/Start9Labs/exver-rs.git", features = [
@@ -96,50 +120,61 @@ exver = { version = "0.2.0", git = "https://github.com/Start9Labs/exver-rs.git",
fd-lock-rs = "0.1.4" fd-lock-rs = "0.1.4"
form_urlencoded = "1.2.1" form_urlencoded = "1.2.1"
futures = "0.3.28" futures = "0.3.28"
gpt = "3.1.0" gpt = "4.1.0"
helpers = { path = "../helpers" }
hex = "0.4.3" hex = "0.4.3"
hickory-client = "0.25.2"
hickory-server = "0.25.2"
hmac = "0.12.1" hmac = "0.12.1"
http = "1.0.0" http = "1.0.0"
http-body-util = "0.1" http-body-util = "0.1"
hyper = { version = "1.5", features = ["server", "http1", "http2"] } hyper = { version = "1.5", features = ["http1", "http2", "server"] }
hyper-util = { version = "0.1.10", features = [ hyper-util = { version = "0.1.10", features = [
"http1",
"http2",
"server", "server",
"server-auto", "server-auto",
"server-graceful", "server-graceful",
"service", "service",
"http1",
"http2",
"tokio", "tokio",
] } ] }
id-pool = { version = "0.2.2", default-features = false, features = [ id-pool = { version = "0.2.2", default-features = false, features = [
"serde", "serde",
"u16", "u16",
] } ] }
imbl = "2.0.3" iddqd = "0.3.14"
imbl-value = "0.1.2" imbl = { version = "6", features = ["serde", "small-chunks"] }
imbl-value = { version = "0.4.3", features = ["ts-rs"] }
include_dir = { version = "0.7.3", features = ["metadata"] } include_dir = { version = "0.7.3", features = ["metadata"] }
indexmap = { version = "2.0.2", features = ["serde"] } indexmap = { version = "2.0.2", features = ["serde"] }
indicatif = { version = "0.17.7", features = ["tokio"] } indicatif = { version = "0.18.3", features = ["tokio"] }
inotify = "0.11.0"
integer-encoding = { version = "4.0.0", features = ["tokio_async"] } integer-encoding = { version = "4.0.0", features = ["tokio_async"] }
ipnet = { version = "2.8.0", features = ["serde"] } ipnet = { version = "2.8.0", features = ["serde"] }
iprange = { version = "0.6.7", features = ["serde"] }
isocountry = "0.3.2" isocountry = "0.3.2"
itertools = "0.13.0" itertools = "0.14.0"
jaq-core = "0.10.1" jaq-core = "0.10.1"
jaq-std = "0.10.0" jaq-std = "0.10.0"
josekit = "0.8.4" josekit = "0.10.3"
jsonpath_lib = { git = "https://github.com/Start9Labs/jsonpath.git" } jsonpath_lib = { git = "https://github.com/Start9Labs/jsonpath.git" }
lazy_async_pool = "0.3.3" lazy_async_pool = "0.3.3"
lazy_format = "2.0" lazy_format = "2.0"
lazy_static = "1.4.0" lazy_static = "1.4.0"
lettre = { version = "0.11.18", default-features = false, features = [
"aws-lc-rs",
"builder",
"hostname",
"pool",
"rustls-platform-verifier",
"smtp-transport",
"tokio1-rustls",
] }
libc = "0.2.149" libc = "0.2.149"
log = "0.4.20" log = "0.4.20"
mbrman = "0.6.0"
miette = { version = "7.6.0", features = ["fancy"] }
mio = "1" mio = "1"
mbrman = "0.5.2"
models = { version = "*", path = "../models" }
new_mime_guess = "4" new_mime_guess = "4"
nix = { version = "0.29.0", features = [ nix = { version = "0.30.1", features = [
"fs", "fs",
"mount", "mount",
"net", "net",
@@ -148,10 +183,10 @@ nix = { version = "0.29.0", features = [
"signal", "signal",
"user", "user",
] } ] }
nom = "7.1.3" nom = "8.0.0"
num = "0.4.1" num = "0.4.1"
num_enum = "0.7.0"
num_cpus = "1.16.0" num_cpus = "1.16.0"
num_enum = "0.7.0"
once_cell = "1.19.0" once_cell = "1.19.0"
openssh-keys = "0.6.2" openssh-keys = "0.6.2"
openssl = { version = "0.10.57", features = ["vendored"] } openssl = { version = "0.10.57", features = ["vendored"] }
@@ -163,70 +198,83 @@ pbkdf2 = "0.12.2"
pin-project = "1.1.3" pin-project = "1.1.3"
pkcs8 = { version = "0.10.2", features = ["std"] } pkcs8 = { version = "0.10.2", features = ["std"] }
prettytable-rs = "0.10.0" prettytable-rs = "0.10.0"
procfs = { version = "0.16.0", optional = true }
proptest = "1.3.1" proptest = "1.3.1"
proptest-derive = "0.5.0" proptest-derive = "0.7.0"
qrcode = "0.14.1" qrcode = "0.14.1"
rand = "0.9.0" r3bl_tui = "0.7.6"
rand = "0.9.2"
regex = "1.10.2" regex = "1.10.2"
reqwest = { version = "0.12.4", features = ["stream", "json", "socks"] } reqwest = { version = "0.12.25", features = [
reqwest_cookie_store = "0.8.0" "json",
"socks",
"stream",
"http2",
] }
reqwest_cookie_store = "0.9.0"
rpassword = "7.2.0" rpassword = "7.2.0"
rpc-toolkit = { git = "https://github.com/Start9Labs/rpc-toolkit.git", branch = "master" } rust-argon2 = "3.0.0"
rust-argon2 = "2.0.0" rpc-toolkit = { git = "https://github.com/Start9Labs/rpc-toolkit.git" }
rustyline-async = "0.4.1" safelog = { version = "0.4.8", git = "https://github.com/Start9Labs/arti.git", branch = "patch/disable-exit", optional = true }
semver = { version = "1.0.20", features = ["serde"] } semver = { version = "1.0.20", features = ["serde"] }
serde = { version = "1.0", features = ["derive", "rc"] } serde = { version = "1.0", features = ["derive", "rc"] }
serde_cbor = { package = "ciborium", version = "0.2.1" } serde_cbor = { package = "ciborium", version = "0.2.1" }
serde_json = "1.0" serde_json = "1.0"
serde_toml = { package = "toml", version = "0.8.2" } serde_toml = { package = "toml", version = "0.9.9+spec-1.0.0" }
serde_urlencoded = "0.7" serde_yaml = { package = "serde_yml", version = "0.0.12" }
serde_with = { version = "3.4.0", features = ["macros", "json"] } sha-crypt = "0.5.0"
serde_yaml = { package = "serde_yml", version = "0.0.10" }
sha2 = "0.10.2" sha2 = "0.10.2"
shell-words = "1"
signal-hook = "0.3.17" signal-hook = "0.3.17"
simple-logging = "2.0.2" socket2 = { version = "0.6.0", features = ["all"] }
socket2 = "0.5.7" socks5-impl = { version = "0.7.2", features = ["client", "server"] }
sqlx = { version = "0.7.2", features = [ sqlx = { version = "0.8.6", features = [
"chrono",
"runtime-tokio-rustls",
"postgres", "postgres",
] } "runtime-tokio-rustls",
], default-features = false }
sscanf = "0.4.1" sscanf = "0.4.1"
ssh-key = { version = "0.6.2", features = ["ed25519"] } ssh-key = { version = "0.6.2", features = ["ed25519"] }
tar = "0.4.40" tar = "0.4.40"
thiserror = "1.0.49" termion = "4.0.5"
textwrap = "0.16.1" textwrap = "0.16.1"
thiserror = "2.0.12"
tokio = { version = "1.38.1", features = ["full"] } tokio = { version = "1.38.1", features = ["full"] }
tokio-rustls = "0.26.0" tokio-rustls = "0.26.4"
tokio-socks = "0.5.1" tokio-stream = { version = "0.1.14", features = ["io-util", "net", "sync"] }
tokio-stream = { version = "0.1.14", features = ["io-util", "sync", "net"] }
tokio-tar = { git = "https://github.com/dr-bonez/tokio-tar.git" } tokio-tar = { git = "https://github.com/dr-bonez/tokio-tar.git" }
tokio-tungstenite = { version = "0.23.1", features = ["native-tls", "url"] } tokio-tungstenite = { version = "0.26.2", features = ["native-tls", "url"] }
tokio-util = { version = "0.7.9", features = ["io"] } tokio-util = { version = "0.7.9", features = ["io"] }
torut = { git = "https://github.com/Start9Labs/torut.git", branch = "update/dependencies", features = [ tor-cell = { version = "0.33", git = "https://github.com/Start9Labs/arti.git", branch = "patch/disable-exit", optional = true }
"serialize", tor-hscrypto = { version = "0.33", features = [
] } "full",
], git = "https://github.com/Start9Labs/arti.git", branch = "patch/disable-exit", optional = true }
tor-hsservice = { version = "0.33", git = "https://github.com/Start9Labs/arti.git", branch = "patch/disable-exit", optional = true }
tor-keymgr = { version = "0.33", features = [
"ephemeral-keystore",
], git = "https://github.com/Start9Labs/arti.git", branch = "patch/disable-exit", optional = true }
tor-llcrypto = { version = "0.33", features = [
"full",
], git = "https://github.com/Start9Labs/arti.git", branch = "patch/disable-exit", optional = true }
tor-proto = { version = "0.33", git = "https://github.com/Start9Labs/arti.git", branch = "patch/disable-exit", optional = true }
tor-rtcompat = { version = "0.33", features = [
"rustls",
"tokio",
], git = "https://github.com/Start9Labs/arti.git", branch = "patch/disable-exit", optional = true }
torut = "0.2.1"
tower-service = "0.3.3" tower-service = "0.3.3"
tracing = "0.1.39" tracing = "0.1.39"
tracing-error = "0.2.0" tracing-error = "0.2.0"
tracing-futures = "0.2.5"
tracing-journald = "0.3.0" tracing-journald = "0.3.0"
tracing-subscriber = { version = "0.3.17", features = ["env-filter"] } tracing-subscriber = { version = "0.3.17", features = ["env-filter"] }
trust-dns-server = "0.23.1" ts-rs = "9.0.1"
ts-rs = { git = "https://github.com/dr-bonez/ts-rs.git", branch = "feature/top-level-as" } # "8.1.0" typed-builder = "0.23.2"
tty-spawn = { version = "0.4.0", optional = true }
typed-builder = "0.18.0"
unix-named-pipe = "0.2.0"
url = { version = "2.4.1", features = ["serde"] } url = { version = "2.4.1", features = ["serde"] }
urlencoding = "2.1.3"
uuid = { version = "1.4.1", features = ["v4"] } uuid = { version = "1.4.1", features = ["v4"] }
visit-rs = "0.1.1"
x25519-dalek = { version = "2.0.1", features = ["static_secrets"] }
zbus = "5.1.1" zbus = "5.1.1"
zeroize = "1.6.0"
mail-send = { git = "https://github.com/dr-bonez/mail-send.git", branch = "main", optional = true } [target.'cfg(target_os = "linux")'.dependencies]
rustls = "0.23.20" procfs = "0.18.0"
rustls-pki-types = { version = "1.10.1", features = ["alloc"] } pty-process = "0.5.1"
[profile.test] [profile.test]
opt-level = 3 opt-level = 3

View File

@@ -1,12 +1,14 @@
use std::collections::BTreeMap;
use std::time::SystemTime; use std::time::SystemTime;
use imbl_value::InternedString;
use openssl::pkey::{PKey, Private}; use openssl::pkey::{PKey, Private};
use openssl::x509::X509; use openssl::x509::X509;
use torut::onion::TorSecretKeyV3;
use crate::db::model::DatabaseModel; use crate::db::model::DatabaseModel;
use crate::hostname::{generate_hostname, generate_id, Hostname}; use crate::hostname::{Hostname, generate_hostname, generate_id};
use crate::net::ssl::{generate_key, make_root_cert}; use crate::net::ssl::{gen_nistp256, make_root_cert};
use crate::net::tor::TorSecretKey;
use crate::prelude::*; use crate::prelude::*;
use crate::util::serde::Pem; use crate::util::serde::Pem;
@@ -19,28 +21,28 @@ fn hash_password(password: &str) -> Result<String, Error> {
.with_kind(crate::ErrorKind::PasswordHashGeneration) .with_kind(crate::ErrorKind::PasswordHashGeneration)
} }
#[derive(Debug, Clone)] #[derive(Clone)]
pub struct AccountInfo { pub struct AccountInfo {
pub server_id: String, pub server_id: String,
pub hostname: Hostname, pub hostname: Hostname,
pub password: String, pub password: String,
pub tor_keys: Vec<TorSecretKeyV3>, pub tor_keys: Vec<TorSecretKey>,
pub root_ca_key: PKey<Private>, pub root_ca_key: PKey<Private>,
pub root_ca_cert: X509, pub root_ca_cert: X509,
pub ssh_key: ssh_key::PrivateKey, pub ssh_key: ssh_key::PrivateKey,
pub compat_s9pk_key: ed25519_dalek::SigningKey, pub developer_key: ed25519_dalek::SigningKey,
} }
impl AccountInfo { impl AccountInfo {
pub fn new(password: &str, start_time: SystemTime) -> Result<Self, Error> { pub fn new(password: &str, start_time: SystemTime) -> Result<Self, Error> {
let server_id = generate_id(); let server_id = generate_id();
let hostname = generate_hostname(); let hostname = generate_hostname();
let tor_key = vec![TorSecretKeyV3::generate()]; let tor_key = vec![TorSecretKey::generate()];
let root_ca_key = generate_key()?; let root_ca_key = gen_nistp256()?;
let root_ca_cert = make_root_cert(&root_ca_key, &hostname, start_time)?; let root_ca_cert = make_root_cert(&root_ca_key, &hostname, start_time)?;
let ssh_key = ssh_key::PrivateKey::from(ssh_key::private::Ed25519Keypair::random( let ssh_key = ssh_key::PrivateKey::from(ssh_key::private::Ed25519Keypair::random(
&mut ssh_key::rand_core::OsRng::default(), &mut ssh_key::rand_core::OsRng::default(),
)); ));
let compat_s9pk_key = let developer_key =
ed25519_dalek::SigningKey::generate(&mut ssh_key::rand_core::OsRng::default()); ed25519_dalek::SigningKey::generate(&mut ssh_key::rand_core::OsRng::default());
Ok(Self { Ok(Self {
server_id, server_id,
@@ -50,7 +52,7 @@ impl AccountInfo {
root_ca_key, root_ca_key,
root_ca_cert, root_ca_cert,
ssh_key, ssh_key,
compat_s9pk_key, developer_key,
}) })
} }
@@ -59,7 +61,13 @@ impl AccountInfo {
let hostname = Hostname(db.as_public().as_server_info().as_hostname().de()?); let hostname = Hostname(db.as_public().as_server_info().as_hostname().de()?);
let password = db.as_private().as_password().de()?; let password = db.as_private().as_password().de()?;
let key_store = db.as_private().as_key_store(); let key_store = db.as_private().as_key_store();
let tor_addrs = db.as_public().as_server_info().as_host().as_onions().de()?; let tor_addrs = db
.as_public()
.as_server_info()
.as_network()
.as_host()
.as_onions()
.de()?;
let tor_keys = tor_addrs let tor_keys = tor_addrs
.into_iter() .into_iter()
.map(|tor_addr| key_store.as_onion().get_key(&tor_addr)) .map(|tor_addr| key_store.as_onion().get_key(&tor_addr))
@@ -68,7 +76,7 @@ impl AccountInfo {
let root_ca_key = cert_store.as_root_key().de()?.0; let root_ca_key = cert_store.as_root_key().de()?.0;
let root_ca_cert = cert_store.as_root_cert().de()?.0; let root_ca_cert = cert_store.as_root_cert().de()?.0;
let ssh_key = db.as_private().as_ssh_privkey().de()?.0; let ssh_key = db.as_private().as_ssh_privkey().de()?.0;
let compat_s9pk_key = db.as_private().as_compat_s9pk_key().de()?.0; let compat_s9pk_key = db.as_private().as_developer_key().de()?.0;
Ok(Self { Ok(Self {
server_id, server_id,
@@ -78,7 +86,7 @@ impl AccountInfo {
root_ca_key, root_ca_key,
root_ca_cert, root_ca_cert,
ssh_key, ssh_key,
compat_s9pk_key, developer_key: compat_s9pk_key,
}) })
} }
@@ -89,31 +97,44 @@ impl AccountInfo {
server_info server_info
.as_pubkey_mut() .as_pubkey_mut()
.ser(&self.ssh_key.public_key().to_openssh()?)?; .ser(&self.ssh_key.public_key().to_openssh()?)?;
server_info.as_host_mut().as_onions_mut().ser( server_info
&self .as_network_mut()
.tor_keys .as_host_mut()
.iter() .as_onions_mut()
.map(|tor_key| tor_key.public().get_onion_address()) .ser(
.collect(), &self
)?; .tor_keys
.iter()
.map(|tor_key| tor_key.onion_address())
.collect(),
)?;
server_info.as_password_hash_mut().ser(&self.password)?;
db.as_private_mut().as_password_mut().ser(&self.password)?; db.as_private_mut().as_password_mut().ser(&self.password)?;
db.as_private_mut() db.as_private_mut()
.as_ssh_privkey_mut() .as_ssh_privkey_mut()
.ser(Pem::new_ref(&self.ssh_key))?; .ser(Pem::new_ref(&self.ssh_key))?;
db.as_private_mut() db.as_private_mut()
.as_compat_s9pk_key_mut() .as_developer_key_mut()
.ser(Pem::new_ref(&self.compat_s9pk_key))?; .ser(Pem::new_ref(&self.developer_key))?;
let key_store = db.as_private_mut().as_key_store_mut(); let key_store = db.as_private_mut().as_key_store_mut();
for tor_key in &self.tor_keys { for tor_key in &self.tor_keys {
key_store.as_onion_mut().insert_key(tor_key)?; key_store.as_onion_mut().insert_key(tor_key)?;
} }
let cert_store = key_store.as_local_certs_mut(); let cert_store = key_store.as_local_certs_mut();
cert_store if cert_store.as_root_cert().de()?.0 != self.root_ca_cert {
.as_root_key_mut() cert_store
.ser(Pem::new_ref(&self.root_ca_key))?; .as_root_key_mut()
cert_store .ser(Pem::new_ref(&self.root_ca_key))?;
.as_root_cert_mut() cert_store
.ser(Pem::new_ref(&self.root_ca_cert))?; .as_root_cert_mut()
.ser(Pem::new_ref(&self.root_ca_cert))?;
let int_key = crate::net::ssl::gen_nistp256()?;
let int_cert =
crate::net::ssl::make_int_cert((&self.root_ca_key, &self.root_ca_cert), &int_key)?;
cert_store.as_int_key_mut().ser(&Pem(int_key))?;
cert_store.as_int_cert_mut().ser(&Pem(int_cert))?;
cert_store.as_leaves_mut().ser(&BTreeMap::new())?;
}
Ok(()) Ok(())
} }
@@ -121,4 +142,17 @@ impl AccountInfo {
self.password = hash_password(password)?; self.password = hash_password(password)?;
Ok(()) Ok(())
} }
pub fn hostnames(&self) -> impl IntoIterator<Item = InternedString> + Send + '_ {
[
self.hostname.no_dot_host_name(),
self.hostname.local_domain_name(),
]
.into_iter()
.chain(
self.tor_keys
.iter()
.map(|k| InternedString::from_display(&k.onion_address())),
)
}
} }

View File

@@ -1,20 +1,21 @@
use std::fmt; use std::fmt;
use clap::{CommandFactory, FromArgMatches, Parser}; use clap::{CommandFactory, FromArgMatches, Parser};
pub use models::ActionId;
use models::PackageId;
use qrcode::QrCode; use qrcode::QrCode;
use rpc_toolkit::{from_fn_async, Context, HandlerExt, ParentHandler}; use rpc_toolkit::{Context, HandlerExt, ParentHandler, from_fn_async};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use tracing::instrument; use tracing::instrument;
use ts_rs::TS; use ts_rs::TS;
pub use crate::ActionId;
use crate::context::{CliContext, RpcContext}; use crate::context::{CliContext, RpcContext};
use crate::db::model::package::TaskSeverity;
use crate::prelude::*; use crate::prelude::*;
use crate::rpc_continuations::Guid; use crate::rpc_continuations::Guid;
use crate::util::serde::{ use crate::util::serde::{
display_serializable, HandlerExtSerde, StdinDeserializable, WithIoFormat, HandlerExtSerde, StdinDeserializable, WithIoFormat, display_serializable,
}; };
use crate::{PackageId, ReplayId};
pub fn action_api<C: Context>() -> ParentHandler<C> { pub fn action_api<C: Context>() -> ParentHandler<C> {
ParentHandler::new() ParentHandler::new()
@@ -38,12 +39,21 @@ pub fn action_api<C: Context>() -> ParentHandler<C> {
.with_about("Run service action") .with_about("Run service action")
.with_call_remote::<CliContext>(), .with_call_remote::<CliContext>(),
) )
.subcommand(
"clear-task",
from_fn_async(clear_task)
.no_display()
.with_about("Clear a service task")
.with_call_remote::<CliContext>(),
)
} }
#[derive(Debug, Clone, Deserialize, Serialize, TS)] #[derive(Debug, Clone, Deserialize, Serialize, TS)]
#[ts(export)] #[ts(export)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct ActionInput { pub struct ActionInput {
#[serde(default)]
pub event_id: Guid,
#[ts(type = "Record<string, unknown>")] #[ts(type = "Record<string, unknown>")]
pub spec: Value, pub spec: Value,
#[ts(type = "Record<string, unknown> | null")] #[ts(type = "Record<string, unknown> | null")]
@@ -83,6 +93,28 @@ pub enum ActionResult {
#[serde(rename = "1")] #[serde(rename = "1")]
V1(ActionResultV1), V1(ActionResultV1),
} }
impl ActionResult {
pub fn upcast(self) -> Self {
match self {
Self::V0(ActionResultV0 {
message,
value,
copyable,
qr,
}) => Self::V1(ActionResultV1 {
title: "Action Complete".into(),
message: Some(message),
result: value.map(|value| ActionResultValue::Single {
value,
copyable,
qr,
masked: false,
}),
}),
Self::V1(a) => Self::V1(a),
}
}
}
impl fmt::Display for ActionResult { impl fmt::Display for ActionResult {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self { match self {
@@ -222,20 +254,25 @@ impl fmt::Display for ActionResultV1 {
} }
} }
pub fn display_action_result<T: Serialize>(params: WithIoFormat<T>, result: Option<ActionResult>) { pub fn display_action_result<T: Serialize>(
params: WithIoFormat<T>,
result: Option<ActionResult>,
) -> Result<(), Error> {
let Some(result) = result else { let Some(result) = result else {
return; return Ok(());
}; };
if let Some(format) = params.format { if let Some(format) = params.format {
return display_serializable(format, result); return display_serializable(format, result);
} }
println!("{result}") println!("{result}");
Ok(())
} }
#[derive(Deserialize, Serialize, TS)] #[derive(Deserialize, Serialize, TS)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct RunActionParams { pub struct RunActionParams {
pub package_id: PackageId, pub package_id: PackageId,
pub event_id: Option<Guid>,
pub action_id: ActionId, pub action_id: ActionId,
#[ts(optional, type = "any")] #[ts(optional, type = "any")]
pub input: Option<Value>, pub input: Option<Value>,
@@ -244,6 +281,7 @@ pub struct RunActionParams {
#[derive(Parser)] #[derive(Parser)]
struct CliRunActionParams { struct CliRunActionParams {
pub package_id: PackageId, pub package_id: PackageId,
pub event_id: Option<Guid>,
pub action_id: ActionId, pub action_id: ActionId,
#[command(flatten)] #[command(flatten)]
pub input: StdinDeserializable<Option<Value>>, pub input: StdinDeserializable<Option<Value>>,
@@ -252,12 +290,14 @@ impl From<CliRunActionParams> for RunActionParams {
fn from( fn from(
CliRunActionParams { CliRunActionParams {
package_id, package_id,
event_id,
action_id, action_id,
input, input,
}: CliRunActionParams, }: CliRunActionParams,
) -> Self { ) -> Self {
Self { Self {
package_id, package_id,
event_id,
action_id, action_id,
input: input.0, input: input.0,
} }
@@ -297,6 +337,7 @@ pub async fn run_action(
ctx: RpcContext, ctx: RpcContext,
RunActionParams { RunActionParams {
package_id, package_id,
event_id,
action_id, action_id,
input, input,
}: RunActionParams, }: RunActionParams,
@@ -306,6 +347,54 @@ pub async fn run_action(
.await .await
.as_ref() .as_ref()
.or_not_found(lazy_format!("Manager for {}", package_id))? .or_not_found(lazy_format!("Manager for {}", package_id))?
.run_action(Guid::new(), action_id, input.unwrap_or_default()) .run_action(
event_id.unwrap_or_default(),
action_id,
input.unwrap_or_default(),
)
.await .await
.map(|res| res.map(ActionResult::upcast))
}
#[derive(Deserialize, Serialize, Parser, TS)]
#[serde(rename_all = "camelCase")]
#[command(rename_all = "kebab-case")]
pub struct ClearTaskParams {
pub package_id: PackageId,
pub replay_id: ReplayId,
#[arg(long)]
#[serde(default)]
pub force: bool,
}
#[instrument(skip_all)]
pub async fn clear_task(
ctx: RpcContext,
ClearTaskParams {
package_id,
replay_id,
force,
}: ClearTaskParams,
) -> Result<(), Error> {
ctx.db
.mutate(|db| {
if let Some(task) = db
.as_public_mut()
.as_package_data_mut()
.as_idx_mut(&package_id)
.or_not_found(&package_id)?
.as_tasks_mut()
.remove(&replay_id)?
{
if !force && task.as_task().as_severity().de()? == TaskSeverity::Critical {
return Err(Error::new(
eyre!("Cannot clear critical task"),
ErrorKind::InvalidRequest,
));
}
}
Ok(())
})
.await
.result
} }

View File

@@ -3,27 +3,27 @@ use std::collections::BTreeMap;
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use clap::Parser; use clap::Parser;
use color_eyre::eyre::eyre; use color_eyre::eyre::eyre;
use imbl_value::{json, InternedString}; use imbl_value::{InternedString, json};
use itertools::Itertools; use itertools::Itertools;
use josekit::jwk::Jwk; use josekit::jwk::Jwk;
use rpc_toolkit::yajrc::RpcError; use rpc_toolkit::yajrc::RpcError;
use rpc_toolkit::{from_fn_async, Context, HandlerArgs, HandlerExt, ParentHandler}; use rpc_toolkit::{CallRemote, Context, HandlerArgs, HandlerExt, ParentHandler, from_fn_async};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use tokio::io::AsyncWriteExt;
use tracing::instrument; use tracing::instrument;
use ts_rs::TS; use ts_rs::TS;
use crate::context::{CliContext, RpcContext}; use crate::context::{CliContext, RpcContext};
use crate::db::model::DatabaseModel; use crate::middleware::auth::session::{
use crate::middleware::auth::{ AsLogoutSessionId, HasLoggedOutSessions, HashSessionToken, LoginRes, SessionAuthContext,
AsLogoutSessionId, HasLoggedOutSessions, HashSessionToken, LoginRes,
}; };
use crate::prelude::*; use crate::prelude::*;
use crate::util::crypto::EncryptedWire; use crate::util::crypto::EncryptedWire;
use crate::util::serde::{display_serializable, HandlerExtSerde, WithIoFormat}; use crate::util::io::create_file_mod;
use crate::{ensure_code, Error, ResultExt}; use crate::util::serde::{HandlerExtSerde, WithIoFormat, display_serializable};
use crate::{Error, ResultExt, ensure_code};
#[derive(Debug, Clone, Default, Deserialize, Serialize, TS)] #[derive(Debug, Clone, Default, Deserialize, Serialize, TS)]
#[ts(as = "BTreeMap::<String, Session>")]
pub struct Sessions(pub BTreeMap<InternedString, Session>); pub struct Sessions(pub BTreeMap<InternedString, Session>);
impl Sessions { impl Sessions {
pub fn new() -> Self { pub fn new() -> Self {
@@ -41,6 +41,33 @@ impl Map for Sessions {
} }
} }
pub async fn write_shadow(password: &str) -> Result<(), Error> {
let hash: String = sha_crypt::sha512_simple(password, &sha_crypt::Sha512Params::default())
.map_err(|e| Error::new(eyre!("{e:?}"), ErrorKind::Serialization))?;
let shadow_contents = tokio::fs::read_to_string("/etc/shadow").await?;
let mut shadow_file =
create_file_mod("/media/startos/config/overlay/etc/shadow", 0o640).await?;
for line in shadow_contents.lines() {
match line.split_once(":") {
Some((user, rest)) if user == "start9" || user == "kiosk" => {
let (_, rest) = rest.split_once(":").ok_or_else(|| {
Error::new(eyre!("malformed /etc/shadow"), ErrorKind::ParseSysInfo)
})?;
shadow_file
.write_all(format!("{user}:{hash}:{rest}\n").as_bytes())
.await?;
}
_ => {
shadow_file.write_all(line.as_bytes()).await?;
shadow_file.write_all(b"\n").await?;
}
}
}
shadow_file.sync_all().await?;
tokio::fs::copy("/media/startos/config/overlay/etc/shadow", "/etc/shadow").await?;
Ok(())
}
#[derive(Clone, Serialize, Deserialize, TS)] #[derive(Clone, Serialize, Deserialize, TS)]
#[serde(untagged)] #[serde(untagged)]
#[ts(export)] #[ts(export)]
@@ -83,31 +110,34 @@ impl std::str::FromStr for PasswordType {
}) })
} }
} }
pub fn auth<C: Context>() -> ParentHandler<C> { pub fn auth<C: Context, AC: SessionAuthContext>() -> ParentHandler<C>
where
CliContext: CallRemote<AC>,
{
ParentHandler::new() ParentHandler::new()
.subcommand( .subcommand(
"login", "login",
from_fn_async(login_impl) from_fn_async(login_impl::<AC>)
.with_metadata("login", Value::Bool(true)) .with_metadata("login", Value::Bool(true))
.no_cli(), .no_cli(),
) )
.subcommand( .subcommand(
"login", "login",
from_fn_async(cli_login) from_fn_async(cli_login::<AC>)
.no_display() .no_display()
.with_about("Log in to StartOS server"), .with_about("Log in a new auth session"),
) )
.subcommand( .subcommand(
"logout", "logout",
from_fn_async(logout) from_fn_async(logout::<AC>)
.with_metadata("get_session", Value::Bool(true)) .with_metadata("get_session", Value::Bool(true))
.no_display() .no_display()
.with_about("Log out of StartOS server") .with_about("Log out of current auth session")
.with_call_remote::<CliContext>(), .with_call_remote::<CliContext>(),
) )
.subcommand( .subcommand(
"session", "session",
session::<C>().with_about("List or kill StartOS sessions"), session::<C, AC>().with_about("List or kill auth sessions"),
) )
.subcommand( .subcommand(
"reset-password", "reset-password",
@@ -117,7 +147,7 @@ pub fn auth<C: Context>() -> ParentHandler<C> {
"reset-password", "reset-password",
from_fn_async(cli_reset_password) from_fn_async(cli_reset_password)
.no_display() .no_display()
.with_about("Reset StartOS password"), .with_about("Reset password"),
) )
.subcommand( .subcommand(
"get-pubkey", "get-pubkey",
@@ -143,17 +173,20 @@ fn gen_pwd() {
} }
#[instrument(skip_all)] #[instrument(skip_all)]
async fn cli_login( async fn cli_login<C: SessionAuthContext>(
HandlerArgs { HandlerArgs {
context: ctx, context: ctx,
parent_method, parent_method,
method, method,
.. ..
}: HandlerArgs<CliContext>, }: HandlerArgs<CliContext>,
) -> Result<(), RpcError> { ) -> Result<(), RpcError>
where
CliContext: CallRemote<C>,
{
let password = rpassword::prompt_password("Password: ")?; let password = rpassword::prompt_password("Password: ")?;
ctx.call_remote::<RpcContext>( ctx.call_remote::<C>(
&parent_method.into_iter().chain(method).join("."), &parent_method.into_iter().chain(method).join("."),
json!({ json!({
"password": password, "password": password,
@@ -181,66 +214,52 @@ pub fn check_password(hash: &str, password: &str) -> Result<(), Error> {
Ok(()) Ok(())
} }
pub fn check_password_against_db(db: &DatabaseModel, password: &str) -> Result<(), Error> {
let pw_hash = db.as_private().as_password().de()?;
check_password(&pw_hash, password)?;
Ok(())
}
#[derive(Deserialize, Serialize, TS)] #[derive(Deserialize, Serialize, TS)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
#[ts(export)] #[ts(export)]
pub struct LoginParams { pub struct LoginParams {
password: Option<PasswordType>, password: String,
#[ts(skip)] #[ts(skip)]
#[serde(rename = "__auth_userAgent")] // from Auth middleware #[serde(rename = "__Auth_userAgent")] // from Auth middleware
user_agent: Option<String>, user_agent: Option<String>,
#[serde(default)] #[serde(default)]
ephemeral: bool, ephemeral: bool,
#[serde(default)]
#[ts(type = "any")]
metadata: Value,
} }
#[instrument(skip_all)] #[instrument(skip_all)]
pub async fn login_impl( pub async fn login_impl<C: SessionAuthContext>(
ctx: RpcContext, ctx: C,
LoginParams { LoginParams {
password, password,
user_agent, user_agent,
ephemeral, ephemeral,
metadata,
}: LoginParams, }: LoginParams,
) -> Result<LoginRes, Error> { ) -> Result<LoginRes, Error> {
let password = password.unwrap_or_default().decrypt(&ctx)?; let tok = if ephemeral {
C::check_password(&ctx.db().peek().await, &password)?;
if ephemeral {
check_password_against_db(&ctx.db.peek().await, &password)?;
let hash_token = HashSessionToken::new(); let hash_token = HashSessionToken::new();
ctx.ephemeral_sessions.mutate(|s| { ctx.ephemeral_sessions().mutate(|s| {
s.0.insert( s.0.insert(
hash_token.hashed().clone(), hash_token.hashed().clone(),
Session { Session {
logged_in: Utc::now(), logged_in: Utc::now(),
last_active: Utc::now(), last_active: Utc::now(),
user_agent, user_agent,
metadata,
}, },
) )
}); });
Ok(hash_token.to_login_res()) Ok(hash_token.to_login_res())
} else { } else {
ctx.db ctx.db()
.mutate(|db| { .mutate(|db| {
check_password_against_db(db, &password)?; C::check_password(db, &password)?;
let hash_token = HashSessionToken::new(); let hash_token = HashSessionToken::new();
db.as_private_mut().as_sessions_mut().insert( C::access_sessions(db).insert(
hash_token.hashed(), hash_token.hashed(),
&Session { &Session {
logged_in: Utc::now(), logged_in: Utc::now(),
last_active: Utc::now(), last_active: Utc::now(),
user_agent, user_agent,
metadata,
}, },
)?; )?;
@@ -248,7 +267,11 @@ pub async fn login_impl(
}) })
.await .await
.result .result
} }?;
ctx.post_login_hook(&password).await?;
Ok(tok)
} }
#[derive(Deserialize, Serialize, Parser, TS)] #[derive(Deserialize, Serialize, Parser, TS)]
@@ -256,12 +279,12 @@ pub async fn login_impl(
#[command(rename_all = "kebab-case")] #[command(rename_all = "kebab-case")]
pub struct LogoutParams { pub struct LogoutParams {
#[ts(skip)] #[ts(skip)]
#[serde(rename = "__auth_session")] // from Auth middleware #[serde(rename = "__Auth_session")] // from Auth middleware
session: InternedString, session: InternedString,
} }
pub async fn logout( pub async fn logout<C: SessionAuthContext>(
ctx: RpcContext, ctx: C,
LogoutParams { session }: LogoutParams, LogoutParams { session }: LogoutParams,
) -> Result<Option<HasLoggedOutSessions>, Error> { ) -> Result<Option<HasLoggedOutSessions>, Error> {
Ok(Some( Ok(Some(
@@ -277,10 +300,7 @@ pub struct Session {
pub logged_in: DateTime<Utc>, pub logged_in: DateTime<Utc>,
#[ts(type = "string")] #[ts(type = "string")]
pub last_active: DateTime<Utc>, pub last_active: DateTime<Utc>,
#[ts(skip)]
pub user_agent: Option<String>, pub user_agent: Option<String>,
#[ts(type = "any")]
pub metadata: Value,
} }
#[derive(Deserialize, Serialize, TS)] #[derive(Deserialize, Serialize, TS)]
@@ -292,29 +312,30 @@ pub struct SessionList {
sessions: Sessions, sessions: Sessions,
} }
pub fn session<C: Context>() -> ParentHandler<C> { pub fn session<C: Context, AC: SessionAuthContext>() -> ParentHandler<C>
where
CliContext: CallRemote<AC>,
{
ParentHandler::new() ParentHandler::new()
.subcommand( .subcommand(
"list", "list",
from_fn_async(list) from_fn_async(list::<AC>)
.with_metadata("get_session", Value::Bool(true)) .with_metadata("get_session", Value::Bool(true))
.with_display_serializable() .with_display_serializable()
.with_custom_display_fn(|handle, result| { .with_custom_display_fn(|handle, result| display_sessions(handle.params, result))
Ok(display_sessions(handle.params, result)) .with_about("Display all auth sessions")
})
.with_about("Display all server sessions")
.with_call_remote::<CliContext>(), .with_call_remote::<CliContext>(),
) )
.subcommand( .subcommand(
"kill", "kill",
from_fn_async(kill) from_fn_async(kill::<AC>)
.no_display() .no_display()
.with_about("Terminate existing server session(s)") .with_about("Terminate existing auth session(s)")
.with_call_remote::<CliContext>(), .with_call_remote::<CliContext>(),
) )
} }
fn display_sessions(params: WithIoFormat<ListParams>, arg: SessionList) { fn display_sessions(params: WithIoFormat<ListParams>, arg: SessionList) -> Result<(), Error> {
use prettytable::*; use prettytable::*;
if let Some(format) = params.format { if let Some(format) = params.format {
@@ -327,7 +348,6 @@ fn display_sessions(params: WithIoFormat<ListParams>, arg: SessionList) {
"LOGGED IN", "LOGGED IN",
"LAST ACTIVE", "LAST ACTIVE",
"USER AGENT", "USER AGENT",
"METADATA",
]); ]);
for (id, session) in arg.sessions.0 { for (id, session) in arg.sessions.0 {
let mut row = row![ let mut row = row![
@@ -335,7 +355,6 @@ fn display_sessions(params: WithIoFormat<ListParams>, arg: SessionList) {
&format!("{}", session.logged_in), &format!("{}", session.logged_in),
&format!("{}", session.last_active), &format!("{}", session.last_active),
session.user_agent.as_deref().unwrap_or("N/A"), session.user_agent.as_deref().unwrap_or("N/A"),
&format!("{}", session.metadata),
]; ];
if Some(id) == arg.current { if Some(id) == arg.current {
row.iter_mut() row.iter_mut()
@@ -344,7 +363,8 @@ fn display_sessions(params: WithIoFormat<ListParams>, arg: SessionList) {
} }
table.add_row(row); table.add_row(row);
} }
table.print_tty(false).unwrap(); table.print_tty(false)?;
Ok(())
} }
#[derive(Deserialize, Serialize, Parser, TS)] #[derive(Deserialize, Serialize, Parser, TS)]
@@ -353,18 +373,18 @@ fn display_sessions(params: WithIoFormat<ListParams>, arg: SessionList) {
pub struct ListParams { pub struct ListParams {
#[arg(skip)] #[arg(skip)]
#[ts(skip)] #[ts(skip)]
#[serde(rename = "__auth_session")] // from Auth middleware #[serde(rename = "__Auth_session")] // from Auth middleware
session: Option<InternedString>, session: Option<InternedString>,
} }
// #[command(display(display_sessions))] // #[command(display(display_sessions))]
#[instrument(skip_all)] #[instrument(skip_all)]
pub async fn list( pub async fn list<C: SessionAuthContext>(
ctx: RpcContext, ctx: C,
ListParams { session, .. }: ListParams, ListParams { session, .. }: ListParams,
) -> Result<SessionList, Error> { ) -> Result<SessionList, Error> {
let mut sessions = ctx.db.peek().await.into_private().into_sessions().de()?; let mut sessions = C::access_sessions(&mut ctx.db().peek().await).de()?;
ctx.ephemeral_sessions.peek(|s| { ctx.ephemeral_sessions().peek(|s| {
sessions sessions
.0 .0
.extend(s.0.iter().map(|(k, v)| (k.clone(), v.clone()))) .extend(s.0.iter().map(|(k, v)| (k.clone(), v.clone())))
@@ -398,7 +418,10 @@ pub struct KillParams {
} }
#[instrument(skip_all)] #[instrument(skip_all)]
pub async fn kill(ctx: RpcContext, KillParams { ids }: KillParams) -> Result<(), Error> { pub async fn kill<C: SessionAuthContext>(
ctx: C,
KillParams { ids }: KillParams,
) -> Result<(), Error> {
HasLoggedOutSessions::new(ids.into_iter().map(KillSessionId::new), &ctx).await?; HasLoggedOutSessions::new(ids.into_iter().map(KillSessionId::new), &ctx).await?;
Ok(()) Ok(())
} }
@@ -454,30 +477,19 @@ pub async fn reset_password_impl(
let old_password = old_password.unwrap_or_default().decrypt(&ctx)?; let old_password = old_password.unwrap_or_default().decrypt(&ctx)?;
let new_password = new_password.unwrap_or_default().decrypt(&ctx)?; let new_password = new_password.unwrap_or_default().decrypt(&ctx)?;
let mut account = ctx.account.write().await; let account = ctx.account.mutate(|account| {
if !argon2::verify_encoded(&account.password, old_password.as_bytes()) if !argon2::verify_encoded(&account.password, old_password.as_bytes())
.with_kind(crate::ErrorKind::IncorrectPassword)? .with_kind(crate::ErrorKind::IncorrectPassword)?
{ {
return Err(Error::new( return Err(Error::new(
eyre!("Incorrect Password"), eyre!("Incorrect Password"),
crate::ErrorKind::IncorrectPassword, crate::ErrorKind::IncorrectPassword,
)); ));
} }
account.set_password(&new_password)?; account.set_password(&new_password)?;
let account_password = &account.password; Ok(account.clone())
let account = account.clone(); })?;
ctx.db ctx.db.mutate(|d| account.save(d)).await.result
.mutate(|d| {
d.as_public_mut()
.as_server_info_mut()
.as_password_hash_mut()
.ser(account_password)?;
account.save(d)?;
Ok(())
})
.await
.result
} }
#[instrument(skip_all)] #[instrument(skip_all)]

View File

@@ -5,17 +5,15 @@ use std::sync::Arc;
use chrono::Utc; use chrono::Utc;
use clap::Parser; use clap::Parser;
use color_eyre::eyre::eyre; use color_eyre::eyre::eyre;
use helpers::AtomicFile;
use imbl::OrdSet; use imbl::OrdSet;
use models::PackageId;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use tokio::io::AsyncWriteExt; use tokio::io::AsyncWriteExt;
use tracing::instrument; use tracing::instrument;
use ts_rs::TS; use ts_rs::TS;
use super::target::{BackupTargetId, PackageBackupInfo};
use super::PackageBackupReport; use super::PackageBackupReport;
use crate::auth::check_password_against_db; use super::target::{BackupTargetId, PackageBackupInfo};
use crate::PackageId;
use crate::backup::os::OsBackup; use crate::backup::os::OsBackup;
use crate::backup::{BackupReport, ServerBackupReport}; use crate::backup::{BackupReport, ServerBackupReport};
use crate::context::RpcContext; use crate::context::RpcContext;
@@ -24,9 +22,10 @@ use crate::db::model::{Database, DatabaseModel};
use crate::disk::mount::backup::BackupMountGuard; use crate::disk::mount::backup::BackupMountGuard;
use crate::disk::mount::filesystem::ReadWrite; use crate::disk::mount::filesystem::ReadWrite;
use crate::disk::mount::guard::{GenericMountGuard, TmpMountGuard}; use crate::disk::mount::guard::{GenericMountGuard, TmpMountGuard};
use crate::notifications::{notify, NotificationLevel}; use crate::middleware::auth::session::SessionAuthContext;
use crate::notifications::{NotificationLevel, notify};
use crate::prelude::*; use crate::prelude::*;
use crate::util::io::dir_copy; use crate::util::io::{AtomicFile, dir_copy};
use crate::util::serde::IoFormat; use crate::util::serde::IoFormat;
use crate::version::VersionT; use crate::version::VersionT;
@@ -170,7 +169,7 @@ pub async fn backup_all(
let ((fs, package_ids, server_id), status_guard) = ( let ((fs, package_ids, server_id), status_guard) = (
ctx.db ctx.db
.mutate(|db| { .mutate(|db| {
check_password_against_db(db, &password)?; RpcContext::check_password(db, &password)?;
let fs = target_id.load(db)?; let fs = target_id.load(db)?;
let package_ids = if let Some(ids) = package_ids { let package_ids = if let Some(ids) = package_ids {
ids.into_iter().collect() ids.into_iter().collect()
@@ -223,18 +222,7 @@ fn assure_backing_up<'a>(
.as_server_info_mut() .as_server_info_mut()
.as_status_info_mut() .as_status_info_mut()
.as_backup_progress_mut(); .as_backup_progress_mut();
if backing_up if backing_up.transpose_ref().is_some() {
.clone()
.de()?
.iter()
.flat_map(|x| x.values())
.fold(false, |acc, x| {
if !x.complete {
return true;
}
acc
})
{
return Err(Error::new( return Err(Error::new(
eyre!("Server is already backing up!"), eyre!("Server is already backing up!"),
ErrorKind::InvalidRequest, ErrorKind::InvalidRequest,
@@ -287,6 +275,22 @@ async fn perform_backup(
timestamp: Utc::now(), timestamp: Utc::now(),
}, },
); );
ctx.db
.mutate(|db| {
if let Some(progress) = db
.as_public_mut()
.as_server_info_mut()
.as_status_info_mut()
.as_backup_progress_mut()
.transpose_mut()
{
progress.insert(&id, &BackupProgress { complete: true })?;
}
Ok(())
})
.await
.result?;
} }
backup_report.insert( backup_report.insert(
id.clone(), id.clone(),
@@ -307,19 +311,14 @@ async fn perform_backup(
let ui = ctx.db.peek().await.into_public().into_ui().de()?; let ui = ctx.db.peek().await.into_public().into_ui().de()?;
let mut os_backup_file = let mut os_backup_file =
AtomicFile::new(backup_guard.path().join("os-backup.json"), None::<PathBuf>) AtomicFile::new(backup_guard.path().join("os-backup.json"), None::<PathBuf>).await?;
.await
.with_kind(ErrorKind::Filesystem)?;
os_backup_file os_backup_file
.write_all(&IoFormat::Json.to_vec(&OsBackup { .write_all(&IoFormat::Json.to_vec(&OsBackup {
account: ctx.account.read().await.clone(), account: ctx.account.peek(|a| a.clone()),
ui, ui,
})?) })?)
.await?; .await?;
os_backup_file os_backup_file.save().await?;
.save()
.await
.with_kind(ErrorKind::Filesystem)?;
let luks_folder_old = backup_guard.path().join("luks.old"); let luks_folder_old = backup_guard.path().join("luks.old");
if tokio::fs::metadata(&luks_folder_old).await.is_ok() { if tokio::fs::metadata(&luks_folder_old).await.is_ok() {
@@ -337,7 +336,7 @@ async fn perform_backup(
let timestamp = Utc::now(); let timestamp = Utc::now();
backup_guard.unencrypted_metadata.version = crate::version::Current::default().semver().into(); backup_guard.unencrypted_metadata.version = crate::version::Current::default().semver().into();
backup_guard.unencrypted_metadata.hostname = ctx.account.read().await.hostname.clone(); backup_guard.unencrypted_metadata.hostname = ctx.account.peek(|a| a.hostname.clone());
backup_guard.unencrypted_metadata.timestamp = timestamp.clone(); backup_guard.unencrypted_metadata.timestamp = timestamp.clone();
backup_guard.metadata.version = crate::version::Current::default().semver().into(); backup_guard.metadata.version = crate::version::Current::default().semver().into();
backup_guard.metadata.timestamp = Some(timestamp); backup_guard.metadata.timestamp = Some(timestamp);

View File

@@ -1,15 +1,12 @@
use std::collections::BTreeMap; use std::collections::BTreeMap;
use chrono::{DateTime, Utc}; use rpc_toolkit::{Context, HandlerExt, ParentHandler, from_fn_async};
use models::{HostId, PackageId};
use reqwest::Url;
use rpc_toolkit::{from_fn_async, Context, HandlerExt, ParentHandler};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::PackageId;
use crate::context::CliContext; use crate::context::CliContext;
#[allow(unused_imports)] #[allow(unused_imports)]
use crate::prelude::*; use crate::prelude::*;
use crate::util::serde::{Base32, Base64};
pub mod backup_bulk; pub mod backup_bulk;
pub mod os; pub mod os;
@@ -58,13 +55,3 @@ pub fn package_backup<C: Context>() -> ParentHandler<C> {
.with_call_remote::<CliContext>(), .with_call_remote::<CliContext>(),
) )
} }
#[derive(Deserialize, Serialize)]
struct BackupMetadata {
pub timestamp: DateTime<Utc>,
#[serde(default)]
pub network_keys: BTreeMap<HostId, Base64<[u8; 32]>>,
#[serde(default)]
pub tor_keys: BTreeMap<HostId, Base32<[u8; 64]>>, // DEPRECATED
pub registry: Option<Url>,
}

View File

@@ -4,10 +4,10 @@ use openssl::x509::X509;
use patch_db::Value; use patch_db::Value;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use ssh_key::private::Ed25519Keypair; use ssh_key::private::Ed25519Keypair;
use torut::onion::TorSecretKeyV3;
use crate::account::AccountInfo; use crate::account::AccountInfo;
use crate::hostname::{generate_hostname, generate_id, Hostname}; use crate::hostname::{Hostname, generate_hostname, generate_id};
use crate::net::tor::TorSecretKey;
use crate::prelude::*; use crate::prelude::*;
use crate::util::crypto::ed25519_expand_key; use crate::util::crypto::ed25519_expand_key;
use crate::util::serde::{Base32, Base64, Pem}; use crate::util::serde::{Base32, Base64, Pem};
@@ -36,7 +36,7 @@ impl<'de> Deserialize<'de> for OsBackup {
v => { v => {
return Err(serde::de::Error::custom(&format!( return Err(serde::de::Error::custom(&format!(
"Unknown backup version {v}" "Unknown backup version {v}"
))) )));
} }
}) })
} }
@@ -85,8 +85,11 @@ impl OsBackupV0 {
&mut ssh_key::rand_core::OsRng::default(), &mut ssh_key::rand_core::OsRng::default(),
ssh_key::Algorithm::Ed25519, ssh_key::Algorithm::Ed25519,
)?, )?,
tor_keys: vec![TorSecretKeyV3::from(self.tor_key.0)], tor_keys: TorSecretKey::from_bytes(self.tor_key.0)
compat_s9pk_key: ed25519_dalek::SigningKey::generate( .ok()
.into_iter()
.collect(),
developer_key: ed25519_dalek::SigningKey::generate(
&mut ssh_key::rand_core::OsRng::default(), &mut ssh_key::rand_core::OsRng::default(),
), ),
}, },
@@ -116,8 +119,11 @@ impl OsBackupV1 {
root_ca_key: self.root_ca_key.0, root_ca_key: self.root_ca_key.0,
root_ca_cert: self.root_ca_cert.0, root_ca_cert: self.root_ca_cert.0,
ssh_key: ssh_key::PrivateKey::from(Ed25519Keypair::from_seed(&self.net_key.0)), ssh_key: ssh_key::PrivateKey::from(Ed25519Keypair::from_seed(&self.net_key.0)),
tor_keys: vec![TorSecretKeyV3::from(ed25519_expand_key(&self.net_key.0))], tor_keys: TorSecretKey::from_bytes(ed25519_expand_key(&self.net_key.0))
compat_s9pk_key: ed25519_dalek::SigningKey::from_bytes(&self.net_key), .ok()
.into_iter()
.collect(),
developer_key: ed25519_dalek::SigningKey::from_bytes(&self.net_key),
}, },
ui: self.ui, ui: self.ui,
} }
@@ -134,7 +140,7 @@ struct OsBackupV2 {
root_ca_key: Pem<PKey<Private>>, // PEM Encoded OpenSSL Key root_ca_key: Pem<PKey<Private>>, // PEM Encoded OpenSSL Key
root_ca_cert: Pem<X509>, // PEM Encoded OpenSSL X509 Certificate root_ca_cert: Pem<X509>, // PEM Encoded OpenSSL X509 Certificate
ssh_key: Pem<ssh_key::PrivateKey>, // PEM Encoded OpenSSH Key ssh_key: Pem<ssh_key::PrivateKey>, // PEM Encoded OpenSSH Key
tor_keys: Vec<TorSecretKeyV3>, // Base64 Encoded Ed25519 Expanded Secret Key tor_keys: Vec<TorSecretKey>, // Base64 Encoded Ed25519 Expanded Secret Key
compat_s9pk_key: Pem<ed25519_dalek::SigningKey>, // PEM Encoded ED25519 Key compat_s9pk_key: Pem<ed25519_dalek::SigningKey>, // PEM Encoded ED25519 Key
ui: Value, // JSON Value ui: Value, // JSON Value
} }
@@ -149,7 +155,7 @@ impl OsBackupV2 {
root_ca_cert: self.root_ca_cert.0, root_ca_cert: self.root_ca_cert.0,
ssh_key: self.ssh_key.0, ssh_key: self.ssh_key.0,
tor_keys: self.tor_keys, tor_keys: self.tor_keys,
compat_s9pk_key: self.compat_s9pk_key.0, developer_key: self.compat_s9pk_key.0,
}, },
ui: self.ui, ui: self.ui,
} }
@@ -162,7 +168,7 @@ impl OsBackupV2 {
root_ca_cert: Pem(backup.account.root_ca_cert.clone()), root_ca_cert: Pem(backup.account.root_ca_cert.clone()),
ssh_key: Pem(backup.account.ssh_key.clone()), ssh_key: Pem(backup.account.ssh_key.clone()),
tor_keys: backup.account.tor_keys.clone(), tor_keys: backup.account.tor_keys.clone(),
compat_s9pk_key: Pem(backup.account.compat_s9pk_key.clone()), compat_s9pk_key: Pem(backup.account.developer_key.clone()),
ui: backup.ui.clone(), ui: backup.ui.clone(),
} }
} }

View File

@@ -2,8 +2,7 @@ use std::collections::BTreeMap;
use std::sync::Arc; use std::sync::Arc;
use clap::Parser; use clap::Parser;
use futures::{stream, StreamExt}; use futures::{StreamExt, stream};
use models::PackageId;
use patch_db::json_ptr::ROOT; use patch_db::json_ptr::ROOT;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use tokio::sync::Mutex; use tokio::sync::Mutex;
@@ -20,10 +19,13 @@ use crate::disk::mount::filesystem::ReadWrite;
use crate::disk::mount::guard::{GenericMountGuard, TmpMountGuard}; use crate::disk::mount::guard::{GenericMountGuard, TmpMountGuard};
use crate::init::init; use crate::init::init;
use crate::prelude::*; use crate::prelude::*;
use crate::progress::ProgressUnits;
use crate::s9pk::S9pk; use crate::s9pk::S9pk;
use crate::service::service_map::DownloadInstallFuture; use crate::service::service_map::DownloadInstallFuture;
use crate::setup::SetupExecuteProgress; use crate::setup::SetupExecuteProgress;
use crate::system::sync_kiosk;
use crate::util::serde::IoFormat; use crate::util::serde::IoFormat;
use crate::{PLATFORM, PackageId};
#[derive(Deserialize, Serialize, Parser, TS)] #[derive(Deserialize, Serialize, Parser, TS)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
@@ -80,6 +82,7 @@ pub async fn recover_full_embassy(
recovery_source: TmpMountGuard, recovery_source: TmpMountGuard,
server_id: &str, server_id: &str,
recovery_password: &str, recovery_password: &str,
kiosk: Option<bool>,
SetupExecuteProgress { SetupExecuteProgress {
init_phases, init_phases,
restore_phase, restore_phase,
@@ -105,8 +108,12 @@ pub async fn recover_full_embassy(
) )
.with_kind(ErrorKind::PasswordHashGeneration)?; .with_kind(ErrorKind::PasswordHashGeneration)?;
let kiosk = Some(kiosk.unwrap_or(true)).filter(|_| &*PLATFORM != "raspberrypi");
sync_kiosk(kiosk).await?;
let db = ctx.db().await?; let db = ctx.db().await?;
db.put(&ROOT, &Database::init(&os_backup.account)?).await?; db.put(&ROOT, &Database::init(&os_backup.account, kiosk)?)
.await?;
drop(db); drop(db);
let init_result = init(&ctx.webserver, &ctx.config, init_phases).await?; let init_result = init(&ctx.webserver, &ctx.config, init_phases).await?;
@@ -129,6 +136,7 @@ pub async fn recover_full_embassy(
.collect(); .collect();
let tasks = restore_packages(&rpc_ctx, backup_guard, ids).await?; let tasks = restore_packages(&rpc_ctx, backup_guard, ids).await?;
restore_phase.set_total(tasks.len() as u64); restore_phase.set_total(tasks.len() as u64);
restore_phase.set_units(Some(ProgressUnits::Steps));
let restore_phase = Arc::new(Mutex::new(restore_phase)); let restore_phase = Arc::new(Mutex::new(restore_phase));
stream::iter(tasks) stream::iter(tasks)
.for_each_concurrent(5, |(id, res)| { .for_each_concurrent(5, |(id, res)| {
@@ -166,6 +174,7 @@ async fn restore_packages(
.install( .install(
ctx.clone(), ctx.clone(),
|| S9pk::open(s9pk_path, Some(&id)), || S9pk::open(s9pk_path, Some(&id)),
None, // TODO: pull from metadata?
Some(backup_dir), Some(backup_dir),
None, None,
) )

View File

@@ -4,17 +4,17 @@ use std::path::{Path, PathBuf};
use clap::Parser; use clap::Parser;
use color_eyre::eyre::eyre; use color_eyre::eyre::eyre;
use imbl_value::InternedString; use imbl_value::InternedString;
use rpc_toolkit::{from_fn_async, Context, HandlerExt, ParentHandler}; use rpc_toolkit::{Context, HandlerExt, ParentHandler, from_fn_async};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use ts_rs::TS; use ts_rs::TS;
use super::{BackupTarget, BackupTargetId}; use super::{BackupTarget, BackupTargetId};
use crate::context::{CliContext, RpcContext}; use crate::context::{CliContext, RpcContext};
use crate::db::model::DatabaseModel; use crate::db::model::DatabaseModel;
use crate::disk::mount::filesystem::cifs::Cifs;
use crate::disk::mount::filesystem::ReadOnly; use crate::disk::mount::filesystem::ReadOnly;
use crate::disk::mount::filesystem::cifs::Cifs;
use crate::disk::mount::guard::{GenericMountGuard, TmpMountGuard}; use crate::disk::mount::guard::{GenericMountGuard, TmpMountGuard};
use crate::disk::util::{recovery_info, StartOsRecoveryInfo}; use crate::disk::util::{StartOsRecoveryInfo, recovery_info};
use crate::prelude::*; use crate::prelude::*;
use crate::util::serde::KeyVal; use crate::util::serde::KeyVal;

View File

@@ -2,15 +2,14 @@ use std::collections::BTreeMap;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use clap::builder::ValueParserFactory;
use clap::Parser; use clap::Parser;
use clap::builder::ValueParserFactory;
use color_eyre::eyre::eyre; use color_eyre::eyre::eyre;
use digest::generic_array::GenericArray;
use digest::OutputSizeUser; use digest::OutputSizeUser;
use digest::generic_array::GenericArray;
use exver::Version; use exver::Version;
use imbl_value::InternedString; use imbl_value::InternedString;
use models::{FromStrParser, PackageId}; use rpc_toolkit::{Context, HandlerExt, ParentHandler, from_fn_async};
use rpc_toolkit::{from_fn_async, Context, HandlerExt, ParentHandler};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use sha2::Sha256; use sha2::Sha256;
use tokio::sync::Mutex; use tokio::sync::Mutex;
@@ -18,6 +17,7 @@ use tracing::instrument;
use ts_rs::TS; use ts_rs::TS;
use self::cifs::CifsBackupTarget; use self::cifs::CifsBackupTarget;
use crate::PackageId;
use crate::context::{CliContext, RpcContext}; use crate::context::{CliContext, RpcContext};
use crate::db::model::DatabaseModel; use crate::db::model::DatabaseModel;
use crate::disk::mount::backup::BackupMountGuard; use crate::disk::mount::backup::BackupMountGuard;
@@ -28,9 +28,9 @@ use crate::disk::mount::guard::{GenericMountGuard, TmpMountGuard};
use crate::disk::util::PartitionInfo; use crate::disk::util::PartitionInfo;
use crate::prelude::*; use crate::prelude::*;
use crate::util::serde::{ use crate::util::serde::{
deserialize_from_str, display_serializable, serialize_display, HandlerExtSerde, WithIoFormat, HandlerExtSerde, WithIoFormat, deserialize_from_str, display_serializable, serialize_display,
}; };
use crate::util::VersionString; use crate::util::{FromStrParser, VersionString};
pub mod cifs; pub mod cifs;
@@ -157,7 +157,7 @@ pub fn target<C: Context>() -> ParentHandler<C> {
from_fn_async(info) from_fn_async(info)
.with_display_serializable() .with_display_serializable()
.with_custom_display_fn::<CliContext, _>(|params, info| { .with_custom_display_fn::<CliContext, _>(|params, info| {
Ok(display_backup_info(params.params, info)) display_backup_info(params.params, info)
}) })
.with_about("Display package backup information") .with_about("Display package backup information")
.with_call_remote::<CliContext>(), .with_call_remote::<CliContext>(),
@@ -227,7 +227,7 @@ pub struct PackageBackupInfo {
pub timestamp: DateTime<Utc>, pub timestamp: DateTime<Utc>,
} }
fn display_backup_info(params: WithIoFormat<InfoParams>, info: BackupInfo) { fn display_backup_info(params: WithIoFormat<InfoParams>, info: BackupInfo) -> Result<(), Error> {
use prettytable::*; use prettytable::*;
if let Some(format) = params.format { if let Some(format) = params.format {
@@ -260,7 +260,8 @@ fn display_backup_info(params: WithIoFormat<InfoParams>, info: BackupInfo) {
]; ];
table.add_row(row); table.add_row(row);
} }
table.print_tty(false).unwrap(); table.print_tty(false)?;
Ok(())
} }
#[derive(Deserialize, Serialize, Parser, TS)] #[derive(Deserialize, Serialize, Parser, TS)]
@@ -296,17 +297,20 @@ pub async fn info(
} }
lazy_static::lazy_static! { lazy_static::lazy_static! {
static ref USER_MOUNTS: Mutex<BTreeMap<BackupTargetId, BackupMountGuard<TmpMountGuard>>> = static ref USER_MOUNTS: Mutex<BTreeMap<BackupTargetId, Result<BackupMountGuard<TmpMountGuard>, TmpMountGuard>>> =
Mutex::new(BTreeMap::new()); Mutex::new(BTreeMap::new());
} }
#[derive(Deserialize, Serialize, Parser, TS)] #[derive(Deserialize, Serialize, Parser)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
#[command(rename_all = "kebab-case")] #[command(rename_all = "kebab-case")]
pub struct MountParams { pub struct MountParams {
target_id: BackupTargetId, target_id: BackupTargetId,
server_id: String, #[arg(long)]
password: String, server_id: Option<String>,
password: String, // TODO: rpassword
#[arg(long)]
allow_partial: bool,
} }
#[instrument(skip_all)] #[instrument(skip_all)]
@@ -316,24 +320,63 @@ pub async fn mount(
target_id, target_id,
server_id, server_id,
password, password,
allow_partial,
}: MountParams, }: MountParams,
) -> Result<String, Error> { ) -> Result<String, Error> {
let server_id = if let Some(server_id) = server_id {
server_id
} else {
ctx.db
.peek()
.await
.into_public()
.into_server_info()
.into_id()
.de()?
};
let mut mounts = USER_MOUNTS.lock().await; let mut mounts = USER_MOUNTS.lock().await;
if let Some(existing) = mounts.get(&target_id) { let existing = mounts.get(&target_id);
return Ok(existing.path().display().to_string());
}
let guard = BackupMountGuard::mount( let base = match existing {
TmpMountGuard::mount(&target_id.clone().load(&ctx.db.peek().await)?, ReadWrite).await?, Some(Ok(a)) => return Ok(a.path().display().to_string()),
&server_id, Some(Err(e)) => e.clone(),
&password, None => {
) TmpMountGuard::mount(&target_id.clone().load(&ctx.db.peek().await)?, ReadWrite).await?
.await?; }
};
let guard = match BackupMountGuard::mount(base.clone(), &server_id, &password).await {
Ok(a) => a,
Err(e) => {
if allow_partial {
mounts.insert(target_id, Err(base.clone()));
let enc_key = BackupMountGuard::<TmpMountGuard>::load_metadata(
base.path(),
&server_id,
&password,
)
.await
.map(|(_, k)| k);
return Err(e)
.with_ctx(|e| (
e.kind,
format!(
"\nThe base filesystem did successfully mount at {:?}\nWrapped Key: {:?}",
base.path(),
enc_key
)
));
} else {
return Err(e);
}
}
};
let res = guard.path().display().to_string(); let res = guard.path().display().to_string();
mounts.insert(target_id, guard); mounts.insert(target_id, Ok(guard));
Ok(res) Ok(res)
} }
@@ -350,11 +393,17 @@ pub async fn umount(_: RpcContext, UmountParams { target_id }: UmountParams) ->
let mut mounts = USER_MOUNTS.lock().await; // TODO: move to context let mut mounts = USER_MOUNTS.lock().await; // TODO: move to context
if let Some(target_id) = target_id { if let Some(target_id) = target_id {
if let Some(existing) = mounts.remove(&target_id) { if let Some(existing) = mounts.remove(&target_id) {
existing.unmount().await?; match existing {
Ok(e) => e.unmount().await?,
Err(e) => e.unmount().await?,
}
} }
} else { } else {
for (_, existing) in std::mem::take(&mut *mounts) { for (_, existing) in std::mem::take(&mut *mounts) {
existing.unmount().await?; match existing {
Ok(e) => e.unmount().await?,
Err(e) => e.unmount().await?,
}
} }
} }

View File

@@ -1,68 +1,85 @@
use std::collections::VecDeque; use std::collections::{BTreeMap, VecDeque};
use std::ffi::OsString; use std::ffi::OsString;
use std::path::Path; use std::path::Path;
#[cfg(feature = "container-runtime")]
pub mod container_cli; pub mod container_cli;
pub mod deprecated; pub mod deprecated;
#[cfg(feature = "registry")]
pub mod registry; pub mod registry;
#[cfg(feature = "cli")]
pub mod start_cli; pub mod start_cli;
#[cfg(feature = "daemon")]
pub mod start_init; pub mod start_init;
#[cfg(feature = "daemon")]
pub mod startd; pub mod startd;
pub mod tunnel;
fn select_executable(name: &str) -> Option<fn(VecDeque<OsString>)> { #[derive(Default)]
match name { pub struct MultiExecutable(BTreeMap<&'static str, fn(VecDeque<OsString>)>);
#[cfg(feature = "cli")] impl MultiExecutable {
"start-cli" => Some(start_cli::main), pub fn enable_startd(&mut self) -> &mut Self {
#[cfg(feature = "container-runtime")] self.0.insert("startd", startd::main);
"start-cli" => Some(container_cli::main), self.0
#[cfg(feature = "daemon")] .insert("embassyd", |_| deprecated::renamed("embassyd", "startd"));
"startd" => Some(startd::main), self.0
#[cfg(feature = "registry")] .insert("embassy-init", |_| deprecated::removed("embassy-init"));
"registry" => Some(registry::main), self
"embassy-cli" => Some(|_| deprecated::renamed("embassy-cli", "start-cli")), }
"embassy-sdk" => Some(|_| deprecated::renamed("embassy-sdk", "start-sdk")), pub fn enable_start_cli(&mut self) -> &mut Self {
"embassyd" => Some(|_| deprecated::renamed("embassyd", "startd")), self.0.insert("start-cli", start_cli::main);
"embassy-init" => Some(|_| deprecated::removed("embassy-init")), self.0.insert("embassy-cli", |_| {
"contents" => Some(|_| { deprecated::renamed("embassy-cli", "start-cli")
#[cfg(feature = "cli")] });
println!("start-cli"); self.0
#[cfg(feature = "container-runtime")] .insert("embassy-sdk", |_| deprecated::removed("embassy-sdk"));
println!("start-cli (container)"); self
#[cfg(feature = "daemon")] }
println!("startd"); pub fn enable_start_container(&mut self) -> &mut Self {
#[cfg(feature = "registry")] self.0.insert("start-container", container_cli::main);
println!("registry"); self
}), }
_ => None, pub fn enable_start_registryd(&mut self) -> &mut Self {
self.0.insert("start-registryd", registry::main);
self
}
pub fn enable_start_registry(&mut self) -> &mut Self {
self.0.insert("start-registry", registry::cli);
self
}
pub fn enable_start_tunneld(&mut self) -> &mut Self {
self.0.insert("start-tunneld", tunnel::main);
self
}
pub fn enable_start_tunnel(&mut self) -> &mut Self {
self.0.insert("start-tunnel", tunnel::cli);
self
} }
}
pub fn startbox() { fn select_executable(&self, name: &str) -> Option<fn(VecDeque<OsString>)> {
let mut args = std::env::args_os().collect::<VecDeque<_>>(); self.0.get(&name).copied()
for _ in 0..2 { }
if let Some(s) = args.pop_front() {
if let Some(x) = Path::new(&*s) pub fn execute(&self) {
.file_name() let mut args = std::env::args_os().collect::<VecDeque<_>>();
.and_then(|s| s.to_str()) for _ in 0..2 {
.and_then(|s| select_executable(&s)) if let Some(s) = args.pop_front() {
{ if let Some(name) = Path::new(&*s).file_name().and_then(|s| s.to_str()) {
args.push_front(s); if name == "--contents" {
return x(args); for name in self.0.keys() {
println!("{name}");
}
}
if let Some(x) = self.select_executable(&name) {
args.push_front(s);
return x(args);
}
}
} }
} }
let args = std::env::args().collect::<VecDeque<_>>();
eprintln!(
"unknown executable: {}",
args.get(1)
.or_else(|| args.get(0))
.map(|s| s.as_str())
.unwrap_or("N/A")
);
std::process::exit(1);
} }
let args = std::env::args().collect::<VecDeque<_>>();
eprintln!(
"unknown executable: {}",
args.get(1)
.or_else(|| args.get(0))
.map(|s| s.as_str())
.unwrap_or("N/A")
);
std::process::exit(1);
} }

View File

@@ -2,20 +2,26 @@ use std::ffi::OsString;
use clap::Parser; use clap::Parser;
use futures::FutureExt; use futures::FutureExt;
use rpc_toolkit::CliApp;
use tokio::signal::unix::signal; use tokio::signal::unix::signal;
use tracing::instrument; use tracing::instrument;
use crate::context::CliContext;
use crate::context::config::ClientConfig;
use crate::net::web_server::{Acceptor, WebServer}; use crate::net::web_server::{Acceptor, WebServer};
use crate::prelude::*; use crate::prelude::*;
use crate::registry::context::{RegistryConfig, RegistryContext}; use crate::registry::context::{RegistryConfig, RegistryContext};
use crate::registry::registry_router;
use crate::util::logger::LOGGER; use crate::util::logger::LOGGER;
#[instrument(skip_all)] #[instrument(skip_all)]
async fn inner_main(config: &RegistryConfig) -> Result<(), Error> { async fn inner_main(config: &RegistryConfig) -> Result<(), Error> {
let server = async { let server = async {
let ctx = RegistryContext::init(config).await?; let ctx = RegistryContext::init(config).await?;
let mut server = WebServer::new(Acceptor::bind([ctx.listen]).await?); let server = WebServer::new(
server.serve_registry(ctx.clone()); Acceptor::bind([ctx.listen]).await?,
registry_router(ctx.clone()),
);
let mut shutdown_recv = ctx.shutdown.subscribe(); let mut shutdown_recv = ctx.shutdown.subscribe();
@@ -85,3 +91,30 @@ pub fn main(args: impl IntoIterator<Item = OsString>) {
} }
} }
} }
pub fn cli(args: impl IntoIterator<Item = OsString>) {
LOGGER.enable();
if let Err(e) = CliApp::new(
|cfg: ClientConfig| Ok(CliContext::init(cfg.load()?)?),
crate::registry::registry_api(),
)
.run(args)
{
match e.data {
Some(serde_json::Value::String(s)) => eprintln!("{}: {}", e.message, s),
Some(serde_json::Value::Object(o)) => {
if let Some(serde_json::Value::String(s)) = o.get("details") {
eprintln!("{}: {}", e.message, s);
if let Some(serde_json::Value::String(s)) = o.get("debug") {
tracing::debug!("{}", s)
}
}
}
Some(a) => eprintln!("{}: {}", e.message, a),
None => eprintln!("{}", e.message),
}
std::process::exit(e.code);
}
}

View File

@@ -3,8 +3,8 @@ use std::ffi::OsString;
use rpc_toolkit::CliApp; use rpc_toolkit::CliApp;
use serde_json::Value; use serde_json::Value;
use crate::context::config::ClientConfig;
use crate::context::CliContext; use crate::context::CliContext;
use crate::context::config::ClientConfig;
use crate::util::logger::LOGGER; use crate::util::logger::LOGGER;
use crate::version::{Current, VersionT}; use crate::version::{Current, VersionT};
@@ -17,7 +17,7 @@ pub fn main(args: impl IntoIterator<Item = OsString>) {
if let Err(e) = CliApp::new( if let Err(e) = CliApp::new(
|cfg: ClientConfig| Ok(CliContext::init(cfg.load()?)?), |cfg: ClientConfig| Ok(CliContext::init(cfg.load()?)?),
crate::expanded_api(), crate::main_api(),
) )
.run(args) .run(args)
{ {

View File

@@ -1,4 +1,3 @@
use std::path::Path;
use std::sync::Arc; use std::sync::Arc;
use tokio::process::Command; use tokio::process::Command;
@@ -7,12 +6,13 @@ use tracing::instrument;
use crate::context::config::ServerConfig; use crate::context::config::ServerConfig;
use crate::context::rpc::InitRpcContextPhases; use crate::context::rpc::InitRpcContextPhases;
use crate::context::{DiagnosticContext, InitContext, InstallContext, RpcContext, SetupContext}; use crate::context::{DiagnosticContext, InitContext, InstallContext, RpcContext, SetupContext};
use crate::disk::REPAIR_DISK_PATH;
use crate::disk::fsck::RepairStrategy; use crate::disk::fsck::RepairStrategy;
use crate::disk::main::DEFAULT_PASSWORD; use crate::disk::main::DEFAULT_PASSWORD;
use crate::disk::REPAIR_DISK_PATH;
use crate::firmware::{check_for_firmware_update, update_firmware}; use crate::firmware::{check_for_firmware_update, update_firmware};
use crate::init::{InitPhases, STANDBY_MODE_PATH}; use crate::init::{InitPhases, STANDBY_MODE_PATH};
use crate::net::web_server::{UpgradableListener, WebServer}; use crate::net::gateway::UpgradableListener;
use crate::net::web_server::WebServer;
use crate::prelude::*; use crate::prelude::*;
use crate::progress::FullProgressTracker; use crate::progress::FullProgressTracker;
use crate::shutdown::Shutdown; use crate::shutdown::Shutdown;
@@ -38,7 +38,7 @@ async fn setup_or_init(
let mut update_phase = handle.add_phase("Updating Firmware".into(), Some(10)); let mut update_phase = handle.add_phase("Updating Firmware".into(), Some(10));
let mut reboot_phase = handle.add_phase("Rebooting".into(), Some(1)); let mut reboot_phase = handle.add_phase("Rebooting".into(), Some(1));
server.serve_init(init_ctx); server.serve_ui_for(init_ctx);
update_phase.start(); update_phase.start();
if let Err(e) = update_firmware(firmware).await { if let Err(e) = update_firmware(firmware).await {
@@ -48,7 +48,7 @@ async fn setup_or_init(
update_phase.complete(); update_phase.complete();
reboot_phase.start(); reboot_phase.start();
return Ok(Err(Shutdown { return Ok(Err(Shutdown {
export_args: None, disk_guid: None,
restart: true, restart: true,
})); }));
} }
@@ -94,7 +94,7 @@ async fn setup_or_init(
let ctx = InstallContext::init().await?; let ctx = InstallContext::init().await?;
server.serve_install(ctx.clone()); server.serve_ui_for(ctx.clone());
ctx.shutdown ctx.shutdown
.subscribe() .subscribe()
@@ -103,7 +103,7 @@ async fn setup_or_init(
.expect("context dropped"); .expect("context dropped");
return Ok(Err(Shutdown { return Ok(Err(Shutdown {
export_args: None, disk_guid: None,
restart: true, restart: true,
})); }));
} }
@@ -114,10 +114,12 @@ async fn setup_or_init(
{ {
let ctx = SetupContext::init(server, config)?; let ctx = SetupContext::init(server, config)?;
server.serve_setup(ctx.clone()); server.serve_ui_for(ctx.clone());
let mut shutdown = ctx.shutdown.subscribe(); let mut shutdown = ctx.shutdown.subscribe();
shutdown.recv().await.expect("context dropped"); if let Some(shutdown) = shutdown.recv().await.expect("context dropped") {
return Ok(Err(shutdown));
}
tokio::task::yield_now().await; tokio::task::yield_now().await;
if let Err(e) = Command::new("killall") if let Err(e) = Command::new("killall")
@@ -136,7 +138,7 @@ async fn setup_or_init(
return Err(Error::new( return Err(Error::new(
eyre!("Setup mode exited before setup completed"), eyre!("Setup mode exited before setup completed"),
ErrorKind::Unknown, ErrorKind::Unknown,
)) ));
} }
})) }))
} else { } else {
@@ -148,7 +150,7 @@ async fn setup_or_init(
let init_phases = InitPhases::new(&handle); let init_phases = InitPhases::new(&handle);
let rpc_ctx_phases = InitRpcContextPhases::new(&handle); let rpc_ctx_phases = InitRpcContextPhases::new(&handle);
server.serve_init(init_ctx); server.serve_ui_for(init_ctx);
async { async {
disk_phase.start(); disk_phase.start();
@@ -183,7 +185,7 @@ async fn setup_or_init(
let mut reboot_phase = handle.add_phase("Rebooting".into(), Some(1)); let mut reboot_phase = handle.add_phase("Rebooting".into(), Some(1));
reboot_phase.start(); reboot_phase.start();
return Ok(Err(Shutdown { return Ok(Err(Shutdown {
export_args: Some((disk_guid, Path::new(DATA_DIR).to_owned())), disk_guid: Some(disk_guid),
restart: true, restart: true,
})); }));
} }
@@ -246,7 +248,7 @@ pub async fn main(
e, e,
)?; )?;
server.serve_diagnostic(ctx.clone()); server.serve_ui_for(ctx.clone());
let shutdown = ctx.shutdown.subscribe().recv().await.unwrap(); let shutdown = ctx.shutdown.subscribe().recv().await.unwrap();

View File

@@ -1,6 +1,5 @@
use std::cmp::max; use std::cmp::max;
use std::ffi::OsString; use std::ffi::OsString;
use std::net::IpAddr;
use std::sync::Arc; use std::sync::Arc;
use std::time::Duration; use std::time::Duration;
@@ -13,9 +12,9 @@ use tracing::instrument;
use crate::context::config::ServerConfig; use crate::context::config::ServerConfig;
use crate::context::rpc::InitRpcContextPhases; use crate::context::rpc::InitRpcContextPhases;
use crate::context::{DiagnosticContext, InitContext, RpcContext}; use crate::context::{DiagnosticContext, InitContext, RpcContext};
use crate::net::network_interface::SelfContainedNetworkInterfaceListener; use crate::net::gateway::{BindTcp, SelfContainedNetworkInterfaceListener, UpgradableListener};
use crate::net::utils::ipv6_is_local; use crate::net::static_server::refresher;
use crate::net::web_server::{Acceptor, UpgradableListener, WebServer}; use crate::net::web_server::{Acceptor, WebServer};
use crate::shutdown::Shutdown; use crate::shutdown::Shutdown;
use crate::system::launch_metrics_task; use crate::system::launch_metrics_task;
use crate::util::io::append_file; use crate::util::io::append_file;
@@ -40,7 +39,7 @@ async fn inner_main(
}; };
tokio::fs::write("/run/startos/initialized", "").await?; tokio::fs::write("/run/startos/initialized", "").await?;
server.serve_main(ctx.clone()); server.serve_ui_for(ctx.clone());
LOGGER.set_logfile(None); LOGGER.set_logfile(None);
handle.complete(); handle.complete();
@@ -49,7 +48,7 @@ async fn inner_main(
let init_ctx = InitContext::init(config).await?; let init_ctx = InitContext::init(config).await?;
let handle = init_ctx.progress.clone(); let handle = init_ctx.progress.clone();
let rpc_ctx_phases = InitRpcContextPhases::new(&handle); let rpc_ctx_phases = InitRpcContextPhases::new(&handle);
server.serve_init(init_ctx); server.serve_ui_for(init_ctx);
let ctx = RpcContext::init( let ctx = RpcContext::init(
&server.acceptor_setter(), &server.acceptor_setter(),
@@ -65,14 +64,14 @@ async fn inner_main(
) )
.await?; .await?;
server.serve_main(ctx.clone()); server.serve_ui_for(ctx.clone());
handle.complete(); handle.complete();
ctx ctx
}; };
let (rpc_ctx, shutdown) = async { let (rpc_ctx, shutdown) = async {
crate::hostname::sync_hostname(&rpc_ctx.account.read().await.hostname).await?; crate::hostname::sync_hostname(&rpc_ctx.account.peek(|a| a.hostname.clone())).await?;
let mut shutdown_recv = rpc_ctx.shutdown.subscribe(); let mut shutdown_recv = rpc_ctx.shutdown.subscribe();
@@ -134,8 +133,6 @@ async fn inner_main(
.await?; .await?;
rpc_ctx.shutdown().await?; rpc_ctx.shutdown().await?;
tracing::info!("RPC Context is dropped");
Ok(shutdown) Ok(shutdown)
} }
@@ -146,14 +143,15 @@ pub fn main(args: impl IntoIterator<Item = OsString>) {
let res = { let res = {
let rt = tokio::runtime::Builder::new_multi_thread() let rt = tokio::runtime::Builder::new_multi_thread()
.worker_threads(max(4, num_cpus::get())) .worker_threads(max(1, num_cpus::get()))
.enable_all() .enable_all()
.build() .build()
.expect("failed to initialize runtime"); .expect("failed to initialize runtime");
let res = rt.block_on(async { let res = rt.block_on(async {
let mut server = WebServer::new(Acceptor::bind_upgradable( let mut server = WebServer::new(
SelfContainedNetworkInterfaceListener::bind(80), Acceptor::bind_upgradable(SelfContainedNetworkInterfaceListener::bind(BindTcp, 80)),
)); refresher(),
);
match inner_main(&mut server, &config).await { match inner_main(&mut server, &config).await {
Ok(a) => { Ok(a) => {
server.shutdown().await; server.shutdown().await;
@@ -181,7 +179,7 @@ pub fn main(args: impl IntoIterator<Item = OsString>) {
e, e,
)?; )?;
server.serve_diagnostic(ctx.clone()); server.serve_ui_for(ctx.clone());
let mut shutdown = ctx.shutdown.subscribe(); let mut shutdown = ctx.shutdown.subscribe();

View File

@@ -0,0 +1,200 @@
use std::ffi::OsString;
use std::net::SocketAddr;
use std::sync::Arc;
use std::time::Duration;
use clap::Parser;
use futures::FutureExt;
use rpc_toolkit::CliApp;
use tokio::signal::unix::signal;
use tracing::instrument;
use visit_rs::Visit;
use crate::context::CliContext;
use crate::context::config::ClientConfig;
use crate::net::gateway::{Bind, BindTcp};
use crate::net::tls::TlsListener;
use crate::net::web_server::{Accept, Acceptor, MetadataVisitor, WebServer};
use crate::prelude::*;
use crate::tunnel::context::{TunnelConfig, TunnelContext};
use crate::tunnel::tunnel_router;
use crate::tunnel::web::TunnelCertHandler;
use crate::util::future::NonDetachingJoinHandle;
use crate::util::logger::LOGGER;
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
enum WebserverListener {
Http,
Https(SocketAddr),
}
impl<V: MetadataVisitor> Visit<V> for WebserverListener {
fn visit(&self, visitor: &mut V) -> <V as visit_rs::Visitor>::Result {
visitor.visit(self)
}
}
#[instrument(skip_all)]
async fn inner_main(config: &TunnelConfig) -> Result<(), Error> {
let server = async {
let ctx = TunnelContext::init(config).await?;
let listen = ctx.listen;
let server = WebServer::new(
Acceptor::bind_map_dyn([(WebserverListener::Http, listen)]).await?,
tunnel_router(ctx.clone()),
);
let acceptor_setter = server.acceptor_setter();
let https_db = ctx.db.clone();
let https_thread: NonDetachingJoinHandle<()> = tokio::spawn(async move {
let mut sub = https_db.subscribe("/webserver".parse().unwrap()).await;
while {
while let Err(e) = async {
let webserver = https_db.peek().await.into_webserver();
if webserver.as_enabled().de()? {
let addr = webserver.as_listen().de()?.or_not_found("listen address")?;
acceptor_setter.send_if_modified(|a| {
let key = WebserverListener::Https(addr);
if !a.contains_key(&key) {
match (|| {
Ok::<_, Error>(TlsListener::new(
BindTcp.bind(addr)?,
TunnelCertHandler {
db: https_db.clone(),
crypto_provider: Arc::new(tokio_rustls::rustls::crypto::ring::default_provider()),
},
))
})() {
Ok(l) => {
a.retain(|k, _| *k == WebserverListener::Http);
a.insert(key, l.into_dyn());
true
}
Err(e) => {
tracing::error!("error adding ssl listener: {e}");
tracing::debug!("{e:?}");
false
}
}
} else {
false
}
});
} else {
acceptor_setter.send_if_modified(|a| {
let before = a.len();
a.retain(|k, _| *k == WebserverListener::Http);
a.len() != before
});
}
Ok::<_, Error>(())
}
.await
{
tracing::error!("error updating webserver bind: {e}");
tracing::debug!("{e:?}");
tokio::time::sleep(Duration::from_secs(5)).await;
}
sub.recv().await.is_some()
} {}
})
.into();
let mut shutdown_recv = ctx.shutdown.subscribe();
let sig_handler_ctx = ctx;
let sig_handler: NonDetachingJoinHandle<()> = tokio::spawn(async move {
use tokio::signal::unix::SignalKind;
futures::future::select_all(
[
SignalKind::interrupt(),
SignalKind::quit(),
SignalKind::terminate(),
]
.iter()
.map(|s| {
async move {
signal(*s)
.unwrap_or_else(|_| panic!("register {:?} handler", s))
.recv()
.await
}
.boxed()
}),
)
.await;
sig_handler_ctx
.shutdown
.send(())
.map_err(|_| ())
.expect("send shutdown signal");
})
.into();
shutdown_recv
.recv()
.await
.with_kind(crate::ErrorKind::Unknown)?;
sig_handler.wait_for_abort().await.with_kind(ErrorKind::Unknown)?;
https_thread.wait_for_abort().await.with_kind(ErrorKind::Unknown)?;
Ok::<_, Error>(server)
}
.await?;
server.shutdown().await;
Ok(())
}
pub fn main(args: impl IntoIterator<Item = OsString>) {
LOGGER.enable();
let config = TunnelConfig::parse_from(args).load().unwrap();
let res = {
let rt = tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()
.expect("failed to initialize runtime");
rt.block_on(inner_main(&config))
};
match res {
Ok(()) => (),
Err(e) => {
eprintln!("{}", e.source);
tracing::debug!("{:?}", e.source);
drop(e.source);
std::process::exit(e.kind as i32)
}
}
}
pub fn cli(args: impl IntoIterator<Item = OsString>) {
LOGGER.enable();
if let Err(e) = CliApp::new(
|cfg: ClientConfig| Ok(CliContext::init(cfg.load()?)?),
crate::tunnel::api::tunnel_api(),
)
.run(args)
{
match e.data {
Some(serde_json::Value::String(s)) => eprintln!("{}: {}", e.message, s),
Some(serde_json::Value::Object(o)) => {
if let Some(serde_json::Value::String(s)) = o.get("details") {
eprintln!("{}: {}", e.message, s);
if let Some(serde_json::Value::String(s)) = o.get("debug") {
tracing::debug!("{}", s)
}
}
}
Some(a) => eprintln!("{}: {}", e.message, a),
None => eprintln!("{}", e.message),
}
std::process::exit(e.code);
}
}

View File

@@ -0,0 +1,53 @@
use helpers::Callback;
use itertools::Itertools;
use jsonpath_lib::Compiled;
use crate::PackageId;
use serde_json::Value;
use crate::context::RpcContext;
pub struct ConfigHook {
pub path: Compiled,
pub prev: Vec<Value>,
pub callback: Callback,
}
impl RpcContext {
pub async fn add_config_hook(&self, id: PackageId, hook: ConfigHook) {
let mut hooks = self.config_hooks.lock().await;
let prev = hooks.remove(&id).unwrap_or_default();
hooks.insert(
id,
prev.into_iter()
.filter(|h| h.callback.is_listening())
.chain(std::iter::once(hook))
.collect(),
);
}
pub async fn call_config_hooks(&self, id: PackageId, config: &Value) {
let mut hooks = self.config_hooks.lock().await;
let mut prev = hooks.remove(&id).unwrap_or_default();
for hook in &mut prev {
let new = hook
.path
.select(config)
.unwrap_or_default()
.into_iter()
.cloned()
.collect_vec();
if new != hook.prev {
hook.callback
.call(vec![Value::Array(new.clone())])
.unwrap_or_default();
hook.prev = new;
}
}
hooks.insert(
id,
prev.into_iter()
.filter(|h| h.callback.is_listening())
.collect(),
);
}
}

View File

@@ -1,27 +1,33 @@
use std::fs::File; use std::fs::File;
use std::io::BufReader; use std::io::BufReader;
use std::net::SocketAddr;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::sync::Arc; use std::sync::Arc;
use cookie_store::{CookieStore, RawCookie}; use cookie::{Cookie, Expiration, SameSite};
use cookie_store::CookieStore;
use http::HeaderMap;
use imbl_value::InternedString;
use josekit::jwk::Jwk; use josekit::jwk::Jwk;
use once_cell::sync::OnceCell; use once_cell::sync::OnceCell;
use reqwest::Proxy; use reqwest::Proxy;
use reqwest_cookie_store::CookieStoreMutex; use reqwest_cookie_store::CookieStoreMutex;
use rpc_toolkit::reqwest::{Client, Url}; use rpc_toolkit::reqwest::{Client, Url};
use rpc_toolkit::yajrc::RpcError; use rpc_toolkit::yajrc::RpcError;
use rpc_toolkit::{call_remote_http, CallRemote, Context, Empty}; use rpc_toolkit::{CallRemote, Context, Empty};
use tokio::net::TcpStream; use tokio::net::TcpStream;
use tokio::runtime::Runtime; use tokio::runtime::Runtime;
use tokio_tungstenite::{MaybeTlsStream, WebSocketStream}; use tokio_tungstenite::{MaybeTlsStream, WebSocketStream};
use tracing::instrument; use tracing::instrument;
use super::setup::CURRENT_SECRET; use super::setup::CURRENT_SECRET;
use crate::context::config::{local_config_path, ClientConfig}; use crate::context::config::{ClientConfig, local_config_path};
use crate::context::{DiagnosticContext, InitContext, InstallContext, RpcContext, SetupContext}; use crate::context::{DiagnosticContext, InitContext, InstallContext, RpcContext, SetupContext};
use crate::middleware::auth::LOCAL_AUTH_COOKIE_PATH; use crate::developer::{OS_DEVELOPER_KEY_PATH, default_developer_key_path};
use crate::middleware::auth::local::LocalAuthContext;
use crate::prelude::*; use crate::prelude::*;
use crate::rpc_continuations::Guid; use crate::rpc_continuations::Guid;
use crate::util::io::read_file_to_string;
#[derive(Debug)] #[derive(Debug)]
pub struct CliContextSeed { pub struct CliContextSeed {
@@ -29,6 +35,10 @@ pub struct CliContextSeed {
pub base_url: Url, pub base_url: Url,
pub rpc_url: Url, pub rpc_url: Url,
pub registry_url: Option<Url>, pub registry_url: Option<Url>,
pub registry_hostname: Vec<InternedString>,
pub registry_listen: Option<SocketAddr>,
pub tunnel_addr: Option<SocketAddr>,
pub tunnel_listen: Option<SocketAddr>,
pub client: Client, pub client: Client,
pub cookie_store: Arc<CookieStoreMutex>, pub cookie_store: Arc<CookieStoreMutex>,
pub cookie_path: PathBuf, pub cookie_path: PathBuf,
@@ -37,6 +47,11 @@ pub struct CliContextSeed {
} }
impl Drop for CliContextSeed { impl Drop for CliContextSeed {
fn drop(&mut self) { fn drop(&mut self) {
if let Some(rt) = self.runtime.take() {
if let Ok(rt) = Arc::try_unwrap(rt) {
rt.shutdown_background();
}
}
let tmp = format!("{}.tmp", self.cookie_path.display()); let tmp = format!("{}.tmp", self.cookie_path.display());
let parent_dir = self.cookie_path.parent().unwrap_or(Path::new("/")); let parent_dir = self.cookie_path.parent().unwrap_or(Path::new("/"));
if !parent_dir.exists() { if !parent_dir.exists() {
@@ -50,9 +65,8 @@ impl Drop for CliContextSeed {
true, true,
) )
.unwrap(); .unwrap();
let mut store = self.cookie_store.lock().unwrap(); let store = self.cookie_store.lock().unwrap();
store.remove("localhost", "", "local"); cookie_store::serde::json::save(&store, &mut *writer).unwrap();
store.save_json(&mut *writer).unwrap();
writer.sync_all().unwrap(); writer.sync_all().unwrap();
std::fs::rename(tmp, &self.cookie_path).unwrap(); std::fs::rename(tmp, &self.cookie_path).unwrap();
} }
@@ -80,26 +94,14 @@ impl CliContext {
.unwrap_or(Path::new("/")) .unwrap_or(Path::new("/"))
.join(".cookies.json") .join(".cookies.json")
}); });
let cookie_store = Arc::new(CookieStoreMutex::new({ let cookie_store = Arc::new(CookieStoreMutex::new(if cookie_path.exists() {
let mut store = if cookie_path.exists() { cookie_store::serde::json::load(BufReader::new(
CookieStore::load_json(BufReader::new( File::open(&cookie_path)
File::open(&cookie_path) .with_ctx(|_| (ErrorKind::Filesystem, cookie_path.display()))?,
.with_ctx(|_| (ErrorKind::Filesystem, cookie_path.display()))?, ))
)) .unwrap_or_default()
.map_err(|e| eyre!("{}", e)) } else {
.with_kind(crate::ErrorKind::Deserialization)? CookieStore::default()
} else {
CookieStore::default()
};
if let Ok(local) = std::fs::read_to_string(LOCAL_AUTH_COOKIE_PATH) {
store
.insert_raw(
&RawCookie::new("local", local),
&"http://localhost".parse()?,
)
.with_kind(crate::ErrorKind::Network)?;
}
store
})); }));
Ok(CliContext(Arc::new(CliContextSeed { Ok(CliContext(Arc::new(CliContextSeed {
@@ -124,9 +126,17 @@ impl CliContext {
Ok::<_, Error>(registry) Ok::<_, Error>(registry)
}) })
.transpose()?, .transpose()?,
registry_hostname: config.registry_hostname.unwrap_or_default(),
registry_listen: config.registry_listen,
tunnel_addr: config.tunnel,
tunnel_listen: config.tunnel_listen,
client: { client: {
let mut builder = Client::builder().cookie_provider(cookie_store.clone()); let mut builder = Client::builder().cookie_provider(cookie_store.clone());
if let Some(proxy) = config.proxy { if let Some(proxy) = config.proxy.or_else(|| {
config
.socks_listen
.and_then(|socks| format!("socks5h://{socks}").parse::<Url>().log_err())
}) {
builder = builder =
builder.proxy(Proxy::all(proxy).with_kind(crate::ErrorKind::ParseUrl)?) builder.proxy(Proxy::all(proxy).with_kind(crate::ErrorKind::ParseUrl)?)
} }
@@ -134,14 +144,9 @@ impl CliContext {
}, },
cookie_store, cookie_store,
cookie_path, cookie_path,
developer_key_path: config.developer_key_path.unwrap_or_else(|| { developer_key_path: config
local_config_path() .developer_key_path
.as_deref() .unwrap_or_else(default_developer_key_path),
.unwrap_or_else(|| Path::new(super::config::CONFIG_PATH))
.parent()
.unwrap_or(Path::new("/"))
.join("developer.key.pem")
}),
developer_key: OnceCell::new(), developer_key: OnceCell::new(),
}))) })))
} }
@@ -150,20 +155,26 @@ impl CliContext {
#[instrument(skip_all)] #[instrument(skip_all)]
pub fn developer_key(&self) -> Result<&ed25519_dalek::SigningKey, Error> { pub fn developer_key(&self) -> Result<&ed25519_dalek::SigningKey, Error> {
self.developer_key.get_or_try_init(|| { self.developer_key.get_or_try_init(|| {
if !self.developer_key_path.exists() { for path in [Path::new(OS_DEVELOPER_KEY_PATH), &self.developer_key_path] {
return Err(Error::new(eyre!("Developer Key does not exist! Please run `start-cli init` before running this command."), crate::ErrorKind::Uninitialized)); if !path.exists() {
} continue;
let pair = <ed25519::KeypairBytes as ed25519::pkcs8::DecodePrivateKey>::from_pkcs8_pem( }
&std::fs::read_to_string(&self.developer_key_path)?, let pair = <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!("pkcs8 key is of incorrect length"),
ErrorKind::OpenSsl,
) )
})?; .with_kind(crate::ErrorKind::Pem)?;
Ok(secret.into()) let secret = ed25519_dalek::SecretKey::try_from(&pair.secret_key[..]).map_err(|_| {
Error::new(
eyre!("pkcs8 key is of incorrect length"),
ErrorKind::OpenSsl,
)
})?;
return Ok(secret.into())
}
Err(Error::new(
eyre!("Developer Key does not exist! Please run `start-cli init-key` before running this command."),
crate::ErrorKind::Uninitialized
))
}) })
} }
@@ -180,7 +191,7 @@ impl CliContext {
eyre!("Cannot parse scheme from base URL"), eyre!("Cannot parse scheme from base URL"),
crate::ErrorKind::ParseUrl, crate::ErrorKind::ParseUrl,
) )
.into()) .into());
} }
}; };
url.set_scheme(ws_scheme) url.set_scheme(ws_scheme)
@@ -223,23 +234,28 @@ impl CliContext {
&self, &self,
method: &str, method: &str,
params: Value, params: Value,
) -> Result<Value, RpcError> ) -> Result<Value, Error>
where where
Self: CallRemote<RemoteContext>, Self: CallRemote<RemoteContext>,
{ {
<Self as CallRemote<RemoteContext, Empty>>::call_remote(&self, method, params, Empty {}) <Self as CallRemote<RemoteContext, Empty>>::call_remote(&self, method, params, Empty {})
.await .await
.map_err(Error::from)
.with_ctx(|e| (e.kind, method))
} }
pub async fn call_remote_with<RemoteContext, T>( pub async fn call_remote_with<RemoteContext, T>(
&self, &self,
method: &str, method: &str,
params: Value, params: Value,
extra: T, extra: T,
) -> Result<Value, RpcError> ) -> Result<Value, Error>
where where
Self: CallRemote<RemoteContext, T>, Self: CallRemote<RemoteContext, T>,
{ {
<Self as CallRemote<RemoteContext, T>>::call_remote(&self, method, params, extra).await <Self as CallRemote<RemoteContext, T>>::call_remote(&self, method, params, extra)
.await
.map_err(Error::from)
.with_ctx(|e| (e.kind, method))
} }
} }
impl AsRef<Jwk> for CliContext { impl AsRef<Jwk> for CliContext {
@@ -269,40 +285,88 @@ impl Context for CliContext {
) )
} }
} }
impl AsRef<Client> for CliContext {
fn as_ref(&self) -> &Client {
&self.client
}
}
impl CallRemote<RpcContext> for CliContext { impl CallRemote<RpcContext> for CliContext {
async fn call_remote(&self, method: &str, params: Value, _: Empty) -> Result<Value, RpcError> { async fn call_remote(&self, method: &str, params: Value, _: Empty) -> Result<Value, RpcError> {
call_remote_http(&self.client, self.rpc_url.clone(), method, params).await if let Ok(local) = read_file_to_string(RpcContext::LOCAL_AUTH_COOKIE_PATH).await {
self.cookie_store
.lock()
.unwrap()
.insert_raw(
&Cookie::build(("local", local))
.domain("localhost")
.expires(Expiration::Session)
.same_site(SameSite::Strict)
.build(),
&"http://localhost".parse()?,
)
.with_kind(crate::ErrorKind::Network)?;
}
crate::middleware::auth::signature::call_remote(
self,
self.rpc_url.clone(),
HeaderMap::new(),
self.rpc_url.host_str(),
method,
params,
)
.await
} }
} }
impl CallRemote<DiagnosticContext> for CliContext { impl CallRemote<DiagnosticContext> for CliContext {
async fn call_remote(&self, method: &str, params: Value, _: Empty) -> Result<Value, RpcError> { async fn call_remote(&self, method: &str, params: Value, _: Empty) -> Result<Value, RpcError> {
call_remote_http(&self.client, self.rpc_url.clone(), method, params).await crate::middleware::auth::signature::call_remote(
self,
self.rpc_url.clone(),
HeaderMap::new(),
self.rpc_url.host_str(),
method,
params,
)
.await
} }
} }
impl CallRemote<InitContext> for CliContext { impl CallRemote<InitContext> for CliContext {
async fn call_remote(&self, method: &str, params: Value, _: Empty) -> Result<Value, RpcError> { async fn call_remote(&self, method: &str, params: Value, _: Empty) -> Result<Value, RpcError> {
call_remote_http(&self.client, self.rpc_url.clone(), method, params).await crate::middleware::auth::signature::call_remote(
self,
self.rpc_url.clone(),
HeaderMap::new(),
self.rpc_url.host_str(),
method,
params,
)
.await
} }
} }
impl CallRemote<SetupContext> for CliContext { impl CallRemote<SetupContext> for CliContext {
async fn call_remote(&self, method: &str, params: Value, _: Empty) -> Result<Value, RpcError> { async fn call_remote(&self, method: &str, params: Value, _: Empty) -> Result<Value, RpcError> {
call_remote_http(&self.client, self.rpc_url.clone(), method, params).await crate::middleware::auth::signature::call_remote(
self,
self.rpc_url.clone(),
HeaderMap::new(),
self.rpc_url.host_str(),
method,
params,
)
.await
} }
} }
impl CallRemote<InstallContext> for CliContext { impl CallRemote<InstallContext> for CliContext {
async fn call_remote(&self, method: &str, params: Value, _: Empty) -> Result<Value, RpcError> { async fn call_remote(&self, method: &str, params: Value, _: Empty) -> Result<Value, RpcError> {
call_remote_http(&self.client, self.rpc_url.clone(), method, params).await crate::middleware::auth::signature::call_remote(
self,
self.rpc_url.clone(),
HeaderMap::new(),
self.rpc_url.host_str(),
method,
params,
)
.await
} }
} }
#[test]
fn test() {
let ctx = CliContext::init(ClientConfig::default()).unwrap();
ctx.runtime().unwrap().block_on(async {
reqwest::Client::new()
.get("http://example.com")
.send()
.await
.unwrap();
});
}

View File

@@ -3,18 +3,16 @@ use std::net::SocketAddr;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use clap::Parser; use clap::Parser;
use imbl_value::InternedString;
use reqwest::Url; use reqwest::Url;
use serde::de::DeserializeOwned; use serde::de::DeserializeOwned;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use sqlx::postgres::PgConnectOptions;
use sqlx::PgPool;
use crate::MAIN_DATA;
use crate::disk::OsPartitionInfo; use crate::disk::OsPartitionInfo;
use crate::init::init_postgres;
use crate::prelude::*; use crate::prelude::*;
use crate::util::serde::IoFormat; use crate::util::serde::IoFormat;
use crate::version::VersionT; use crate::version::VersionT;
use crate::MAIN_DATA;
pub const DEVICE_CONFIG_PATH: &str = "/media/startos/config/config.yaml"; // "/media/startos/config/config.yaml"; pub const DEVICE_CONFIG_PATH: &str = "/media/startos/config/config.yaml"; // "/media/startos/config/config.yaml";
pub const CONFIG_PATH: &str = "/etc/startos/config.yaml"; pub const CONFIG_PATH: &str = "/etc/startos/config.yaml";
@@ -58,7 +56,6 @@ pub trait ContextConfig: DeserializeOwned + Default {
#[derive(Debug, Default, Deserialize, Serialize, Parser)] #[derive(Debug, Default, Deserialize, Serialize, Parser)]
#[serde(rename_all = "kebab-case")] #[serde(rename_all = "kebab-case")]
#[command(rename_all = "kebab-case")] #[command(rename_all = "kebab-case")]
#[command(name = "start-cli")]
#[command(version = crate::version::Current::default().semver().to_string())] #[command(version = crate::version::Current::default().semver().to_string())]
pub struct ClientConfig { pub struct ClientConfig {
#[arg(short = 'c', long)] #[arg(short = 'c', long)]
@@ -67,8 +64,18 @@ pub struct ClientConfig {
pub host: Option<Url>, pub host: Option<Url>,
#[arg(short = 'r', long)] #[arg(short = 'r', long)]
pub registry: Option<Url>, pub registry: Option<Url>,
#[arg(long)]
pub registry_hostname: Option<Vec<InternedString>>,
#[arg(skip)]
pub registry_listen: Option<SocketAddr>,
#[arg(short = 't', long)]
pub tunnel: Option<SocketAddr>,
#[arg(skip)]
pub tunnel_listen: Option<SocketAddr>,
#[arg(short = 'p', long)] #[arg(short = 'p', long)]
pub proxy: Option<Url>, pub proxy: Option<Url>,
#[arg(skip)]
pub socks_listen: Option<SocketAddr>,
#[arg(long)] #[arg(long)]
pub cookie_path: Option<PathBuf>, pub cookie_path: Option<PathBuf>,
#[arg(long)] #[arg(long)]
@@ -81,6 +88,8 @@ impl ContextConfig for ClientConfig {
fn merge_with(&mut self, other: Self) { fn merge_with(&mut self, other: Self) {
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.tunnel = self.tunnel.take().or(other.tunnel);
self.proxy = self.proxy.take().or(other.proxy); self.proxy = self.proxy.take().or(other.proxy);
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);
@@ -107,15 +116,15 @@ pub struct ServerConfig {
#[arg(skip)] #[arg(skip)]
pub os_partitions: Option<OsPartitionInfo>, pub os_partitions: Option<OsPartitionInfo>,
#[arg(long)] #[arg(long)]
pub tor_control: Option<SocketAddr>, pub socks_listen: Option<SocketAddr>,
#[arg(long)]
pub tor_socks: Option<SocketAddr>,
#[arg(long)] #[arg(long)]
pub revision_cache_size: Option<usize>, pub revision_cache_size: Option<usize>,
#[arg(long)] #[arg(long)]
pub disable_encryption: Option<bool>, pub disable_encryption: Option<bool>,
#[arg(long)] #[arg(long)]
pub multi_arch_s9pks: Option<bool>, pub multi_arch_s9pks: Option<bool>,
#[arg(long)]
pub developer_key_path: Option<PathBuf>,
} }
impl ContextConfig for ServerConfig { impl ContextConfig for ServerConfig {
fn next(&mut self) -> Option<PathBuf> { fn next(&mut self) -> Option<PathBuf> {
@@ -124,14 +133,14 @@ impl ContextConfig for ServerConfig {
fn merge_with(&mut self, other: Self) { fn merge_with(&mut self, other: Self) {
self.ethernet_interface = self.ethernet_interface.take().or(other.ethernet_interface); self.ethernet_interface = self.ethernet_interface.take().or(other.ethernet_interface);
self.os_partitions = self.os_partitions.take().or(other.os_partitions); self.os_partitions = self.os_partitions.take().or(other.os_partitions);
self.tor_control = self.tor_control.take().or(other.tor_control); self.socks_listen = self.socks_listen.take().or(other.socks_listen);
self.tor_socks = self.tor_socks.take().or(other.tor_socks);
self.revision_cache_size = self self.revision_cache_size = self
.revision_cache_size .revision_cache_size
.take() .take()
.or(other.revision_cache_size); .or(other.revision_cache_size);
self.disable_encryption = self.disable_encryption.take().or(other.disable_encryption); self.disable_encryption = self.disable_encryption.take().or(other.disable_encryption);
self.multi_arch_s9pks = self.multi_arch_s9pks.take().or(other.multi_arch_s9pks); self.multi_arch_s9pks = self.multi_arch_s9pks.take().or(other.multi_arch_s9pks);
self.developer_key_path = self.developer_key_path.take().or(other.developer_key_path);
} }
} }
@@ -151,16 +160,4 @@ impl ServerConfig {
Ok(db) Ok(db)
} }
#[instrument(skip_all)]
pub async fn secret_store(&self) -> Result<PgPool, Error> {
init_postgres("/media/startos/data").await?;
let secret_store =
PgPool::connect_with(PgConnectOptions::new().database("secrets").username("root"))
.await?;
sqlx::migrate!()
.run(&secret_store)
.await
.with_kind(crate::ErrorKind::Database)?;
Ok(secret_store)
}
} }

View File

@@ -1,15 +1,15 @@
use std::ops::Deref; use std::ops::Deref;
use std::sync::Arc; use std::sync::Arc;
use rpc_toolkit::yajrc::RpcError;
use rpc_toolkit::Context; use rpc_toolkit::Context;
use rpc_toolkit::yajrc::RpcError;
use tokio::sync::broadcast::Sender; use tokio::sync::broadcast::Sender;
use tracing::instrument; use tracing::instrument;
use crate::Error;
use crate::context::config::ServerConfig; use crate::context::config::ServerConfig;
use crate::rpc_continuations::RpcContinuations; use crate::rpc_continuations::RpcContinuations;
use crate::shutdown::Shutdown; use crate::shutdown::Shutdown;
use crate::Error;
pub struct DiagnosticContextSeed { pub struct DiagnosticContextSeed {
pub shutdown: Sender<Shutdown>, pub shutdown: Sender<Shutdown>,

Some files were not shown because too many files have changed in this diff Show More