mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-26 02:11:53 +00:00
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>
This commit is contained in:
@@ -56,7 +56,7 @@ check:
|
||||
npm run check
|
||||
|
||||
fmt: package/node_modules base/node_modules
|
||||
npx prettier . "**/*.ts" --write
|
||||
npx --prefix base prettier "**/*.ts" --write
|
||||
|
||||
package/package-lock.json: package/package.json
|
||||
cd package && npm i
|
||||
|
||||
@@ -298,6 +298,20 @@ export class Value<Type extends StaticValidatedAs, StaticValidatedAs = Type> {
|
||||
required: Required
|
||||
minLength?: number | null
|
||||
maxLength?: number | null
|
||||
/**
|
||||
* @description A list of regular expressions to which the text must conform to pass validation. A human readable description is provided in case the validation fails.
|
||||
* @default []
|
||||
* @example
|
||||
* ```
|
||||
[
|
||||
{
|
||||
regex: "[a-z]",
|
||||
description: "May only contain lower case letters from the English alphabet."
|
||||
}
|
||||
]
|
||||
* ```
|
||||
*/
|
||||
patterns?: Pattern[]
|
||||
/** Defaults to 3 */
|
||||
minRows?: number
|
||||
/** Maximum number of rows before scroll appears. Defaults to 6 */
|
||||
@@ -316,6 +330,7 @@ export class Value<Type extends StaticValidatedAs, StaticValidatedAs = Type> {
|
||||
warning: null,
|
||||
minLength: null,
|
||||
maxLength: null,
|
||||
patterns: [],
|
||||
minRows: 3,
|
||||
maxRows: 6,
|
||||
placeholder: null,
|
||||
@@ -336,6 +351,7 @@ export class Value<Type extends StaticValidatedAs, StaticValidatedAs = Type> {
|
||||
required: Required
|
||||
minLength?: number | null
|
||||
maxLength?: number | null
|
||||
patterns?: Pattern[]
|
||||
minRows?: number
|
||||
maxRows?: number
|
||||
placeholder?: string | null
|
||||
@@ -351,6 +367,7 @@ export class Value<Type extends StaticValidatedAs, StaticValidatedAs = Type> {
|
||||
warning: null,
|
||||
minLength: null,
|
||||
maxLength: null,
|
||||
patterns: [],
|
||||
minRows: 3,
|
||||
maxRows: 6,
|
||||
placeholder: null,
|
||||
|
||||
@@ -58,12 +58,14 @@ export type ValueSpecTextarea = {
|
||||
warning: string | null
|
||||
|
||||
type: "textarea"
|
||||
patterns: Pattern[]
|
||||
placeholder: string | null
|
||||
minLength: number | null
|
||||
maxLength: number | null
|
||||
minRows: number
|
||||
maxRows: number
|
||||
required: boolean
|
||||
default: string | null
|
||||
disabled: false | string
|
||||
immutable: boolean
|
||||
}
|
||||
|
||||
@@ -45,10 +45,9 @@ export interface ActionInfo<
|
||||
readonly _INPUT: Type
|
||||
}
|
||||
|
||||
export class Action<
|
||||
Id extends T.ActionId,
|
||||
Type extends Record<string, any>,
|
||||
> implements ActionInfo<Id, Type> {
|
||||
export class Action<Id extends T.ActionId, Type extends Record<string, any>>
|
||||
implements ActionInfo<Id, Type>
|
||||
{
|
||||
readonly _INPUT: Type = null as any as Type
|
||||
private prevInputSpec: Record<
|
||||
string,
|
||||
@@ -149,7 +148,8 @@ export class Action<
|
||||
|
||||
export class Actions<
|
||||
AllActions extends Record<T.ActionId, Action<T.ActionId, any>>,
|
||||
> implements InitScript {
|
||||
> implements InitScript
|
||||
{
|
||||
private constructor(private readonly actions: AllActions) {}
|
||||
static of(): Actions<{}> {
|
||||
return new Actions({})
|
||||
|
||||
@@ -137,7 +137,7 @@ export class MultiHost {
|
||||
const sslProto = this.getSslProto(options)
|
||||
const addSsl = sslProto
|
||||
? {
|
||||
// addXForwardedHeaders: null,
|
||||
addXForwardedHeaders: false,
|
||||
preferredExternalPort: knownProtocols[sslProto].defaultPort,
|
||||
scheme: sslProto,
|
||||
alpn: "alpn" in protoInfo ? protoInfo.alpn : null,
|
||||
@@ -145,7 +145,7 @@ export class MultiHost {
|
||||
}
|
||||
: options.addSsl
|
||||
? {
|
||||
// addXForwardedHeaders: null,
|
||||
addXForwardedHeaders: false,
|
||||
preferredExternalPort: 443,
|
||||
scheme: sslProto,
|
||||
alpn: null,
|
||||
|
||||
@@ -3,5 +3,6 @@ import type { AlpnInfo } from "./AlpnInfo"
|
||||
|
||||
export type AddSslOptions = {
|
||||
preferredExternalPort: number
|
||||
addXForwardedHeaders: boolean
|
||||
alpn: AlpnInfo | null
|
||||
}
|
||||
|
||||
3
sdk/base/lib/osBindings/IdMap.ts
Normal file
3
sdk/base/lib/osBindings/IdMap.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type IdMap = { fromId: number; toId: number; range: number }
|
||||
@@ -1,5 +1,5 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { FileType } from "./FileType"
|
||||
import type { IdMap } from "./IdMap"
|
||||
import type { PackageId } from "./PackageId"
|
||||
import type { VolumeId } from "./VolumeId"
|
||||
|
||||
@@ -8,5 +8,5 @@ export type MountTarget = {
|
||||
volumeId: VolumeId
|
||||
subpath: string | null
|
||||
readonly: boolean
|
||||
filetype: FileType
|
||||
idmap: Array<IdMap>
|
||||
}
|
||||
|
||||
@@ -105,6 +105,7 @@ export { HostId } from "./HostId"
|
||||
export { HostnameInfo } from "./HostnameInfo"
|
||||
export { Hosts } from "./Hosts"
|
||||
export { Host } from "./Host"
|
||||
export { IdMap } from "./IdMap"
|
||||
export { ImageConfig } from "./ImageConfig"
|
||||
export { ImageId } from "./ImageId"
|
||||
export { ImageMetadata } from "./ImageMetadata"
|
||||
|
||||
@@ -109,11 +109,9 @@ export class DropPromise<T> implements Promise<T> {
|
||||
}
|
||||
}
|
||||
|
||||
export class DropGenerator<
|
||||
T = unknown,
|
||||
TReturn = any,
|
||||
TNext = unknown,
|
||||
> implements AsyncGenerator<T, TReturn, TNext> {
|
||||
export class DropGenerator<T = unknown, TReturn = any, TNext = unknown>
|
||||
implements AsyncGenerator<T, TReturn, TNext>
|
||||
{
|
||||
private static dropFns: { [id: number]: () => void } = {}
|
||||
private static registry = new FinalizationRegistry((id: number) => {
|
||||
const drop = DropGenerator.dropFns[id]
|
||||
|
||||
@@ -19,7 +19,7 @@ export const getHostname = (url: string): Hostname | null => {
|
||||
|
||||
type FilterKinds =
|
||||
| "onion"
|
||||
| "local"
|
||||
| "mdns"
|
||||
| "domain"
|
||||
| "ip"
|
||||
| "ipv4"
|
||||
@@ -42,10 +42,10 @@ type VisibilityFilter<V extends "public" | "private"> = V extends "public"
|
||||
: never
|
||||
type KindFilter<K extends FilterKinds> = K extends "onion"
|
||||
? (HostnameInfo & { kind: "onion" }) | KindFilter<Exclude<K, "onion">>
|
||||
: K extends "local"
|
||||
: K extends "mdns"
|
||||
?
|
||||
| (HostnameInfo & { kind: "ip"; hostname: { kind: "local" } })
|
||||
| KindFilter<Exclude<K, "local">>
|
||||
| KindFilter<Exclude<K, "mdns">>
|
||||
: K extends "domain"
|
||||
?
|
||||
| (HostnameInfo & { kind: "ip"; hostname: { kind: "domain" } })
|
||||
@@ -80,11 +80,17 @@ type FilterReturnTy<F extends Filter> = F extends {
|
||||
: Exclude<HostnameInfo, FilterReturnTy<E>>
|
||||
: HostnameInfo
|
||||
|
||||
const defaultFilter = {
|
||||
const nonLocalFilter = {
|
||||
exclude: {
|
||||
kind: ["localhost", "link-local"] as ("localhost" | "link-local")[],
|
||||
},
|
||||
}
|
||||
} as const
|
||||
const publicFilter = {
|
||||
visibility: "public",
|
||||
} as const
|
||||
const onionFilter = {
|
||||
kind: "onion",
|
||||
} as const
|
||||
|
||||
type Formats = "hostname-info" | "urlstring" | "url"
|
||||
type FormatReturnTy<
|
||||
@@ -98,7 +104,7 @@ type FormatReturnTy<
|
||||
? UrlString | FormatReturnTy<F, Exclude<Format, "urlstring">>
|
||||
: never
|
||||
|
||||
export type Filled = {
|
||||
export type Filled<F extends Filter = {}> = {
|
||||
hostnames: HostnameInfo[]
|
||||
|
||||
toUrls: (h: HostnameInfo) => {
|
||||
@@ -106,30 +112,17 @@ export type Filled = {
|
||||
sslUrl: UrlString | null
|
||||
}
|
||||
|
||||
filter: <
|
||||
F extends Filter = typeof defaultFilter,
|
||||
Format extends Formats = "urlstring",
|
||||
>(
|
||||
filter?: F,
|
||||
format: <Format extends Formats = "urlstring">(
|
||||
format?: Format,
|
||||
) => FormatReturnTy<F, Format>[]
|
||||
) => FormatReturnTy<{}, Format>[]
|
||||
|
||||
publicHostnames: HostnameInfo[]
|
||||
onionHostnames: HostnameInfo[]
|
||||
localHostnames: HostnameInfo[]
|
||||
ipHostnames: HostnameInfo[]
|
||||
ipv4Hostnames: HostnameInfo[]
|
||||
ipv6Hostnames: HostnameInfo[]
|
||||
nonIpHostnames: HostnameInfo[]
|
||||
filter: <NewFilter extends Filter>(
|
||||
filter: NewFilter,
|
||||
) => Filled<NewFilter & Filter>
|
||||
|
||||
urls: UrlString[]
|
||||
publicUrls: UrlString[]
|
||||
onionUrls: UrlString[]
|
||||
localUrls: UrlString[]
|
||||
ipUrls: UrlString[]
|
||||
ipv4Urls: UrlString[]
|
||||
ipv6Urls: UrlString[]
|
||||
nonIpUrls: UrlString[]
|
||||
nonLocal: Filled<typeof nonLocalFilter & Filter>
|
||||
public: Filled<typeof publicFilter & Filter>
|
||||
onion: Filled<typeof onionFilter & Filter>
|
||||
}
|
||||
export type FilledAddressInfo = AddressInfo & Filled
|
||||
export type ServiceInterfaceFilled = {
|
||||
@@ -225,7 +218,7 @@ function filterRec(
|
||||
(h) =>
|
||||
invert !==
|
||||
((kind.has("onion") && h.kind === "onion") ||
|
||||
(kind.has("local") &&
|
||||
(kind.has("mdns") &&
|
||||
h.kind === "ip" &&
|
||||
h.hostname.kind === "local") ||
|
||||
(kind.has("domain") &&
|
||||
@@ -258,86 +251,45 @@ export const filledAddress = (
|
||||
}
|
||||
const hostnames = host.hostnameInfo[addressInfo.internalPort] ?? []
|
||||
|
||||
return {
|
||||
...addressInfo,
|
||||
hostnames,
|
||||
toUrls,
|
||||
filter: <
|
||||
F extends Filter = typeof defaultFilter,
|
||||
Format extends Formats = "urlstring",
|
||||
>(
|
||||
filter?: F,
|
||||
format?: Format,
|
||||
) => {
|
||||
const filtered = filterRec(hostnames, filter ?? defaultFilter, false)
|
||||
let res: FormatReturnTy<F, Format>[] = filtered as any
|
||||
if (format === "hostname-info") return res
|
||||
const urls = filtered.flatMap(toUrlArray)
|
||||
if (format === "url") res = urls.map((u) => new URL(u)) as any
|
||||
else res = urls as any
|
||||
return res
|
||||
},
|
||||
get publicHostnames() {
|
||||
return hostnames.filter((h) => h.kind === "onion" || h.public)
|
||||
},
|
||||
get onionHostnames() {
|
||||
return hostnames.filter((h) => h.kind === "onion")
|
||||
},
|
||||
get localHostnames() {
|
||||
return hostnames.filter(
|
||||
(h) => h.kind === "ip" && h.hostname.kind === "local",
|
||||
)
|
||||
},
|
||||
get ipHostnames() {
|
||||
return hostnames.filter(
|
||||
(h) =>
|
||||
h.kind === "ip" &&
|
||||
(h.hostname.kind === "ipv4" || h.hostname.kind === "ipv6"),
|
||||
)
|
||||
},
|
||||
get ipv4Hostnames() {
|
||||
return hostnames.filter(
|
||||
(h) => h.kind === "ip" && h.hostname.kind === "ipv4",
|
||||
)
|
||||
},
|
||||
get ipv6Hostnames() {
|
||||
return hostnames.filter(
|
||||
(h) => h.kind === "ip" && h.hostname.kind === "ipv6",
|
||||
)
|
||||
},
|
||||
get nonIpHostnames() {
|
||||
return hostnames.filter(
|
||||
(h) =>
|
||||
h.kind === "ip" &&
|
||||
h.hostname.kind !== "ipv4" &&
|
||||
h.hostname.kind !== "ipv6",
|
||||
)
|
||||
},
|
||||
get urls() {
|
||||
return this.hostnames.flatMap(toUrlArray)
|
||||
},
|
||||
get publicUrls() {
|
||||
return this.publicHostnames.flatMap(toUrlArray)
|
||||
},
|
||||
get onionUrls() {
|
||||
return this.onionHostnames.flatMap(toUrlArray)
|
||||
},
|
||||
get localUrls() {
|
||||
return this.localHostnames.flatMap(toUrlArray)
|
||||
},
|
||||
get ipUrls() {
|
||||
return this.ipHostnames.flatMap(toUrlArray)
|
||||
},
|
||||
get ipv4Urls() {
|
||||
return this.ipv4Hostnames.flatMap(toUrlArray)
|
||||
},
|
||||
get ipv6Urls() {
|
||||
return this.ipv6Hostnames.flatMap(toUrlArray)
|
||||
},
|
||||
get nonIpUrls() {
|
||||
return this.nonIpHostnames.flatMap(toUrlArray)
|
||||
},
|
||||
function filledAddressFromHostnames<F extends Filter>(
|
||||
hostnames: HostnameInfo[],
|
||||
): Filled<F> & AddressInfo {
|
||||
return {
|
||||
...addressInfo,
|
||||
hostnames,
|
||||
toUrls,
|
||||
format: <Format extends Formats = "urlstring">(format?: Format) => {
|
||||
let res: FormatReturnTy<{}, Format>[] = hostnames as any
|
||||
if (format === "hostname-info") return res
|
||||
const urls = hostnames.flatMap(toUrlArray)
|
||||
if (format === "url") res = urls.map((u) => new URL(u)) as any
|
||||
else res = urls as any
|
||||
return res
|
||||
},
|
||||
filter: <NewFilter extends Filter>(filter: NewFilter) => {
|
||||
return filledAddressFromHostnames<NewFilter & F>(
|
||||
filterRec(hostnames, filter, false),
|
||||
)
|
||||
},
|
||||
get nonLocal(): Filled<typeof nonLocalFilter & F> {
|
||||
return filledAddressFromHostnames<typeof nonLocalFilter & F>(
|
||||
filterRec(hostnames, nonLocalFilter, false),
|
||||
)
|
||||
},
|
||||
get public(): Filled<typeof publicFilter & F> {
|
||||
return filledAddressFromHostnames<typeof publicFilter & F>(
|
||||
filterRec(hostnames, publicFilter, false),
|
||||
)
|
||||
},
|
||||
get onion(): Filled<typeof onionFilter & F> {
|
||||
return filledAddressFromHostnames<typeof onionFilter & F>(
|
||||
filterRec(hostnames, onionFilter, false),
|
||||
)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return filledAddressFromHostnames<{}>(hostnames)
|
||||
}
|
||||
|
||||
const makeInterfaceFilled = async ({
|
||||
|
||||
@@ -18,6 +18,9 @@ export class ComposableRegex {
|
||||
}
|
||||
}
|
||||
|
||||
export const escapeLiteral = (str: string) =>
|
||||
str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
|
||||
|
||||
// https://ihateregex.io/expr/ipv6/
|
||||
export const ipv6 = new ComposableRegex(
|
||||
/(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))/,
|
||||
@@ -69,3 +72,13 @@ export const emailWithName = new ComposableRegex(
|
||||
export const base64 = new ComposableRegex(
|
||||
/(?:[a-zA-Z0-9+\/]{4})*(?:|(?:[a-zA-Z0-9+\/]{3}=)|(?:[a-zA-Z0-9+\/]{2}==)|(?:[a-zA-Z0-9+\/]{1}===))/,
|
||||
)
|
||||
|
||||
//https://rgxdb.com/r/1NUN74O6
|
||||
export const base64Whitespace = new ComposableRegex(
|
||||
/(?:([a-zA-Z0-9+\/]\s*){4})*(?:|(?:([a-zA-Z0-9+\/]\s*){3}=)|(?:([a-zA-Z0-9+\/]\s*){2}==)|(?:([a-zA-Z0-9+\/]\s*){1}===))/,
|
||||
)
|
||||
|
||||
export const pem = (label: string) =>
|
||||
new ComposableRegex(
|
||||
`-----BEGIN ${escapeLiteral(label)}-----\r?\n[a-zA-Z0-9+/\n\r=]*?\r?\n-----END ${escapeLiteral(label)}-----`,
|
||||
)
|
||||
|
||||
7
sdk/base/package-lock.json
generated
7
sdk/base/package-lock.json
generated
@@ -74,6 +74,7 @@
|
||||
"integrity": "sha512-i1SLeK+DzNnQ3LL/CswPCa/E5u4lh1k6IAEphON8F+cXt0t9euTshDru0q7/IqMa1PMPz5RnHuHscF8/ZJsStg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@ampproject/remapping": "^2.2.0",
|
||||
"@babel/code-frame": "^7.26.0",
|
||||
@@ -1614,6 +1615,7 @@
|
||||
"integrity": "sha512-XC70cRZVElFHfIUB40FgZOBbgJYFKKMa5nb9lxcwYstFG/Mi+/Y0bGS+rs6Dmhmkpq4pnNiLiuZAbc02YCOnmA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"undici-types": "~6.20.0"
|
||||
}
|
||||
@@ -1915,6 +1917,7 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"caniuse-lite": "^1.0.30001669",
|
||||
"electron-to-chromium": "^1.5.41",
|
||||
@@ -2984,6 +2987,7 @@
|
||||
"integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@jest/core": "^29.7.0",
|
||||
"@jest/types": "^29.6.3",
|
||||
@@ -4046,6 +4050,7 @@
|
||||
"integrity": "sha512-n7chtCbEoGYRwZZ0i/O3t1cPr6o+d9Xx4Zwy2LYfzv0vjchMBU0tO+qYYyvZloBPcgRgzYvALzGWHe609JjEpg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"commander": "^10.0.0",
|
||||
"source-map-generator": "0.8.0"
|
||||
@@ -4644,6 +4649,7 @@
|
||||
"integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@cspotcode/source-map-support": "^0.8.0",
|
||||
"@tsconfig/node10": "^1.0.7",
|
||||
@@ -4764,6 +4770,7 @@
|
||||
"integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
|
||||
@@ -163,8 +163,8 @@ export class StartSdk<Manifest extends T.SDKManifest> {
|
||||
effects.action.clearTasks({ only: replayIds }),
|
||||
},
|
||||
checkDependencies: checkDependencies as <
|
||||
DependencyId extends keyof Manifest["dependencies"] & PackageId =
|
||||
keyof Manifest["dependencies"] & PackageId,
|
||||
DependencyId extends keyof Manifest["dependencies"] &
|
||||
PackageId = keyof Manifest["dependencies"] & PackageId,
|
||||
>(
|
||||
effects: Effects,
|
||||
packageIds?: DependencyId[],
|
||||
|
||||
@@ -208,13 +208,20 @@ async function runRsync(rsyncOptions: {
|
||||
const lines = String(data).replace("\r", "\n").split("\n")
|
||||
for (const line of lines) {
|
||||
const parsed = /$([0-9.]+)%/.exec(line)?.[1]
|
||||
if (!parsed) continue
|
||||
if (!parsed) {
|
||||
console.log(parsed)
|
||||
continue
|
||||
}
|
||||
percentage = Number.parseFloat(parsed)
|
||||
}
|
||||
})
|
||||
|
||||
spawned.stderr.on("data", (data: unknown) => {
|
||||
console.error(`Backups.runAsync`, asError(data))
|
||||
let stderr = ""
|
||||
|
||||
spawned.stderr.on("data", (data: string | Buffer) => {
|
||||
const errString = data.toString("utf-8")
|
||||
stderr += errString
|
||||
console.error(`Backups.runAsync`, asError(errString))
|
||||
})
|
||||
|
||||
const id = async () => {
|
||||
@@ -229,7 +236,7 @@ async function runRsync(rsyncOptions: {
|
||||
if (code === 0) {
|
||||
resolve(null)
|
||||
} else {
|
||||
reject(new Error(`rsync exited with code ${code}`))
|
||||
reject(new Error(`rsync exited with code ${code}\n${stderr}`))
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
@@ -51,7 +51,9 @@ export class Daemon<
|
||||
)
|
||||
const res = new Daemon(subc, startCommand)
|
||||
effects.onLeaveContext(() => {
|
||||
res.stop().catch((e) => console.error(asError(e)))
|
||||
res
|
||||
.term({ destroySubcontainer: true })
|
||||
.catch((e) => console.error(asError(e)))
|
||||
})
|
||||
return res
|
||||
}
|
||||
@@ -72,7 +74,7 @@ export class Daemon<
|
||||
this.commandController = await this.startCommand()
|
||||
if (!this.shouldBeRunning) {
|
||||
// handles race condition if stopped while starting
|
||||
await this.stop()
|
||||
await this.term()
|
||||
break
|
||||
}
|
||||
const success = await this.commandController.wait().then(
|
||||
@@ -107,12 +109,7 @@ export class Daemon<
|
||||
async term(termOptions?: {
|
||||
signal?: NodeJS.Signals | undefined
|
||||
timeout?: number | undefined
|
||||
}) {
|
||||
return this.stop(termOptions)
|
||||
}
|
||||
async stop(termOptions?: {
|
||||
signal?: NodeJS.Signals | undefined
|
||||
timeout?: number | undefined
|
||||
destroySubcontainer?: boolean
|
||||
}) {
|
||||
this.shouldBeRunning = false
|
||||
this.exitedSuccess = false
|
||||
@@ -122,7 +119,9 @@ export class Daemon<
|
||||
.catch((e) => console.error(asError(e)))
|
||||
this.commandController = null
|
||||
this.onExitFns = []
|
||||
await this.subcontainer?.destroy()
|
||||
if (termOptions?.destroySubcontainer) {
|
||||
await this.subcontainer?.destroy()
|
||||
}
|
||||
}
|
||||
}
|
||||
subcontainerRc(): SubContainerRc<Manifest> | null {
|
||||
@@ -132,6 +131,6 @@ export class Daemon<
|
||||
this.onExitFns.push(fn)
|
||||
}
|
||||
onDrop(): void {
|
||||
this.stop().catch((e) => console.error(asError(e)))
|
||||
this.term().catch((e) => console.error(asError(e)))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,7 +55,7 @@ export type ExecCommandOptions = {
|
||||
runAsInit?: boolean
|
||||
env?:
|
||||
| {
|
||||
[variable: string]: string
|
||||
[variable in string]?: string
|
||||
}
|
||||
| undefined
|
||||
cwd?: string | undefined
|
||||
@@ -412,16 +412,12 @@ export class Daemons<Manifest extends T.SDKManifest, Ids extends string>
|
||||
}
|
||||
|
||||
async term() {
|
||||
try {
|
||||
for (let result of await Promise.allSettled(
|
||||
this.healthDaemons.map((x) => x.term()),
|
||||
)) {
|
||||
if (result.status === "rejected") {
|
||||
console.error(result.reason)
|
||||
}
|
||||
for (let result of await Promise.allSettled(
|
||||
this.healthDaemons.map((x) => x.term({ destroySubcontainer: true })),
|
||||
)) {
|
||||
if (result.status === "rejected") {
|
||||
console.error(result.reason)
|
||||
}
|
||||
} finally {
|
||||
this.effects.setMainStatus({ status: "stopped" })
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -54,6 +54,7 @@ export class HealthDaemon<Manifest extends SDKManifest> {
|
||||
async term(termOptions?: {
|
||||
signal?: NodeJS.Signals | undefined
|
||||
timeout?: number | undefined
|
||||
destroySubcontainer?: boolean
|
||||
}) {
|
||||
this.healthWatchers = []
|
||||
this.running = false
|
||||
@@ -87,7 +88,7 @@ export class HealthDaemon<Manifest extends SDKManifest> {
|
||||
this.started = performance.now()
|
||||
} else {
|
||||
console.debug(`Stopping ${this.id}...`)
|
||||
;(await this.daemon)?.stop()
|
||||
;(await this.daemon)?.term()
|
||||
this.turnOffHealthCheck()
|
||||
|
||||
this.setHealth({ result: "starting", message: null })
|
||||
@@ -143,7 +144,6 @@ export class HealthDaemon<Manifest extends SDKManifest> {
|
||||
const response: HealthCheckResult = await Promise.resolve(
|
||||
this.ready.fn(),
|
||||
).catch((err) => {
|
||||
console.error(asError(err))
|
||||
return {
|
||||
result: "failure",
|
||||
message: "message" in err ? err.message : String(err),
|
||||
@@ -188,6 +188,9 @@ export class HealthDaemon<Manifest extends SDKManifest> {
|
||||
performance.now() - this.started <= (this.ready.gracePeriod ?? 10_000)
|
||||
)
|
||||
result = "starting"
|
||||
if (result === "failure") {
|
||||
console.error(`Health Check ${this.id} failed:`, health.message)
|
||||
}
|
||||
await this.effects.setHealth({
|
||||
...health,
|
||||
id: this.id,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as T from "../../../base/lib/types"
|
||||
import { MountOptions } from "../util/SubContainer"
|
||||
import { IdMap, MountOptions } from "../util/SubContainer"
|
||||
|
||||
type MountArray = { mountpoint: string; options: MountOptions }[]
|
||||
|
||||
@@ -14,6 +14,23 @@ type SharedOptions = {
|
||||
* defaults to "directory"
|
||||
* */
|
||||
type?: "file" | "directory" | "infer"
|
||||
// /**
|
||||
// * Whether to map uids/gids for the mount
|
||||
// *
|
||||
// * https://www.kernel.org/doc/html/latest/filesystems/idmappings.html
|
||||
// */
|
||||
// idmap?: {
|
||||
// /** The (starting) id of the data on the filesystem (u) */
|
||||
// fromId: number
|
||||
// /** The (starting) id of the data in the mount point (k) */
|
||||
// toId: number
|
||||
// /**
|
||||
// * Optional: the number of incremental ids to map (r)
|
||||
// *
|
||||
// * defaults to 1
|
||||
// * */
|
||||
// range?: number
|
||||
// }[]
|
||||
}
|
||||
|
||||
type VolumeOpts<Manifest extends T.SDKManifest> = {
|
||||
@@ -114,6 +131,7 @@ export class Mounts<
|
||||
subpath: v.subpath,
|
||||
readonly: v.readonly,
|
||||
filetype: v.type ?? "directory",
|
||||
idmap: [],
|
||||
},
|
||||
})),
|
||||
)
|
||||
@@ -124,6 +142,7 @@ export class Mounts<
|
||||
type: "assets",
|
||||
subpath: a.subpath,
|
||||
filetype: a.type ?? "directory",
|
||||
idmap: [],
|
||||
},
|
||||
})),
|
||||
)
|
||||
@@ -137,6 +156,7 @@ export class Mounts<
|
||||
subpath: d.subpath,
|
||||
readonly: d.readonly,
|
||||
filetype: d.type ?? "directory",
|
||||
idmap: [],
|
||||
},
|
||||
})),
|
||||
)
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Daemons } from "./Daemons"
|
||||
import "../../../base/lib/interfaces/ServiceInterfaceBuilder"
|
||||
import "../../../base/lib/interfaces/Origin"
|
||||
|
||||
export const DEFAULT_SIGTERM_TIMEOUT = 30_000
|
||||
export const DEFAULT_SIGTERM_TIMEOUT = 60_000
|
||||
/**
|
||||
* Used to ensure that the main function is running with the valid proofs.
|
||||
* We first do the folowing order of things
|
||||
|
||||
@@ -53,10 +53,19 @@ async function bind(
|
||||
from: string,
|
||||
to: string,
|
||||
type: "file" | "directory" | "infer",
|
||||
idmap: IdMap[],
|
||||
) {
|
||||
await prepBind(from, to, type)
|
||||
|
||||
await execFile("mount", ["--bind", from, to])
|
||||
const args = ["--bind"]
|
||||
|
||||
if (idmap.length) {
|
||||
args.push(
|
||||
`-oX-mount.idmap=${idmap.map((i) => `b:${i.fromId}:${i.toId}:${i.range}`).join(" ")}`,
|
||||
)
|
||||
}
|
||||
|
||||
await execFile("mount", [...args, from, to])
|
||||
}
|
||||
|
||||
export interface SubContainer<
|
||||
@@ -137,9 +146,9 @@ export interface SubContainer<
|
||||
* Want to limit what we can do in a container, so we want to launch a container with a specific image and the mounts.
|
||||
*/
|
||||
export class SubContainerOwned<
|
||||
Manifest extends T.SDKManifest,
|
||||
Effects extends T.Effects = T.Effects,
|
||||
>
|
||||
Manifest extends T.SDKManifest,
|
||||
Effects extends T.Effects = T.Effects,
|
||||
>
|
||||
extends Drop
|
||||
implements SubContainer<Manifest, Effects>
|
||||
{
|
||||
@@ -306,7 +315,7 @@ export class SubContainerOwned<
|
||||
: "/"
|
||||
const from = `/media/startos/volumes/${options.volumeId}${subpath}`
|
||||
|
||||
await bind(from, path, mount.options.filetype)
|
||||
await bind(from, path, options.filetype, options.idmap)
|
||||
} else if (options.type === "assets") {
|
||||
const subpath = options.subpath
|
||||
? options.subpath.startsWith("/")
|
||||
@@ -315,9 +324,9 @@ export class SubContainerOwned<
|
||||
: "/"
|
||||
const from = `/media/startos/assets/${subpath}`
|
||||
|
||||
await bind(from, path, mount.options.filetype)
|
||||
await bind(from, path, options.filetype, options.idmap)
|
||||
} else if (options.type === "pointer") {
|
||||
await prepBind(null, path, options.filetype)
|
||||
await prepBind(null, path, "directory")
|
||||
await this.effects.mount({ location: path, target: options })
|
||||
} else if (options.type === "backup") {
|
||||
const subpath = options.subpath
|
||||
@@ -327,7 +336,7 @@ export class SubContainerOwned<
|
||||
: "/"
|
||||
const from = `/media/startos/backup${subpath}`
|
||||
|
||||
await bind(from, path, mount.options.filetype)
|
||||
await bind(from, path, options.filetype, options.idmap)
|
||||
} else {
|
||||
throw new Error(`unknown type ${(options as any).type}`)
|
||||
}
|
||||
@@ -536,7 +545,9 @@ export class SubContainerOwned<
|
||||
delete options.cwd
|
||||
}
|
||||
if (options?.env) {
|
||||
for (let [k, v] of Object.entries(options.env)) {
|
||||
for (let [k, v] of Object.entries(options.env).filter(
|
||||
([_, v]) => v != undefined,
|
||||
)) {
|
||||
extra.push(`--env=${k}=${v}`)
|
||||
}
|
||||
}
|
||||
@@ -585,7 +596,9 @@ export class SubContainerOwned<
|
||||
delete options.cwd
|
||||
}
|
||||
if (options?.env) {
|
||||
for (let [k, v] of Object.entries(options.env)) {
|
||||
for (let [k, v] of Object.entries(options.env).filter(
|
||||
([_, v]) => v != undefined,
|
||||
)) {
|
||||
extra.push(`--env=${k}=${v}`)
|
||||
}
|
||||
}
|
||||
@@ -615,9 +628,9 @@ export class SubContainerOwned<
|
||||
}
|
||||
|
||||
export class SubContainerRc<
|
||||
Manifest extends T.SDKManifest,
|
||||
Effects extends T.Effects = T.Effects,
|
||||
>
|
||||
Manifest extends T.SDKManifest,
|
||||
Effects extends T.Effects = T.Effects,
|
||||
>
|
||||
extends Drop
|
||||
implements SubContainer<Manifest, Effects>
|
||||
{
|
||||
@@ -718,7 +731,9 @@ export class SubContainerRc<
|
||||
if (rcs < 0) console.error(new Error("UNREACHABLE: rcs < 0").stack)
|
||||
}
|
||||
}
|
||||
await this.destroying
|
||||
if (this.destroying) {
|
||||
await this.destroying
|
||||
}
|
||||
this.destroyed = true
|
||||
this.destroying = null
|
||||
return null
|
||||
@@ -798,7 +813,7 @@ export type CommandOptions = {
|
||||
/**
|
||||
* Environment variables to set for this command
|
||||
*/
|
||||
env?: { [variable: string]: string }
|
||||
env?: { [variable in string]?: string }
|
||||
/**
|
||||
* the working directory to run this command in
|
||||
*/
|
||||
@@ -813,6 +828,8 @@ export type StdioOptions = {
|
||||
stdio?: cp.IOType
|
||||
}
|
||||
|
||||
export type IdMap = { fromId: number; toId: number; range: number }
|
||||
|
||||
export type MountOptions =
|
||||
| MountOptionsVolume
|
||||
| MountOptionsAssets
|
||||
@@ -825,12 +842,14 @@ export type MountOptionsVolume = {
|
||||
subpath: string | null
|
||||
readonly: boolean
|
||||
filetype: "file" | "directory" | "infer"
|
||||
idmap: IdMap[]
|
||||
}
|
||||
|
||||
export type MountOptionsAssets = {
|
||||
type: "assets"
|
||||
subpath: string | null
|
||||
filetype: "file" | "directory" | "infer"
|
||||
idmap: { fromId: number; toId: number; range: number }[]
|
||||
}
|
||||
|
||||
export type MountOptionsPointer = {
|
||||
@@ -839,13 +858,14 @@ export type MountOptionsPointer = {
|
||||
volumeId: string
|
||||
subpath: string | null
|
||||
readonly: boolean
|
||||
filetype: "file" | "directory" | "infer"
|
||||
idmap: { fromId: number; toId: number; range: number }[]
|
||||
}
|
||||
|
||||
export type MountOptionsBackup = {
|
||||
type: "backup"
|
||||
subpath: string | null
|
||||
filetype: "file" | "directory" | "infer"
|
||||
idmap: { fromId: number; toId: number; range: number }[]
|
||||
}
|
||||
function wait(time: number) {
|
||||
return new Promise((resolve) => setTimeout(resolve, time))
|
||||
|
||||
Reference in New Issue
Block a user