mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-26 10:21:52 +00:00
Merge branch 'next/minor' of github.com:Start9Labs/start-os into next/major
This commit is contained in:
@@ -1,4 +0,0 @@
|
||||
FROM node:18-alpine
|
||||
|
||||
ADD ./startInit.js /usr/local/lib/startInit.js
|
||||
ADD ./entrypoint.sh /usr/local/bin/entrypoint.sh
|
||||
@@ -3,38 +3,61 @@
|
||||
## Methods
|
||||
|
||||
### init
|
||||
|
||||
initialize runtime (mount `/proc`, `/sys`, `/dev`, and `/run` to each image in `/media/images`)
|
||||
|
||||
called after os has mounted js and images to the container
|
||||
|
||||
#### args
|
||||
|
||||
`[]`
|
||||
|
||||
#### response
|
||||
|
||||
`null`
|
||||
|
||||
### exit
|
||||
|
||||
shutdown runtime
|
||||
|
||||
#### args
|
||||
|
||||
`[]`
|
||||
|
||||
#### response
|
||||
|
||||
`null`
|
||||
|
||||
### start
|
||||
|
||||
run main method if not already running
|
||||
|
||||
#### args
|
||||
|
||||
`[]`
|
||||
|
||||
#### response
|
||||
|
||||
`null`
|
||||
|
||||
### stop
|
||||
|
||||
stop main method by sending SIGTERM to child processes, and SIGKILL after timeout
|
||||
|
||||
#### args
|
||||
|
||||
`{ timeout: millis }`
|
||||
|
||||
#### response
|
||||
|
||||
`null`
|
||||
|
||||
### execute
|
||||
|
||||
run a specific package procedure
|
||||
#### args
|
||||
|
||||
#### args
|
||||
|
||||
```ts
|
||||
{
|
||||
procedure: JsonPath,
|
||||
@@ -42,12 +65,17 @@ run a specific package procedure
|
||||
timeout: millis,
|
||||
}
|
||||
```
|
||||
|
||||
#### response
|
||||
|
||||
`any`
|
||||
|
||||
### sandbox
|
||||
|
||||
run a specific package procedure in sandbox mode
|
||||
#### args
|
||||
|
||||
#### args
|
||||
|
||||
```ts
|
||||
{
|
||||
procedure: JsonPath,
|
||||
@@ -55,5 +83,7 @@ run a specific package procedure in sandbox mode
|
||||
timeout: millis,
|
||||
}
|
||||
```
|
||||
|
||||
#### response
|
||||
|
||||
`any`
|
||||
|
||||
9
container-runtime/container-runtime.service
Normal file
9
container-runtime/container-runtime.service
Normal file
@@ -0,0 +1,9 @@
|
||||
[Unit]
|
||||
Description=StartOS Container Runtime
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
ExecStart=/usr/bin/node --experimental-detect-module --unhandled-rejections=warn /usr/lib/startos/init/index.js
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@@ -1,10 +0,0 @@
|
||||
#!/sbin/openrc-run
|
||||
|
||||
name=containerRuntime
|
||||
#cfgfile="/etc/containerRuntime/containerRuntime.conf"
|
||||
command="/usr/bin/node"
|
||||
command_args="--experimental-detect-module --unhandled-rejections=warn /usr/lib/startos/init/index.js"
|
||||
pidfile="/run/containerRuntime.pid"
|
||||
command_background="yes"
|
||||
output_log="/var/log/containerRuntime.log"
|
||||
error_log="/var/log/containerRuntime.err"
|
||||
23
container-runtime/deb-install.sh
Normal file
23
container-runtime/deb-install.sh
Normal file
@@ -0,0 +1,23 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
mkdir -p /run/systemd/resolve
|
||||
echo "nameserver 8.8.8.8" > /run/systemd/resolve/stub-resolv.conf
|
||||
|
||||
apt-get update
|
||||
apt-get install -y curl rsync qemu-user-static
|
||||
|
||||
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 '/\(^\|#\)Storage=/c\Storage=persistent' /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 '/\(^\|#\)ForwardToSyslog=/c\ForwardToSyslog=no' /etc/systemd/journald.conf
|
||||
|
||||
systemctl enable container-runtime.service
|
||||
|
||||
rm -rf /run/systemd
|
||||
@@ -4,8 +4,8 @@ cd "$(dirname "${BASH_SOURCE[0]}")"
|
||||
|
||||
set -e
|
||||
|
||||
DISTRO=alpine
|
||||
VERSION=3.19
|
||||
DISTRO=debian
|
||||
VERSION=bookworm
|
||||
ARCH=${ARCH:-$(uname -m)}
|
||||
FLAVOR=default
|
||||
|
||||
@@ -16,4 +16,8 @@ elif [ "$_ARCH" = "aarch64" ]; then
|
||||
_ARCH=arm64
|
||||
fi
|
||||
|
||||
curl https://images.linuxcontainers.org/$(curl --silent https://images.linuxcontainers.org/meta/1.0/index-system | grep "^$DISTRO;$VERSION;$_ARCH;$FLAVOR;" | head -n1 | sed 's/^.*;//g')/rootfs.squashfs --output alpine.${ARCH}.squashfs
|
||||
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"
|
||||
|
||||
echo "Downloading $URL to debian.${ARCH}.squashfs"
|
||||
|
||||
curl -fsSL "$URL" > debian.${ARCH}.squashfs
|
||||
8
container-runtime/jest.config.js
Normal file
8
container-runtime/jest.config.js
Normal file
@@ -0,0 +1,8 @@
|
||||
/** @type {import('ts-jest').JestConfigWithTsJest} */
|
||||
module.exports = {
|
||||
preset: "ts-jest",
|
||||
automock: false,
|
||||
testEnvironment: "node",
|
||||
rootDir: "./src/",
|
||||
modulePathIgnorePatterns: ["./dist/"],
|
||||
}
|
||||
6501
container-runtime/package-lock.json
generated
6501
container-runtime/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,12 +1,13 @@
|
||||
{
|
||||
"name": "start-init",
|
||||
"name": "container-runtime",
|
||||
"version": "0.0.0",
|
||||
"description": "We want to be the sdk intermitent for the system",
|
||||
"module": "./index.js",
|
||||
"scripts": {
|
||||
"check": "tsc --noEmit",
|
||||
"build": "prettier --write '**/*.ts' && rm -rf dist && tsc",
|
||||
"tsc": "rm -rf dist; tsc"
|
||||
"build": "prettier . '!tmp/**' --write && rm -rf dist && tsc",
|
||||
"tsc": "rm -rf dist; tsc",
|
||||
"test": "jest -c ./jest.config.js"
|
||||
},
|
||||
"author": "",
|
||||
"prettier": {
|
||||
@@ -17,12 +18,16 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@iarna/toml": "^2.2.5",
|
||||
"@noble/curves": "^1.4.0",
|
||||
"@noble/hashes": "^1.4.0",
|
||||
"@start9labs/start-sdk": "file:../sdk/dist",
|
||||
"esbuild-plugin-resolve": "^2.0.0",
|
||||
"filebrowser": "^1.0.0",
|
||||
"isomorphic-fetch": "^3.0.0",
|
||||
"jsonpath": "^1.1.1",
|
||||
"lodash.merge": "^4.6.2",
|
||||
"node-fetch": "^3.1.0",
|
||||
"ts-matches": "^5.4.1",
|
||||
"ts-matches": "^5.5.1",
|
||||
"tslib": "^2.5.3",
|
||||
"typescript": "^5.1.3",
|
||||
"yaml": "^2.3.1"
|
||||
@@ -30,8 +35,12 @@
|
||||
"devDependencies": {
|
||||
"@swc/cli": "^0.1.62",
|
||||
"@swc/core": "^1.3.65",
|
||||
"@types/jest": "^29.5.12",
|
||||
"@types/jsonpath": "^0.2.4",
|
||||
"@types/node": "^20.11.13",
|
||||
"jest": "^29.7.0",
|
||||
"prettier": "^3.2.5",
|
||||
"ts-jest": "^29.2.3",
|
||||
"typescript": ">5.2"
|
||||
}
|
||||
}
|
||||
|
||||
301
container-runtime/src/Adapters/EffectCreator.ts
Normal file
301
container-runtime/src/Adapters/EffectCreator.ts
Normal file
@@ -0,0 +1,301 @@
|
||||
import { types as T } from "@start9labs/start-sdk"
|
||||
import * as net from "net"
|
||||
import { object, string, number, literals, some, unknown } from "ts-matches"
|
||||
import { Effects } from "../Models/Effects"
|
||||
|
||||
import { CallbackHolder } from "../Models/CallbackHolder"
|
||||
import { MainEffects } from "@start9labs/start-sdk/cjs/lib/StartSdk"
|
||||
const matchRpcError = object({
|
||||
error: object(
|
||||
{
|
||||
code: number,
|
||||
message: string,
|
||||
data: some(
|
||||
string,
|
||||
object(
|
||||
{
|
||||
details: string,
|
||||
debug: string,
|
||||
},
|
||||
["debug"],
|
||||
),
|
||||
),
|
||||
},
|
||||
["data"],
|
||||
),
|
||||
})
|
||||
const testRpcError = matchRpcError.test
|
||||
const testRpcResult = object({
|
||||
result: unknown,
|
||||
}).test
|
||||
type RpcError = typeof matchRpcError._TYPE
|
||||
|
||||
const SOCKET_PATH = "/media/startos/rpc/host.sock"
|
||||
let hostSystemId = 0
|
||||
|
||||
export type EffectContext = {
|
||||
procedureId: string | null
|
||||
callbacks: CallbackHolder | null
|
||||
}
|
||||
|
||||
const rpcRoundFor =
|
||||
(procedureId: string | null) =>
|
||||
<K extends keyof Effects | "getStore" | "setStore" | "clearCallbacks">(
|
||||
method: K,
|
||||
params: Record<string, unknown>,
|
||||
) => {
|
||||
const id = hostSystemId++
|
||||
const client = net.createConnection({ path: SOCKET_PATH }, () => {
|
||||
client.write(
|
||||
JSON.stringify({
|
||||
id,
|
||||
method,
|
||||
params: { ...params, procedureId },
|
||||
}) + "\n",
|
||||
)
|
||||
})
|
||||
let bufs: Buffer[] = []
|
||||
return new Promise((resolve, reject) => {
|
||||
client.on("data", (data) => {
|
||||
try {
|
||||
bufs.push(data)
|
||||
if (data.reduce((acc, x) => acc || x == 10, false)) {
|
||||
const res: unknown = JSON.parse(
|
||||
Buffer.concat(bufs).toString().split("\n")[0],
|
||||
)
|
||||
if (testRpcError(res)) {
|
||||
let message = res.error.message
|
||||
console.error("Error in host RPC:", { method, params })
|
||||
if (string.test(res.error.data)) {
|
||||
message += ": " + res.error.data
|
||||
console.error(`Details: ${res.error.data}`)
|
||||
} else {
|
||||
if (res.error.data?.details) {
|
||||
message += ": " + res.error.data.details
|
||||
console.error(`Details: ${res.error.data.details}`)
|
||||
}
|
||||
if (res.error.data?.debug) {
|
||||
message += "\n" + res.error.data.debug
|
||||
console.error(`Debug: ${res.error.data.debug}`)
|
||||
}
|
||||
}
|
||||
reject(new Error(`${message}@${method}`))
|
||||
} else if (testRpcResult(res)) {
|
||||
resolve(res.result)
|
||||
} else {
|
||||
reject(new Error(`malformed response ${JSON.stringify(res)}`))
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
reject(error)
|
||||
}
|
||||
client.end()
|
||||
})
|
||||
client.on("error", (error) => {
|
||||
reject(error)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function makeEffects(context: EffectContext): Effects {
|
||||
const rpcRound = rpcRoundFor(context.procedureId)
|
||||
const self: Effects = {
|
||||
bind(...[options]: Parameters<T.Effects["bind"]>) {
|
||||
return rpcRound("bind", {
|
||||
...options,
|
||||
stack: new Error().stack,
|
||||
}) as ReturnType<T.Effects["bind"]>
|
||||
},
|
||||
clearBindings(...[]: Parameters<T.Effects["clearBindings"]>) {
|
||||
return rpcRound("clearBindings", {}) as ReturnType<
|
||||
T.Effects["clearBindings"]
|
||||
>
|
||||
},
|
||||
clearServiceInterfaces(
|
||||
...[]: Parameters<T.Effects["clearServiceInterfaces"]>
|
||||
) {
|
||||
return rpcRound("clearServiceInterfaces", {}) as ReturnType<
|
||||
T.Effects["clearServiceInterfaces"]
|
||||
>
|
||||
},
|
||||
getInstalledPackages(...[]: Parameters<T.Effects["getInstalledPackages"]>) {
|
||||
return rpcRound("getInstalledPackages", {}) as ReturnType<
|
||||
T.Effects["getInstalledPackages"]
|
||||
>
|
||||
},
|
||||
createOverlayedImage(options: {
|
||||
imageId: string
|
||||
}): Promise<[string, string]> {
|
||||
return rpcRound("createOverlayedImage", options) as ReturnType<
|
||||
T.Effects["createOverlayedImage"]
|
||||
>
|
||||
},
|
||||
destroyOverlayedImage(options: { guid: string }): Promise<void> {
|
||||
return rpcRound("destroyOverlayedImage", options) as ReturnType<
|
||||
T.Effects["destroyOverlayedImage"]
|
||||
>
|
||||
},
|
||||
executeAction(...[options]: Parameters<T.Effects["executeAction"]>) {
|
||||
return rpcRound("executeAction", options) as ReturnType<
|
||||
T.Effects["executeAction"]
|
||||
>
|
||||
},
|
||||
exportAction(...[options]: Parameters<T.Effects["exportAction"]>) {
|
||||
return rpcRound("exportAction", options) as ReturnType<
|
||||
T.Effects["exportAction"]
|
||||
>
|
||||
},
|
||||
exportServiceInterface: ((
|
||||
...[options]: Parameters<Effects["exportServiceInterface"]>
|
||||
) => {
|
||||
return rpcRound("exportServiceInterface", options) as ReturnType<
|
||||
T.Effects["exportServiceInterface"]
|
||||
>
|
||||
}) as Effects["exportServiceInterface"],
|
||||
exposeForDependents(
|
||||
...[options]: Parameters<T.Effects["exposeForDependents"]>
|
||||
) {
|
||||
return rpcRound("exposeForDependents", options) as ReturnType<
|
||||
T.Effects["exposeForDependents"]
|
||||
>
|
||||
},
|
||||
getConfigured(...[]: Parameters<T.Effects["getConfigured"]>) {
|
||||
return rpcRound("getConfigured", {}) as ReturnType<
|
||||
T.Effects["getConfigured"]
|
||||
>
|
||||
},
|
||||
getContainerIp(...[]: Parameters<T.Effects["getContainerIp"]>) {
|
||||
return rpcRound("getContainerIp", {}) as ReturnType<
|
||||
T.Effects["getContainerIp"]
|
||||
>
|
||||
},
|
||||
getHostInfo: ((...[allOptions]: Parameters<T.Effects["getHostInfo"]>) => {
|
||||
const options = {
|
||||
...allOptions,
|
||||
callback: context.callbacks?.addCallback(allOptions.callback) || null,
|
||||
}
|
||||
return rpcRound("getHostInfo", options) as ReturnType<
|
||||
T.Effects["getHostInfo"]
|
||||
> as any
|
||||
}) as Effects["getHostInfo"],
|
||||
getServiceInterface(
|
||||
...[options]: Parameters<T.Effects["getServiceInterface"]>
|
||||
) {
|
||||
return rpcRound("getServiceInterface", {
|
||||
...options,
|
||||
callback: context.callbacks?.addCallback(options.callback) || null,
|
||||
}) as ReturnType<T.Effects["getServiceInterface"]>
|
||||
},
|
||||
|
||||
getPrimaryUrl(...[options]: Parameters<T.Effects["getPrimaryUrl"]>) {
|
||||
return rpcRound("getPrimaryUrl", {
|
||||
...options,
|
||||
callback: context.callbacks?.addCallback(options.callback) || null,
|
||||
}) as ReturnType<T.Effects["getPrimaryUrl"]>
|
||||
},
|
||||
getServicePortForward(
|
||||
...[options]: Parameters<T.Effects["getServicePortForward"]>
|
||||
) {
|
||||
return rpcRound("getServicePortForward", options) as ReturnType<
|
||||
T.Effects["getServicePortForward"]
|
||||
>
|
||||
},
|
||||
getSslCertificate(options: Parameters<T.Effects["getSslCertificate"]>[0]) {
|
||||
return rpcRound("getSslCertificate", options) as ReturnType<
|
||||
T.Effects["getSslCertificate"]
|
||||
>
|
||||
},
|
||||
getSslKey(options: Parameters<T.Effects["getSslKey"]>[0]) {
|
||||
return rpcRound("getSslKey", options) as ReturnType<
|
||||
T.Effects["getSslKey"]
|
||||
>
|
||||
},
|
||||
getSystemSmtp(...[options]: Parameters<T.Effects["getSystemSmtp"]>) {
|
||||
return rpcRound("getSystemSmtp", {
|
||||
...options,
|
||||
callback: context.callbacks?.addCallback(options.callback) || null,
|
||||
}) as ReturnType<T.Effects["getSystemSmtp"]>
|
||||
},
|
||||
listServiceInterfaces(
|
||||
...[options]: Parameters<T.Effects["listServiceInterfaces"]>
|
||||
) {
|
||||
return rpcRound("listServiceInterfaces", {
|
||||
...options,
|
||||
callback: context.callbacks?.addCallback(options.callback) || null,
|
||||
}) as ReturnType<T.Effects["listServiceInterfaces"]>
|
||||
},
|
||||
mount(...[options]: Parameters<T.Effects["mount"]>) {
|
||||
return rpcRound("mount", options) as ReturnType<T.Effects["mount"]>
|
||||
},
|
||||
clearActions(...[]: Parameters<T.Effects["clearActions"]>) {
|
||||
return rpcRound("clearActions", {}) as ReturnType<
|
||||
T.Effects["clearActions"]
|
||||
>
|
||||
},
|
||||
restart(...[]: Parameters<T.Effects["restart"]>) {
|
||||
return rpcRound("restart", {}) as ReturnType<T.Effects["restart"]>
|
||||
},
|
||||
setConfigured(...[configured]: Parameters<T.Effects["setConfigured"]>) {
|
||||
return rpcRound("setConfigured", { configured }) as ReturnType<
|
||||
T.Effects["setConfigured"]
|
||||
>
|
||||
},
|
||||
setDependencies(
|
||||
dependencies: Parameters<T.Effects["setDependencies"]>[0],
|
||||
): ReturnType<T.Effects["setDependencies"]> {
|
||||
return rpcRound("setDependencies", dependencies) as ReturnType<
|
||||
T.Effects["setDependencies"]
|
||||
>
|
||||
},
|
||||
checkDependencies(
|
||||
options: Parameters<T.Effects["checkDependencies"]>[0],
|
||||
): ReturnType<T.Effects["checkDependencies"]> {
|
||||
return rpcRound("checkDependencies", options) as ReturnType<
|
||||
T.Effects["checkDependencies"]
|
||||
>
|
||||
},
|
||||
getDependencies(): ReturnType<T.Effects["getDependencies"]> {
|
||||
return rpcRound("getDependencies", {}) as ReturnType<
|
||||
T.Effects["getDependencies"]
|
||||
>
|
||||
},
|
||||
setHealth(...[options]: Parameters<T.Effects["setHealth"]>) {
|
||||
return rpcRound("setHealth", options) as ReturnType<
|
||||
T.Effects["setHealth"]
|
||||
>
|
||||
},
|
||||
|
||||
setMainStatus(o: { status: "running" | "stopped" }): Promise<void> {
|
||||
return rpcRound("setMainStatus", o) as ReturnType<T.Effects["setHealth"]>
|
||||
},
|
||||
|
||||
shutdown(...[]: Parameters<T.Effects["shutdown"]>) {
|
||||
return rpcRound("shutdown", {}) as ReturnType<T.Effects["shutdown"]>
|
||||
},
|
||||
store: {
|
||||
get: async (options: any) =>
|
||||
rpcRound("getStore", {
|
||||
...options,
|
||||
callback: context.callbacks?.addCallback(options.callback) || null,
|
||||
}) as any,
|
||||
set: async (options: any) =>
|
||||
rpcRound("setStore", options) as ReturnType<T.Effects["store"]["set"]>,
|
||||
} as T.Effects["store"],
|
||||
}
|
||||
return self
|
||||
}
|
||||
|
||||
export function makeProcedureEffects(procedureId: string): Effects {
|
||||
return makeEffects({ procedureId, callbacks: null })
|
||||
}
|
||||
|
||||
export function makeMainEffects(): MainEffects {
|
||||
const rpcRound = rpcRoundFor(null)
|
||||
return {
|
||||
_type: "main",
|
||||
clearCallbacks: () => {
|
||||
return rpcRound("clearCallbacks", {}) as Promise<void>
|
||||
},
|
||||
...makeEffects({ procedureId: null, callbacks: new CallbackHolder() }),
|
||||
}
|
||||
}
|
||||
@@ -1,290 +0,0 @@
|
||||
import { types as T } from "@start9labs/start-sdk"
|
||||
import * as net from "net"
|
||||
import { object, string, number, literals, some, unknown } from "ts-matches"
|
||||
import { Effects } from "../Models/Effects"
|
||||
|
||||
import { CallbackHolder } from "../Models/CallbackHolder"
|
||||
const matchRpcError = object({
|
||||
error: object(
|
||||
{
|
||||
code: number,
|
||||
message: string,
|
||||
data: some(
|
||||
string,
|
||||
object(
|
||||
{
|
||||
details: string,
|
||||
debug: string,
|
||||
},
|
||||
["debug"],
|
||||
),
|
||||
),
|
||||
},
|
||||
["data"],
|
||||
),
|
||||
})
|
||||
const testRpcError = matchRpcError.test
|
||||
const testRpcResult = object({
|
||||
result: unknown,
|
||||
}).test
|
||||
type RpcError = typeof matchRpcError._TYPE
|
||||
|
||||
const SOCKET_PATH = "/media/startos/rpc/host.sock"
|
||||
const MAIN = "/main" as const
|
||||
export class HostSystemStartOs implements Effects {
|
||||
static of(callbackHolder: CallbackHolder) {
|
||||
return new HostSystemStartOs(callbackHolder)
|
||||
}
|
||||
|
||||
constructor(readonly callbackHolder: CallbackHolder) {}
|
||||
id = 0
|
||||
rpcRound<K extends keyof Effects | "getStore" | "setStore">(
|
||||
method: K,
|
||||
params: unknown,
|
||||
) {
|
||||
const id = this.id++
|
||||
const client = net.createConnection({ path: SOCKET_PATH }, () => {
|
||||
client.write(
|
||||
JSON.stringify({
|
||||
id,
|
||||
method,
|
||||
params,
|
||||
}) + "\n",
|
||||
)
|
||||
})
|
||||
let bufs: Buffer[] = []
|
||||
return new Promise((resolve, reject) => {
|
||||
client.on("data", (data) => {
|
||||
try {
|
||||
bufs.push(data)
|
||||
if (data.reduce((acc, x) => acc || x == 10, false)) {
|
||||
const res: unknown = JSON.parse(
|
||||
Buffer.concat(bufs).toString().split("\n")[0],
|
||||
)
|
||||
if (testRpcError(res)) {
|
||||
let message = res.error.message
|
||||
console.error({ method, params, hostSystemStartOs: true })
|
||||
if (string.test(res.error.data)) {
|
||||
message += ": " + res.error.data
|
||||
console.error(res.error.data)
|
||||
} else {
|
||||
if (res.error.data?.details) {
|
||||
message += ": " + res.error.data.details
|
||||
console.error(res.error.data.details)
|
||||
}
|
||||
if (res.error.data?.debug) {
|
||||
message += "\n" + res.error.data.debug
|
||||
console.error("Debug: " + res.error.data.debug)
|
||||
}
|
||||
}
|
||||
reject(new Error(`${message}@${method}`))
|
||||
} else if (testRpcResult(res)) {
|
||||
resolve(res.result)
|
||||
} else {
|
||||
reject(new Error(`malformed response ${JSON.stringify(res)}`))
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
reject(error)
|
||||
}
|
||||
client.end()
|
||||
})
|
||||
client.on("error", (error) => {
|
||||
reject(error)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
bind(...[options]: Parameters<T.Effects["bind"]>) {
|
||||
return this.rpcRound("bind", options) as ReturnType<T.Effects["bind"]>
|
||||
}
|
||||
clearBindings(...[]: Parameters<T.Effects["clearBindings"]>) {
|
||||
return this.rpcRound("clearBindings", null) as ReturnType<
|
||||
T.Effects["clearBindings"]
|
||||
>
|
||||
}
|
||||
clearServiceInterfaces(
|
||||
...[]: Parameters<T.Effects["clearServiceInterfaces"]>
|
||||
) {
|
||||
return this.rpcRound("clearServiceInterfaces", null) as ReturnType<
|
||||
T.Effects["clearServiceInterfaces"]
|
||||
>
|
||||
}
|
||||
createOverlayedImage(options: {
|
||||
imageId: string
|
||||
}): Promise<[string, string]> {
|
||||
return this.rpcRound("createOverlayedImage", options) as ReturnType<
|
||||
T.Effects["createOverlayedImage"]
|
||||
>
|
||||
}
|
||||
destroyOverlayedImage(options: { guid: string }): Promise<void> {
|
||||
return this.rpcRound("destroyOverlayedImage", options) as ReturnType<
|
||||
T.Effects["destroyOverlayedImage"]
|
||||
>
|
||||
}
|
||||
executeAction(...[options]: Parameters<T.Effects["executeAction"]>) {
|
||||
return this.rpcRound("executeAction", options) as ReturnType<
|
||||
T.Effects["executeAction"]
|
||||
>
|
||||
}
|
||||
exists(...[packageId]: Parameters<T.Effects["exists"]>) {
|
||||
return this.rpcRound("exists", packageId) as ReturnType<T.Effects["exists"]>
|
||||
}
|
||||
exportAction(...[options]: Parameters<T.Effects["exportAction"]>) {
|
||||
return this.rpcRound("exportAction", options) as ReturnType<
|
||||
T.Effects["exportAction"]
|
||||
>
|
||||
}
|
||||
exportServiceInterface: Effects["exportServiceInterface"] = (
|
||||
...[options]: Parameters<Effects["exportServiceInterface"]>
|
||||
) => {
|
||||
return this.rpcRound("exportServiceInterface", options) as ReturnType<
|
||||
T.Effects["exportServiceInterface"]
|
||||
>
|
||||
}
|
||||
exposeForDependents(...[options]: any) {
|
||||
return this.rpcRound("exposeForDependents", null) as ReturnType<
|
||||
T.Effects["exposeForDependents"]
|
||||
>
|
||||
}
|
||||
getConfigured(...[]: Parameters<T.Effects["getConfigured"]>) {
|
||||
return this.rpcRound("getConfigured", null) as ReturnType<
|
||||
T.Effects["getConfigured"]
|
||||
>
|
||||
}
|
||||
getContainerIp(...[]: Parameters<T.Effects["getContainerIp"]>) {
|
||||
return this.rpcRound("getContainerIp", null) as ReturnType<
|
||||
T.Effects["getContainerIp"]
|
||||
>
|
||||
}
|
||||
getHostInfo: Effects["getHostInfo"] = (...[allOptions]: any[]) => {
|
||||
const options = {
|
||||
...allOptions,
|
||||
callback: this.callbackHolder.addCallback(allOptions.callback),
|
||||
}
|
||||
return this.rpcRound("getHostInfo", options) as ReturnType<
|
||||
T.Effects["getHostInfo"]
|
||||
> as any
|
||||
}
|
||||
getServiceInterface(
|
||||
...[options]: Parameters<T.Effects["getServiceInterface"]>
|
||||
) {
|
||||
return this.rpcRound("getServiceInterface", {
|
||||
...options,
|
||||
callback: this.callbackHolder.addCallback(options.callback),
|
||||
}) as ReturnType<T.Effects["getServiceInterface"]>
|
||||
}
|
||||
|
||||
getPrimaryUrl(...[options]: Parameters<T.Effects["getPrimaryUrl"]>) {
|
||||
return this.rpcRound("getPrimaryUrl", {
|
||||
...options,
|
||||
callback: this.callbackHolder.addCallback(options.callback),
|
||||
}) as ReturnType<T.Effects["getPrimaryUrl"]>
|
||||
}
|
||||
getServicePortForward(
|
||||
...[options]: Parameters<T.Effects["getServicePortForward"]>
|
||||
) {
|
||||
return this.rpcRound("getServicePortForward", options) as ReturnType<
|
||||
T.Effects["getServicePortForward"]
|
||||
>
|
||||
}
|
||||
getSslCertificate(options: Parameters<T.Effects["getSslCertificate"]>[0]) {
|
||||
return this.rpcRound("getSslCertificate", options) as ReturnType<
|
||||
T.Effects["getSslCertificate"]
|
||||
>
|
||||
}
|
||||
getSslKey(options: Parameters<T.Effects["getSslKey"]>[0]) {
|
||||
return this.rpcRound("getSslKey", options) as ReturnType<
|
||||
T.Effects["getSslKey"]
|
||||
>
|
||||
}
|
||||
getSystemSmtp(...[options]: Parameters<T.Effects["getSystemSmtp"]>) {
|
||||
return this.rpcRound("getSystemSmtp", {
|
||||
...options,
|
||||
callback: this.callbackHolder.addCallback(options.callback),
|
||||
}) as ReturnType<T.Effects["getSystemSmtp"]>
|
||||
}
|
||||
listServiceInterfaces(
|
||||
...[options]: Parameters<T.Effects["listServiceInterfaces"]>
|
||||
) {
|
||||
return this.rpcRound("listServiceInterfaces", {
|
||||
...options,
|
||||
callback: this.callbackHolder.addCallback(options.callback),
|
||||
}) as ReturnType<T.Effects["listServiceInterfaces"]>
|
||||
}
|
||||
mount(...[options]: Parameters<T.Effects["mount"]>) {
|
||||
return this.rpcRound("mount", options) as ReturnType<T.Effects["mount"]>
|
||||
}
|
||||
removeAction(...[options]: Parameters<T.Effects["removeAction"]>) {
|
||||
return this.rpcRound("removeAction", options) as ReturnType<
|
||||
T.Effects["removeAction"]
|
||||
>
|
||||
}
|
||||
removeAddress(...[options]: Parameters<T.Effects["removeAddress"]>) {
|
||||
return this.rpcRound("removeAddress", options) as ReturnType<
|
||||
T.Effects["removeAddress"]
|
||||
>
|
||||
}
|
||||
restart(...[]: Parameters<T.Effects["restart"]>) {
|
||||
return this.rpcRound("restart", null)
|
||||
}
|
||||
reverseProxy(...[options]: Parameters<T.Effects["reverseProxy"]>) {
|
||||
return this.rpcRound("reverseProxy", options) as ReturnType<
|
||||
T.Effects["reverseProxy"]
|
||||
>
|
||||
}
|
||||
running(...[packageId]: Parameters<T.Effects["running"]>) {
|
||||
return this.rpcRound("running", { packageId }) as ReturnType<
|
||||
T.Effects["running"]
|
||||
>
|
||||
}
|
||||
// runRsync(...[options]: Parameters<T.Effects[""]>) {
|
||||
//
|
||||
// return this.rpcRound('executeAction', options) as ReturnType<T.Effects["executeAction"]>
|
||||
//
|
||||
// return this.rpcRound('executeAction', options) as ReturnType<T.Effects["executeAction"]>
|
||||
// }
|
||||
setConfigured(...[configured]: Parameters<T.Effects["setConfigured"]>) {
|
||||
return this.rpcRound("setConfigured", { configured }) as ReturnType<
|
||||
T.Effects["setConfigured"]
|
||||
>
|
||||
}
|
||||
setDependencies(
|
||||
...[dependencies]: Parameters<T.Effects["setDependencies"]>
|
||||
): ReturnType<T.Effects["setDependencies"]> {
|
||||
return this.rpcRound("setDependencies", { dependencies }) as ReturnType<
|
||||
T.Effects["setDependencies"]
|
||||
>
|
||||
}
|
||||
setHealth(...[options]: Parameters<T.Effects["setHealth"]>) {
|
||||
return this.rpcRound("setHealth", options) as ReturnType<
|
||||
T.Effects["setHealth"]
|
||||
>
|
||||
}
|
||||
|
||||
setMainStatus(o: { status: "running" | "stopped" }): Promise<void> {
|
||||
return this.rpcRound("setMainStatus", o) as ReturnType<
|
||||
T.Effects["setHealth"]
|
||||
>
|
||||
}
|
||||
|
||||
shutdown(...[]: Parameters<T.Effects["shutdown"]>) {
|
||||
return this.rpcRound("shutdown", null)
|
||||
}
|
||||
stopped(...[packageId]: Parameters<T.Effects["stopped"]>) {
|
||||
return this.rpcRound("stopped", { packageId }) as ReturnType<
|
||||
T.Effects["stopped"]
|
||||
>
|
||||
}
|
||||
store: T.Effects["store"] = {
|
||||
get: async (options: any) =>
|
||||
this.rpcRound("getStore", {
|
||||
...options,
|
||||
callback: this.callbackHolder.addCallback(options.callback),
|
||||
}) as any,
|
||||
set: async (options: any) =>
|
||||
this.rpcRound("setStore", options) as ReturnType<
|
||||
T.Effects["store"]["set"]
|
||||
>,
|
||||
}
|
||||
}
|
||||
@@ -15,15 +15,16 @@ import {
|
||||
} from "ts-matches"
|
||||
|
||||
import { types as T } from "@start9labs/start-sdk"
|
||||
import * as CP from "child_process"
|
||||
import * as Mod from "module"
|
||||
import * as fs from "fs"
|
||||
|
||||
import { CallbackHolder } from "../Models/CallbackHolder"
|
||||
import { AllGetDependencies } from "../Interfaces/AllGetDependencies"
|
||||
import { HostSystem } from "../Interfaces/HostSystem"
|
||||
import { jsonPath } from "../Models/JsonPath"
|
||||
import { System } from "../Interfaces/System"
|
||||
import { RunningMain, System } from "../Interfaces/System"
|
||||
import {
|
||||
MakeMainEffects,
|
||||
MakeProcedureEffects,
|
||||
} from "../Interfaces/MakeEffects"
|
||||
type MaybePromise<T> = T | Promise<T>
|
||||
export const matchRpcResult = anyOf(
|
||||
object({ result: any }),
|
||||
@@ -45,7 +46,7 @@ export const matchRpcResult = anyOf(
|
||||
}),
|
||||
)
|
||||
export type RpcResult = typeof matchRpcResult._TYPE
|
||||
type SocketResponse = { jsonrpc: "2.0"; id: IdType } & RpcResult
|
||||
type SocketResponse = ({ jsonrpc: "2.0"; id: IdType } & RpcResult) | null
|
||||
|
||||
const SOCKET_PARENT = "/media/startos/rpc"
|
||||
const SOCKET_PATH = "/media/startos/rpc/service.sock"
|
||||
@@ -58,11 +59,12 @@ const runType = object({
|
||||
method: literal("execute"),
|
||||
params: object(
|
||||
{
|
||||
id: string,
|
||||
procedure: string,
|
||||
input: any,
|
||||
timeout: number,
|
||||
},
|
||||
["timeout"],
|
||||
["timeout", "input"],
|
||||
),
|
||||
})
|
||||
const sandboxRunType = object({
|
||||
@@ -70,18 +72,18 @@ const sandboxRunType = object({
|
||||
method: literal("sandbox"),
|
||||
params: object(
|
||||
{
|
||||
id: string,
|
||||
procedure: string,
|
||||
input: any,
|
||||
timeout: number,
|
||||
},
|
||||
["timeout"],
|
||||
["timeout", "input"],
|
||||
),
|
||||
})
|
||||
const callbackType = object({
|
||||
id: idType,
|
||||
method: literal("callback"),
|
||||
params: object({
|
||||
callback: string,
|
||||
callback: number,
|
||||
args: array,
|
||||
}),
|
||||
})
|
||||
@@ -89,6 +91,14 @@ const initType = object({
|
||||
id: idType,
|
||||
method: literal("init"),
|
||||
})
|
||||
const startType = object({
|
||||
id: idType,
|
||||
method: literal("start"),
|
||||
})
|
||||
const stopType = object({
|
||||
id: idType,
|
||||
method: literal("stop"),
|
||||
})
|
||||
const exitType = object({
|
||||
id: idType,
|
||||
method: literal("exit"),
|
||||
@@ -101,34 +111,41 @@ const evalType = object({
|
||||
}),
|
||||
})
|
||||
|
||||
const jsonParse = (x: Buffer) => JSON.parse(x.toString())
|
||||
function reduceMethod(
|
||||
methodArgs: object,
|
||||
effects: HostSystem,
|
||||
): (previousValue: any, currentValue: string) => any {
|
||||
return (x: any, method: string) =>
|
||||
Promise.resolve(x)
|
||||
.then((x) => x[method])
|
||||
.then((x) =>
|
||||
typeof x !== "function"
|
||||
? x
|
||||
: x({
|
||||
...methodArgs,
|
||||
effects,
|
||||
}),
|
||||
const jsonParse = (x: string) => JSON.parse(x)
|
||||
|
||||
const handleRpc = (id: IdType, result: Promise<RpcResult>) =>
|
||||
result
|
||||
.then((result) => ({
|
||||
jsonrpc,
|
||||
id,
|
||||
...result,
|
||||
}))
|
||||
.then((x) => {
|
||||
if (
|
||||
("result" in x && x.result === undefined) ||
|
||||
!("error" in x || "result" in x)
|
||||
)
|
||||
}
|
||||
(x as any).result = null
|
||||
return x
|
||||
})
|
||||
.catch((error) => ({
|
||||
jsonrpc,
|
||||
id,
|
||||
error: {
|
||||
code: 0,
|
||||
message: typeof error,
|
||||
data: { details: "" + error, debug: error?.stack },
|
||||
},
|
||||
}))
|
||||
|
||||
const hasId = object({ id: idType }).test
|
||||
export class RpcListener {
|
||||
unixSocketServer = net.createServer(async (server) => {})
|
||||
private _system: System | undefined
|
||||
private _effects: HostSystem | undefined
|
||||
private _makeProcedureEffects: MakeProcedureEffects | undefined
|
||||
private _makeMainEffects: MakeMainEffects | undefined
|
||||
|
||||
constructor(
|
||||
readonly getDependencies: AllGetDependencies,
|
||||
private callbacks = new CallbackHolder(),
|
||||
) {
|
||||
constructor(readonly getDependencies: AllGetDependencies) {
|
||||
if (!fs.existsSync(SOCKET_PARENT)) {
|
||||
fs.mkdirSync(SOCKET_PARENT, { recursive: true })
|
||||
}
|
||||
@@ -160,125 +177,140 @@ export class RpcListener {
|
||||
details: error?.message ?? String(error),
|
||||
debug: error?.stack,
|
||||
},
|
||||
code: 0,
|
||||
code: 1,
|
||||
},
|
||||
})
|
||||
const writeDataToSocket = (x: SocketResponse) =>
|
||||
new Promise((resolve) => s.write(JSON.stringify(x), resolve))
|
||||
const writeDataToSocket = (x: SocketResponse) => {
|
||||
if (x != null) {
|
||||
return new Promise((resolve) =>
|
||||
s.write(JSON.stringify(x) + "\n", resolve),
|
||||
)
|
||||
}
|
||||
}
|
||||
s.on("data", (a) =>
|
||||
Promise.resolve(a)
|
||||
.then((b) => b.toString())
|
||||
.then(logData("dataIn"))
|
||||
.then(jsonParse)
|
||||
.then(captureId)
|
||||
.then((x) => this.dealWithInput(x))
|
||||
.catch(mapError)
|
||||
.then(logData("response"))
|
||||
.then(writeDataToSocket)
|
||||
.finally(() => void s.end()),
|
||||
.then(writeDataToSocket),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
private get effects() {
|
||||
return this.getDependencies.hostSystem()(this.callbacks)
|
||||
}
|
||||
|
||||
private get system() {
|
||||
if (!this._system) throw new Error("System not initialized")
|
||||
return this._system
|
||||
}
|
||||
|
||||
private get makeProcedureEffects() {
|
||||
if (!this._makeProcedureEffects) {
|
||||
this._makeProcedureEffects = this.getDependencies.makeProcedureEffects()
|
||||
}
|
||||
return this._makeProcedureEffects
|
||||
}
|
||||
|
||||
private get makeMainEffects() {
|
||||
if (!this._makeMainEffects) {
|
||||
this._makeMainEffects = this.getDependencies.makeMainEffects()
|
||||
}
|
||||
return this._makeMainEffects
|
||||
}
|
||||
|
||||
private dealWithInput(input: unknown): MaybePromise<SocketResponse> {
|
||||
return matches(input)
|
||||
.when(some(runType, sandboxRunType), async ({ id, params }) => {
|
||||
.when(runType, async ({ id, params }) => {
|
||||
const system = this.system
|
||||
const procedure = jsonPath.unsafeCast(params.procedure)
|
||||
return system
|
||||
.execute(this.effects, {
|
||||
const effects = this.getDependencies.makeProcedureEffects()(params.id)
|
||||
return handleRpc(
|
||||
id,
|
||||
system.execute(effects, {
|
||||
procedure,
|
||||
input: params.input,
|
||||
timeout: params.timeout,
|
||||
})
|
||||
.then((result) => ({
|
||||
jsonrpc,
|
||||
id,
|
||||
...result,
|
||||
}))
|
||||
.then((x) => {
|
||||
if (
|
||||
("result" in x && x.result === undefined) ||
|
||||
!("error" in x || "result" in x)
|
||||
)
|
||||
(x as any).result = null
|
||||
return x
|
||||
})
|
||||
.catch((error) => ({
|
||||
jsonrpc,
|
||||
id,
|
||||
error: {
|
||||
code: 0,
|
||||
message: typeof error,
|
||||
data: { details: "" + error, debug: error?.stack },
|
||||
},
|
||||
}))
|
||||
}),
|
||||
)
|
||||
})
|
||||
.when(callbackType, async ({ id, params: { callback, args } }) =>
|
||||
Promise.resolve(this.callbacks.callCallback(callback, args))
|
||||
.then((result) => ({
|
||||
jsonrpc,
|
||||
id,
|
||||
result,
|
||||
}))
|
||||
.catch((error) => ({
|
||||
jsonrpc,
|
||||
id,
|
||||
|
||||
error: {
|
||||
code: 0,
|
||||
message: typeof error,
|
||||
data: {
|
||||
details: error?.message ?? String(error),
|
||||
debug: error?.stack,
|
||||
},
|
||||
},
|
||||
})),
|
||||
)
|
||||
.when(exitType, async ({ id }) => {
|
||||
if (this._system) this._system.exit(this.effects)
|
||||
delete this._system
|
||||
delete this._effects
|
||||
|
||||
return {
|
||||
jsonrpc,
|
||||
.when(sandboxRunType, async ({ id, params }) => {
|
||||
const system = this.system
|
||||
const procedure = jsonPath.unsafeCast(params.procedure)
|
||||
const effects = this.makeProcedureEffects(params.id)
|
||||
return handleRpc(
|
||||
id,
|
||||
result: null,
|
||||
}
|
||||
system.sandbox(effects, {
|
||||
procedure,
|
||||
input: params.input,
|
||||
timeout: params.timeout,
|
||||
}),
|
||||
)
|
||||
})
|
||||
.when(callbackType, async ({ params: { callback, args } }) => {
|
||||
this.system.callCallback(callback, args)
|
||||
return null
|
||||
})
|
||||
.when(startType, async ({ id }) => {
|
||||
return handleRpc(
|
||||
id,
|
||||
this.system
|
||||
.start(this.makeMainEffects())
|
||||
.then((result) => ({ result })),
|
||||
)
|
||||
})
|
||||
.when(stopType, async ({ id }) => {
|
||||
return handleRpc(
|
||||
id,
|
||||
this.system.stop().then((result) => ({ result })),
|
||||
)
|
||||
})
|
||||
.when(exitType, async ({ id }) => {
|
||||
return handleRpc(
|
||||
id,
|
||||
(async () => {
|
||||
if (this._system) await this._system.exit()
|
||||
})().then((result) => ({ result })),
|
||||
)
|
||||
})
|
||||
.when(initType, async ({ id }) => {
|
||||
this._system = await this.getDependencies.system()
|
||||
|
||||
return {
|
||||
jsonrpc,
|
||||
return handleRpc(
|
||||
id,
|
||||
result: null,
|
||||
}
|
||||
(async () => {
|
||||
if (!this._system) {
|
||||
const system = await this.getDependencies.system()
|
||||
await system.init()
|
||||
this._system = system
|
||||
}
|
||||
})().then((result) => ({ result })),
|
||||
)
|
||||
})
|
||||
.when(evalType, async ({ id, params }) => {
|
||||
const result = await new Function(
|
||||
`return (async () => { return (${params.script}) }).call(this)`,
|
||||
).call({
|
||||
listener: this,
|
||||
require: require,
|
||||
})
|
||||
return {
|
||||
jsonrpc,
|
||||
return handleRpc(
|
||||
id,
|
||||
result: !["string", "number", "boolean", "null", "object"].includes(
|
||||
typeof result,
|
||||
)
|
||||
? null
|
||||
: result,
|
||||
}
|
||||
(async () => {
|
||||
const result = await new Function(
|
||||
`return (async () => { return (${params.script}) }).call(this)`,
|
||||
).call({
|
||||
listener: this,
|
||||
require: require,
|
||||
})
|
||||
return {
|
||||
jsonrpc,
|
||||
id,
|
||||
result: ![
|
||||
"string",
|
||||
"number",
|
||||
"boolean",
|
||||
"null",
|
||||
"object",
|
||||
].includes(typeof result)
|
||||
? null
|
||||
: result,
|
||||
}
|
||||
})(),
|
||||
)
|
||||
})
|
||||
.when(shape({ id: idType, method: string }), ({ id, method }) => ({
|
||||
jsonrpc,
|
||||
|
||||
@@ -14,10 +14,11 @@ export class DockerProcedureContainer {
|
||||
// }
|
||||
static async of(
|
||||
effects: T.Effects,
|
||||
packageId: string,
|
||||
data: DockerProcedure,
|
||||
volumes: { [id: VolumeId]: Volume },
|
||||
) {
|
||||
const overlay = await Overlay.of(effects, data.image)
|
||||
const overlay = await Overlay.of(effects, { id: data.image })
|
||||
|
||||
if (data.mounts) {
|
||||
const mounts = data.mounts
|
||||
@@ -38,16 +39,25 @@ export class DockerProcedureContainer {
|
||||
mounts[mount],
|
||||
)
|
||||
} else if (volumeMount.type === "certificate") {
|
||||
volumeMount
|
||||
const hostnames = [
|
||||
`${packageId}.embassy`,
|
||||
...new Set(
|
||||
Object.values(
|
||||
(
|
||||
await effects.getHostInfo({
|
||||
hostId: volumeMount["interface-id"],
|
||||
})
|
||||
)?.hostnameInfo || {},
|
||||
)
|
||||
.flatMap((h) => h)
|
||||
.flatMap((h) => (h.kind === "onion" ? [h.hostname.value] : [])),
|
||||
).values(),
|
||||
]
|
||||
const certChain = await effects.getSslCertificate({
|
||||
packageId: null,
|
||||
hostId: volumeMount["interface-id"],
|
||||
algorithm: null,
|
||||
hostnames,
|
||||
})
|
||||
const key = await effects.getSslKey({
|
||||
packageId: null,
|
||||
hostId: volumeMount["interface-id"],
|
||||
algorithm: null,
|
||||
hostnames,
|
||||
})
|
||||
await fs.writeFile(
|
||||
`${path}/${volumeMount["interface-id"]}.cert.pem`,
|
||||
@@ -58,17 +68,19 @@ export class DockerProcedureContainer {
|
||||
key,
|
||||
)
|
||||
} else if (volumeMount.type === "pointer") {
|
||||
await effects.mount({
|
||||
location: path,
|
||||
target: {
|
||||
packageId: volumeMount["package-id"],
|
||||
subpath: volumeMount.path,
|
||||
readonly: volumeMount.readonly,
|
||||
volumeId: volumeMount["volume-id"],
|
||||
},
|
||||
})
|
||||
await effects
|
||||
.mount({
|
||||
location: path,
|
||||
target: {
|
||||
packageId: volumeMount["package-id"],
|
||||
subpath: volumeMount.path,
|
||||
readonly: volumeMount.readonly,
|
||||
volumeId: volumeMount["volume-id"],
|
||||
},
|
||||
})
|
||||
.catch(console.warn)
|
||||
} else if (volumeMount.type === "backup") {
|
||||
throw new Error("TODO")
|
||||
await overlay.mount({ type: "backup", subpath: null }, mounts[mount])
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -84,10 +96,19 @@ export class DockerProcedureContainer {
|
||||
}
|
||||
}
|
||||
|
||||
async execSpawn(commands: string[]) {
|
||||
async execFail(commands: string[], timeoutMs: number | null) {
|
||||
try {
|
||||
const spawned = await this.overlay.spawn(commands)
|
||||
return spawned
|
||||
const res = await this.overlay.exec(commands, {}, timeoutMs)
|
||||
if (res.exitCode !== 0) {
|
||||
const codeOrSignal =
|
||||
res.exitCode !== null
|
||||
? `code ${res.exitCode}`
|
||||
: `signal ${res.exitSignal}`
|
||||
throw new Error(
|
||||
`Process exited with ${codeOrSignal}: ${res.stderr.toString()}`,
|
||||
)
|
||||
}
|
||||
return res
|
||||
} finally {
|
||||
await this.overlay.destroy()
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { PolyfillEffects } from "./polyfillEffects"
|
||||
import { polyfillEffects } from "./polyfillEffects"
|
||||
import { DockerProcedureContainer } from "./DockerProcedureContainer"
|
||||
import { SystemForEmbassy } from "."
|
||||
import { HostSystemStartOs } from "../../HostSystemStartOs"
|
||||
import { Daemons, T, daemons } from "@start9labs/start-sdk"
|
||||
import { T, utils } from "@start9labs/start-sdk"
|
||||
import { Daemon } from "@start9labs/start-sdk/cjs/lib/mainFn/Daemon"
|
||||
import { Effects } from "../../../Models/Effects"
|
||||
import { off } from "node:process"
|
||||
|
||||
const EMBASSY_HEALTH_INTERVAL = 15 * 1000
|
||||
const EMBASSY_PROPERTIES_LOOP = 30 * 1000
|
||||
@@ -12,25 +14,28 @@ const EMBASSY_PROPERTIES_LOOP = 30 * 1000
|
||||
* Also, this has an ability to clean itself up too if need be.
|
||||
*/
|
||||
export class MainLoop {
|
||||
private healthLoops:
|
||||
| {
|
||||
name: string
|
||||
interval: NodeJS.Timeout
|
||||
}[]
|
||||
| undefined
|
||||
private healthLoops?: {
|
||||
name: string
|
||||
interval: NodeJS.Timeout
|
||||
}[]
|
||||
|
||||
private mainEvent:
|
||||
| Promise<{
|
||||
daemon: T.DaemonReturned
|
||||
wait: Promise<unknown>
|
||||
}>
|
||||
| undefined
|
||||
constructor(
|
||||
private mainEvent?: {
|
||||
daemon: Daemon
|
||||
}
|
||||
|
||||
private constructor(
|
||||
readonly system: SystemForEmbassy,
|
||||
readonly effects: HostSystemStartOs,
|
||||
) {
|
||||
this.healthLoops = this.constructHealthLoops()
|
||||
this.mainEvent = this.constructMainEvent()
|
||||
readonly effects: Effects,
|
||||
) {}
|
||||
|
||||
static async of(
|
||||
system: SystemForEmbassy,
|
||||
effects: Effects,
|
||||
): Promise<MainLoop> {
|
||||
const res = new MainLoop(system, effects)
|
||||
res.healthLoops = res.constructHealthLoops()
|
||||
res.mainEvent = await res.constructMainEvent()
|
||||
return res
|
||||
}
|
||||
|
||||
private async constructMainEvent() {
|
||||
@@ -40,44 +45,76 @@ export class MainLoop {
|
||||
...system.manifest.main.args,
|
||||
]
|
||||
|
||||
await this.setupInterfaces(effects)
|
||||
await effects.setMainStatus({ status: "running" })
|
||||
const jsMain = (this.system.moduleCode as any)?.jsMain
|
||||
const dockerProcedureContainer = await DockerProcedureContainer.of(
|
||||
effects,
|
||||
this.system.manifest.id,
|
||||
this.system.manifest.main,
|
||||
this.system.manifest.volumes,
|
||||
)
|
||||
if (jsMain) {
|
||||
const daemons = Daemons.of({
|
||||
effects,
|
||||
started: async (_) => {},
|
||||
healthReceipts: [],
|
||||
})
|
||||
throw new Error("todo")
|
||||
// return {
|
||||
// daemon,
|
||||
// wait: daemon.wait().finally(() => {
|
||||
// this.clean()
|
||||
// effects.setMainStatus({ status: "stopped" })
|
||||
// }),
|
||||
// }
|
||||
throw new Error("Unreachable")
|
||||
}
|
||||
const daemon = await daemons.runDaemon()(
|
||||
const daemon = await Daemon.of()(
|
||||
this.effects,
|
||||
this.system.manifest.main.image,
|
||||
{ id: this.system.manifest.main.image },
|
||||
currentCommand,
|
||||
{
|
||||
overlay: dockerProcedureContainer.overlay,
|
||||
sigtermTimeout: utils.inMs(
|
||||
this.system.manifest.main["sigterm-timeout"],
|
||||
),
|
||||
},
|
||||
)
|
||||
daemon.start()
|
||||
return {
|
||||
daemon,
|
||||
wait: daemon.wait().finally(() => {
|
||||
this.clean()
|
||||
effects
|
||||
.setMainStatus({ status: "stopped" })
|
||||
.catch((e) => console.error("Could not set the status to stopped"))
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
private async setupInterfaces(effects: T.Effects) {
|
||||
for (const interfaceId in this.system.manifest.interfaces) {
|
||||
const iface = this.system.manifest.interfaces[interfaceId]
|
||||
const internalPorts = new Set<number>()
|
||||
for (const port of Object.values(
|
||||
iface["tor-config"]?.["port-mapping"] || {},
|
||||
)) {
|
||||
internalPorts.add(parseInt(port))
|
||||
}
|
||||
for (const port of Object.values(iface["lan-config"] || {})) {
|
||||
internalPorts.add(port.internal)
|
||||
}
|
||||
for (const internalPort of internalPorts) {
|
||||
const torConf = Object.entries(
|
||||
iface["tor-config"]?.["port-mapping"] || {},
|
||||
)
|
||||
.map(([external, internal]) => ({
|
||||
internal: parseInt(internal),
|
||||
external: parseInt(external),
|
||||
}))
|
||||
.find((conf) => conf.internal == internalPort)
|
||||
const lanConf = Object.entries(iface["lan-config"] || {})
|
||||
.map(([external, conf]) => ({
|
||||
external: parseInt(external),
|
||||
...conf,
|
||||
}))
|
||||
.find((conf) => conf.internal == internalPort)
|
||||
await effects.bind({
|
||||
kind: "multi",
|
||||
id: interfaceId,
|
||||
internalPort,
|
||||
preferredExternalPort: torConf?.external || internalPort,
|
||||
secure: null,
|
||||
addSsl: lanConf?.ssl
|
||||
? {
|
||||
preferredExternalPort: lanConf.external,
|
||||
alpn: { specified: ["http/1.1"] },
|
||||
}
|
||||
: null,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -86,7 +123,8 @@ export class MainLoop {
|
||||
const main = await mainEvent
|
||||
delete this.mainEvent
|
||||
delete this.healthLoops
|
||||
if (mainEvent) await main?.daemon.term()
|
||||
await main?.daemon.stop().catch((e) => console.error(e))
|
||||
this.effects.setMainStatus({ status: "stopped" })
|
||||
if (healthLoops) healthLoops.forEach((x) => clearInterval(x.interval))
|
||||
}
|
||||
|
||||
@@ -102,14 +140,24 @@ export class MainLoop {
|
||||
if (actionProcedure.type === "docker") {
|
||||
const container = await DockerProcedureContainer.of(
|
||||
effects,
|
||||
manifest.id,
|
||||
actionProcedure,
|
||||
manifest.volumes,
|
||||
)
|
||||
const executed = await container.execSpawn([
|
||||
const executed = await container.exec([
|
||||
actionProcedure.entrypoint,
|
||||
...actionProcedure.args,
|
||||
JSON.stringify(timeChanged),
|
||||
])
|
||||
if (executed.exitCode === 0) {
|
||||
await effects.setHealth({
|
||||
id: healthId,
|
||||
name: value.name,
|
||||
result: "success",
|
||||
message: actionProcedure["success-message"],
|
||||
})
|
||||
return
|
||||
}
|
||||
if (executed.exitCode === 59) {
|
||||
await effects.setHealth({
|
||||
id: healthId,
|
||||
@@ -173,7 +221,7 @@ export class MainLoop {
|
||||
}
|
||||
|
||||
const result = await method(
|
||||
new PolyfillEffects(effects, this.system.manifest),
|
||||
polyfillEffects(effects, this.system.manifest),
|
||||
timeChanged,
|
||||
)
|
||||
|
||||
|
||||
@@ -0,0 +1,387 @@
|
||||
export default {
|
||||
"peer-tor-address": {
|
||||
name: "Peer Tor Address",
|
||||
description: "The Tor address of the peer interface",
|
||||
type: "pointer",
|
||||
subtype: "package",
|
||||
"package-id": "bitcoind",
|
||||
target: "tor-address",
|
||||
interface: "peer",
|
||||
},
|
||||
"rpc-tor-address": {
|
||||
name: "RPC Tor Address",
|
||||
description: "The Tor address of the RPC interface",
|
||||
type: "pointer",
|
||||
subtype: "package",
|
||||
"package-id": "bitcoind",
|
||||
target: "tor-address",
|
||||
interface: "rpc",
|
||||
},
|
||||
rpc: {
|
||||
type: "object",
|
||||
name: "RPC Settings",
|
||||
description: "RPC configuration options.",
|
||||
spec: {
|
||||
enable: {
|
||||
type: "boolean",
|
||||
name: "Enable",
|
||||
description: "Allow remote RPC requests.",
|
||||
default: true,
|
||||
},
|
||||
username: {
|
||||
type: "string",
|
||||
nullable: false,
|
||||
name: "Username",
|
||||
description: "The username for connecting to Bitcoin over RPC.",
|
||||
warning:
|
||||
"You will need to restart all services that depend on Bitcoin.",
|
||||
default: "bitcoin",
|
||||
masked: true,
|
||||
pattern: "^[a-zA-Z0-9_]+$",
|
||||
"pattern-description": "Must be alphanumeric (can contain underscore).",
|
||||
},
|
||||
password: {
|
||||
type: "string",
|
||||
nullable: false,
|
||||
name: "RPC Password",
|
||||
description: "The password for connecting to Bitcoin over RPC.",
|
||||
warning:
|
||||
"You will need to restart all services that depend on Bitcoin.",
|
||||
default: {
|
||||
charset: "a-z,2-7",
|
||||
len: 20,
|
||||
},
|
||||
pattern: "^[a-zA-Z0-9_]+$",
|
||||
"pattern-description": "Must be alphanumeric (can contain underscore).",
|
||||
copyable: true,
|
||||
masked: true,
|
||||
},
|
||||
advanced: {
|
||||
type: "object",
|
||||
name: "Advanced",
|
||||
description: "Advanced RPC Settings",
|
||||
spec: {
|
||||
auth: {
|
||||
name: "Authorization",
|
||||
description:
|
||||
"Username and hashed password for JSON-RPC connections. RPC clients connect using the usual http basic authentication.",
|
||||
type: "list",
|
||||
subtype: "string",
|
||||
default: [],
|
||||
spec: {
|
||||
pattern: "^[a-zA-Z0-9_-]+:([0-9a-fA-F]{2})+\\$([0-9a-fA-F]{2})+$",
|
||||
"pattern-description":
|
||||
'Each item must be of the form "<USERNAME>:<SALT>$<HASH>".',
|
||||
},
|
||||
range: "[0,*)",
|
||||
},
|
||||
servertimeout: {
|
||||
name: "Rpc Server Timeout",
|
||||
description:
|
||||
"Number of seconds after which an uncompleted RPC call will time out.",
|
||||
type: "number",
|
||||
nullable: false,
|
||||
range: "[5,300]",
|
||||
integral: true,
|
||||
units: "seconds",
|
||||
default: 30,
|
||||
},
|
||||
threads: {
|
||||
name: "Threads",
|
||||
description:
|
||||
"Set the number of threads for handling RPC calls. You may wish to increase this if you are making lots of calls via an integration.",
|
||||
type: "number",
|
||||
nullable: false,
|
||||
default: 16,
|
||||
range: "[1,64]",
|
||||
integral: true,
|
||||
units: undefined,
|
||||
},
|
||||
workqueue: {
|
||||
name: "Work Queue",
|
||||
description:
|
||||
"Set the depth of the work queue to service RPC calls. Determines how long the backlog of RPC requests can get before it just rejects new ones.",
|
||||
type: "number",
|
||||
nullable: false,
|
||||
default: 128,
|
||||
range: "[8,256]",
|
||||
integral: true,
|
||||
units: "requests",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"zmq-enabled": {
|
||||
type: "boolean",
|
||||
name: "ZeroMQ Enabled",
|
||||
description:
|
||||
"The ZeroMQ interface is useful for some applications which might require data related to block and transaction events from Bitcoin Core. For example, LND requires ZeroMQ be enabled for LND to get the latest block data",
|
||||
default: true,
|
||||
},
|
||||
txindex: {
|
||||
type: "boolean",
|
||||
name: "Transaction Index",
|
||||
description:
|
||||
"By enabling Transaction Index (txindex) Bitcoin Core will build a complete transaction index. This allows Bitcoin Core to access any transaction with commands like `gettransaction`.",
|
||||
default: true,
|
||||
},
|
||||
coinstatsindex: {
|
||||
type: "boolean",
|
||||
name: "Coinstats Index",
|
||||
description:
|
||||
"Enabling Coinstats Index reduces the time for the gettxoutsetinfo RPC to complete at the cost of using additional disk space",
|
||||
default: false,
|
||||
},
|
||||
wallet: {
|
||||
type: "object",
|
||||
name: "Wallet",
|
||||
description: "Wallet Settings",
|
||||
spec: {
|
||||
enable: {
|
||||
name: "Enable Wallet",
|
||||
description: "Load the wallet and enable wallet RPC calls.",
|
||||
type: "boolean",
|
||||
default: true,
|
||||
},
|
||||
avoidpartialspends: {
|
||||
name: "Avoid Partial Spends",
|
||||
description:
|
||||
"Group outputs by address, selecting all or none, instead of selecting on a per-output basis. This improves privacy at the expense of higher transaction fees.",
|
||||
type: "boolean",
|
||||
default: true,
|
||||
},
|
||||
discardfee: {
|
||||
name: "Discard Change Tolerance",
|
||||
description:
|
||||
"The fee rate (in BTC/kB) that indicates your tolerance for discarding change by adding it to the fee.",
|
||||
type: "number",
|
||||
nullable: false,
|
||||
default: 0.0001,
|
||||
range: "[0,.01]",
|
||||
integral: false,
|
||||
units: "BTC/kB",
|
||||
},
|
||||
},
|
||||
},
|
||||
advanced: {
|
||||
type: "object",
|
||||
name: "Advanced",
|
||||
description: "Advanced Settings",
|
||||
spec: {
|
||||
mempool: {
|
||||
type: "object",
|
||||
name: "Mempool",
|
||||
description: "Mempool Settings",
|
||||
spec: {
|
||||
persistmempool: {
|
||||
type: "boolean",
|
||||
name: "Persist Mempool",
|
||||
description: "Save the mempool on shutdown and load on restart.",
|
||||
default: true,
|
||||
},
|
||||
maxmempool: {
|
||||
type: "number",
|
||||
nullable: false,
|
||||
name: "Max Mempool Size",
|
||||
description:
|
||||
"Keep the transaction memory pool below <n> megabytes.",
|
||||
range: "[1,*)",
|
||||
integral: true,
|
||||
units: "MiB",
|
||||
default: 300,
|
||||
},
|
||||
mempoolexpiry: {
|
||||
type: "number",
|
||||
nullable: false,
|
||||
name: "Mempool Expiration",
|
||||
description:
|
||||
"Do not keep transactions in the mempool longer than <n> hours.",
|
||||
range: "[1,*)",
|
||||
integral: true,
|
||||
units: "Hr",
|
||||
default: 336,
|
||||
},
|
||||
mempoolfullrbf: {
|
||||
name: "Enable Full RBF",
|
||||
description:
|
||||
"Policy for your node to use for relaying and mining unconfirmed transactions. For details, see https://github.com/bitcoin/bitcoin/blob/master/doc/release-notes/release-notes-24.0.1.md#notice-of-new-option-for-transaction-replacement-policies",
|
||||
type: "boolean",
|
||||
default: true,
|
||||
},
|
||||
permitbaremultisig: {
|
||||
type: "boolean",
|
||||
name: "Permit Bare Multisig",
|
||||
description: "Relay non-P2SH multisig transactions",
|
||||
default: true,
|
||||
},
|
||||
datacarrier: {
|
||||
type: "boolean",
|
||||
name: "Relay OP_RETURN Transactions",
|
||||
description: "Relay transactions with OP_RETURN outputs",
|
||||
default: true,
|
||||
},
|
||||
datacarriersize: {
|
||||
type: "number",
|
||||
nullable: false,
|
||||
name: "Max OP_RETURN Size",
|
||||
description: "Maximum size of data in OP_RETURN outputs to relay",
|
||||
range: "[0,10000]",
|
||||
integral: true,
|
||||
units: "bytes",
|
||||
default: 83,
|
||||
},
|
||||
},
|
||||
},
|
||||
peers: {
|
||||
type: "object",
|
||||
name: "Peers",
|
||||
description: "Peer Connection Settings",
|
||||
spec: {
|
||||
listen: {
|
||||
type: "boolean",
|
||||
name: "Make Public",
|
||||
description:
|
||||
"Allow other nodes to find your server on the network.",
|
||||
default: true,
|
||||
},
|
||||
onlyconnect: {
|
||||
type: "boolean",
|
||||
name: "Disable Peer Discovery",
|
||||
description: "Only connect to specified peers.",
|
||||
default: false,
|
||||
},
|
||||
onlyonion: {
|
||||
type: "boolean",
|
||||
name: "Disable Clearnet",
|
||||
description: "Only connect to peers over Tor.",
|
||||
default: false,
|
||||
},
|
||||
v2transport: {
|
||||
type: "boolean",
|
||||
name: "Use V2 P2P Transport Protocol",
|
||||
description:
|
||||
"Enable or disable the use of BIP324 V2 P2P transport protocol.",
|
||||
default: false,
|
||||
},
|
||||
addnode: {
|
||||
name: "Add Nodes",
|
||||
description: "Add addresses of nodes to connect to.",
|
||||
type: "list",
|
||||
subtype: "object",
|
||||
range: "[0,*)",
|
||||
default: [],
|
||||
spec: {
|
||||
spec: {
|
||||
hostname: {
|
||||
type: "string",
|
||||
nullable: false,
|
||||
name: "Hostname",
|
||||
description: "Domain or IP address of bitcoin peer",
|
||||
pattern:
|
||||
"(^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$)|((^(([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]))$)|(^[a-z2-7]{16}\\.onion$)|(^([a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?\\.)+[a-z0-9][a-z0-9-]{0,61}[a-z0-9]$))",
|
||||
"pattern-description":
|
||||
"Must be either a domain name, or an IPv4 or IPv6 address. Do not include protocol scheme (eg 'http://') or port.",
|
||||
},
|
||||
port: {
|
||||
type: "number",
|
||||
nullable: true,
|
||||
name: "Port",
|
||||
description:
|
||||
"Port that peer is listening on for inbound p2p connections",
|
||||
range: "[0,65535]",
|
||||
integral: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
pruning: {
|
||||
type: "union",
|
||||
name: "Pruning Settings",
|
||||
description:
|
||||
"Blockchain Pruning Options\nReduce the blockchain size on disk\n",
|
||||
warning:
|
||||
"Disabling pruning will convert your node into a full archival node. This requires a resync of the entire blockchain, a process that may take several days.\n",
|
||||
tag: {
|
||||
id: "mode",
|
||||
name: "Pruning Mode",
|
||||
description:
|
||||
"- Disabled: Disable pruning\n- Automatic: Limit blockchain size on disk to a certain number of megabytes\n",
|
||||
"variant-names": {
|
||||
disabled: "Disabled",
|
||||
automatic: "Automatic",
|
||||
},
|
||||
},
|
||||
variants: {
|
||||
disabled: {},
|
||||
automatic: {
|
||||
size: {
|
||||
type: "number",
|
||||
nullable: false,
|
||||
name: "Max Chain Size",
|
||||
description: "Limit of blockchain size on disk.",
|
||||
warning:
|
||||
"Increasing this value will require re-syncing your node.",
|
||||
default: 550,
|
||||
range: "[550,1000000)",
|
||||
integral: true,
|
||||
units: "MiB",
|
||||
},
|
||||
},
|
||||
},
|
||||
default: "disabled",
|
||||
},
|
||||
dbcache: {
|
||||
type: "number",
|
||||
nullable: true,
|
||||
name: "Database Cache",
|
||||
description:
|
||||
"How much RAM to allocate for caching the TXO set. Higher values improve syncing performance, but increase your chance of using up all your system's memory or corrupting your database in the event of an ungraceful shutdown. Set this high but comfortably below your system's total RAM during IBD, then turn down to 450 (or leave blank) once the sync completes.",
|
||||
warning:
|
||||
"WARNING: Increasing this value results in a higher chance of ungraceful shutdowns, which can leave your node unusable if it happens during the initial block download. Use this setting with caution. Be sure to set this back to the default (450 or leave blank) once your node is synced. DO NOT press the STOP button if your dbcache is large. Instead, set this number back to the default, hit save, and wait for bitcoind to restart on its own.",
|
||||
range: "(0,*)",
|
||||
integral: true,
|
||||
units: "MiB",
|
||||
},
|
||||
blockfilters: {
|
||||
type: "object",
|
||||
name: "Block Filters",
|
||||
description: "Settings for storing and serving compact block filters",
|
||||
spec: {
|
||||
blockfilterindex: {
|
||||
type: "boolean",
|
||||
name: "Compute Compact Block Filters (BIP158)",
|
||||
description:
|
||||
"Generate Compact Block Filters during initial sync (IBD) to enable 'getblockfilter' RPC. This is useful if dependent services need block filters to efficiently scan for addresses/transactions etc.",
|
||||
default: true,
|
||||
},
|
||||
peerblockfilters: {
|
||||
type: "boolean",
|
||||
name: "Serve Compact Block Filters to Peers (BIP157)",
|
||||
description:
|
||||
"Serve Compact Block Filters as a peer service to other nodes on the network. This is useful if you wish to connect an SPV client to your node to make it efficient to scan transactions without having to download all block data. 'Compute Compact Block Filters (BIP158)' is required.",
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
bloomfilters: {
|
||||
type: "object",
|
||||
name: "Bloom Filters (BIP37)",
|
||||
description: "Setting for serving Bloom Filters",
|
||||
spec: {
|
||||
peerbloomfilters: {
|
||||
type: "boolean",
|
||||
name: "Serve Bloom Filters to Peers",
|
||||
description:
|
||||
"Peers have the option of setting filters on each connection they make after the version handshake has completed. Bloom filters are for clients implementing SPV (Simplified Payment Verification) that want to check that block headers connect together correctly, without needing to verify the full blockchain. The client must trust that the transactions in the chain are in fact valid. It is highly recommended AGAINST using for anything except Bisq integration.",
|
||||
warning:
|
||||
"This is ONLY for use with Bisq integration, please use Block Filters for all other applications.",
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
export default {
|
||||
homepage: {
|
||||
name: "Homepage",
|
||||
description:
|
||||
"The page that will be displayed when your Start9 Pages .onion address is visited. Since this page is technically publicly accessible, you can choose to which type of page to display.",
|
||||
type: "union",
|
||||
default: "welcome",
|
||||
tag: {
|
||||
id: "type",
|
||||
name: "Type",
|
||||
"variant-names": {
|
||||
welcome: "Welcome",
|
||||
index: "Table of Contents",
|
||||
"web-page": "Web Page",
|
||||
redirect: "Redirect",
|
||||
},
|
||||
},
|
||||
variants: {
|
||||
welcome: {},
|
||||
index: {},
|
||||
"web-page": {
|
||||
source: {
|
||||
name: "Folder Location",
|
||||
description: "The service that contains your website files.",
|
||||
type: "enum",
|
||||
values: ["filebrowser", "nextcloud"],
|
||||
"value-names": {},
|
||||
default: "nextcloud",
|
||||
},
|
||||
folder: {
|
||||
type: "string",
|
||||
name: "Folder Path",
|
||||
placeholder: "e.g. websites/resume",
|
||||
description:
|
||||
'The path to the folder that contains the static files of your website. For example, a value of "projects/resume" would tell Start9 Pages to look for that folder path in the selected service.',
|
||||
pattern:
|
||||
"^(\\.|[a-zA-Z0-9_ -][a-zA-Z0-9_ .-]*|([a-zA-Z0-9_ .-][a-zA-Z0-9_ -]+\\.*)+)(/[a-zA-Z0-9_ -][a-zA-Z0-9_ .-]*|/([a-zA-Z0-9_ .-][a-zA-Z0-9_ -]+\\.*)+)*/?$",
|
||||
"pattern-description": "Must be a valid relative file path",
|
||||
nullable: false,
|
||||
},
|
||||
},
|
||||
redirect: {
|
||||
target: {
|
||||
type: "string",
|
||||
name: "Target Subdomain",
|
||||
description:
|
||||
"The name of the subdomain to redirect users to. This must be a valid subdomain site within your Start9 Pages.",
|
||||
pattern: "^[a-z-]+$",
|
||||
"pattern-description":
|
||||
"May contain only lowercase characters and hyphens.",
|
||||
nullable: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
subdomains: {
|
||||
type: "list",
|
||||
name: "Subdomains",
|
||||
description: "The websites you want to serve.",
|
||||
default: [],
|
||||
range: "[0, *)",
|
||||
subtype: "object",
|
||||
spec: {
|
||||
"unique-by": "name",
|
||||
"display-as": "{{name}}",
|
||||
spec: {
|
||||
name: {
|
||||
type: "string",
|
||||
nullable: false,
|
||||
name: "Subdomain name",
|
||||
description:
|
||||
'The subdomain of your Start9 Pages .onion address to host the website on. For example, a value of "me" would produce a website hosted at http://me.xxxxxx.onion.',
|
||||
pattern: "^[a-z-]+$",
|
||||
"pattern-description":
|
||||
"May contain only lowercase characters and hyphens",
|
||||
},
|
||||
settings: {
|
||||
type: "union",
|
||||
name: "Settings",
|
||||
description:
|
||||
"The desired behavior you want to occur when the subdomain is visited. You can either redirect to another subdomain, or load a stored web page.",
|
||||
default: "web-page",
|
||||
tag: {
|
||||
id: "type",
|
||||
name: "Type",
|
||||
"variant-names": { "web-page": "Web Page", redirect: "Redirect" },
|
||||
},
|
||||
variants: {
|
||||
"web-page": {
|
||||
source: {
|
||||
name: "Folder Location",
|
||||
description: "The service that contains your website files.",
|
||||
type: "enum",
|
||||
values: ["filebrowser", "nextcloud"],
|
||||
"value-names": {},
|
||||
default: "nextcloud",
|
||||
},
|
||||
folder: {
|
||||
type: "string",
|
||||
name: "Folder Path",
|
||||
placeholder: "e.g. websites/resume",
|
||||
description:
|
||||
'The path to the folder that contains the website files. For example, a value of "projects/resume" would tell Start9 Pages to look for that folder path in the selected service.',
|
||||
pattern:
|
||||
"^(\\.|[a-zA-Z0-9_ -][a-zA-Z0-9_ .-]*|([a-zA-Z0-9_ .-][a-zA-Z0-9_ -]+\\.*)+)(/[a-zA-Z0-9_ -][a-zA-Z0-9_ .-]*|/([a-zA-Z0-9_ .-][a-zA-Z0-9_ -]+\\.*)+)*/?$",
|
||||
"pattern-description": "Must be a valid relative file path",
|
||||
nullable: false,
|
||||
},
|
||||
},
|
||||
redirect: {
|
||||
target: {
|
||||
type: "string",
|
||||
name: "Target Subdomain",
|
||||
description:
|
||||
"The subdomain of your Start9 Pages .onion address to redirect to. This should be the name of another subdomain on Start9 Pages. Leave empty to redirect to the homepage.",
|
||||
pattern: "^[a-z-]+$",
|
||||
"pattern-description":
|
||||
"May contain only lowercase characters and hyphens.",
|
||||
nullable: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
export default {
|
||||
"tor-address": {
|
||||
name: "Tor Address",
|
||||
description: "The Tor address of the network interface",
|
||||
type: "pointer",
|
||||
subtype: "package",
|
||||
"package-id": "nostr-wallet-connect",
|
||||
target: "tor-address",
|
||||
interface: "main",
|
||||
},
|
||||
"lan-address": {
|
||||
name: "LAN Address",
|
||||
description: "The LAN address of the network interface",
|
||||
type: "pointer",
|
||||
subtype: "package",
|
||||
"package-id": "nostr-wallet-connect",
|
||||
target: "lan-address",
|
||||
interface: "main",
|
||||
},
|
||||
"nostr-relay": {
|
||||
type: "string",
|
||||
name: "Nostr Relay",
|
||||
default: "wss://relay.getalby.com/v1",
|
||||
description: "The Nostr Relay to use for Nostr Wallet Connect connections",
|
||||
copyable: true,
|
||||
nullable: false,
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
export default {
|
||||
"instance-name": {
|
||||
type: "string",
|
||||
name: "SearXNG Instance Name",
|
||||
description:
|
||||
"Enter a name for your SearXNG instance. This is the name that will be listed if you want to share your SearXNG engine publicly.",
|
||||
nullable: false,
|
||||
default: "My SearXNG Engine",
|
||||
placeholder: "Uncle Jim SearXNG Engine",
|
||||
},
|
||||
"tor-url": {
|
||||
name: "Enable Tor address as the base URL",
|
||||
description:
|
||||
"Activates the utilization of a .onion address as the primary URL, particularly beneficial for publicly hosted instances over the Tor network.",
|
||||
type: "boolean",
|
||||
default: false,
|
||||
},
|
||||
"enable-metrics": {
|
||||
name: "Enable Stats",
|
||||
description:
|
||||
"Your SearXNG instance will collect anonymous stats about its own usage and performance. You can view these metrics by appending `/stats` or `/stats/errors` to your SearXNG URL.",
|
||||
type: "boolean",
|
||||
default: true,
|
||||
}, //,
|
||||
// "email-address": {
|
||||
// "type": "string",
|
||||
// "name": "Email Address",
|
||||
// "description": "Your Email address - required to create an SSL certificate.",
|
||||
// "nullable": false,
|
||||
// "default": "youremail@domain.com",
|
||||
// },
|
||||
// "public-host": {
|
||||
// "type": "string",
|
||||
// "name": "Public Domain Name",
|
||||
// "description": "Enter a domain name here if you want to share your SearXNG engine publicly. You will also need to modify your domain name's DNS settings to point to your Start9 server.",
|
||||
// "nullable": true,
|
||||
// "placeholder": "https://search.mydomain.com"
|
||||
// }
|
||||
}
|
||||
@@ -0,0 +1,791 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`transformConfigSpec transformConfigSpec(bitcoind) 1`] = `
|
||||
{
|
||||
"advanced": {
|
||||
"description": "Advanced Settings",
|
||||
"name": "Advanced",
|
||||
"spec": {
|
||||
"blockfilters": {
|
||||
"description": "Settings for storing and serving compact block filters",
|
||||
"name": "Block Filters",
|
||||
"spec": {
|
||||
"blockfilterindex": {
|
||||
"default": true,
|
||||
"description": "Generate Compact Block Filters during initial sync (IBD) to enable 'getblockfilter' RPC. This is useful if dependent services need block filters to efficiently scan for addresses/transactions etc.",
|
||||
"disabled": false,
|
||||
"immutable": false,
|
||||
"name": "Compute Compact Block Filters (BIP158)",
|
||||
"type": "toggle",
|
||||
"warning": null,
|
||||
},
|
||||
"peerblockfilters": {
|
||||
"default": false,
|
||||
"description": "Serve Compact Block Filters as a peer service to other nodes on the network. This is useful if you wish to connect an SPV client to your node to make it efficient to scan transactions without having to download all block data. 'Compute Compact Block Filters (BIP158)' is required.",
|
||||
"disabled": false,
|
||||
"immutable": false,
|
||||
"name": "Serve Compact Block Filters to Peers (BIP157)",
|
||||
"type": "toggle",
|
||||
"warning": null,
|
||||
},
|
||||
},
|
||||
"type": "object",
|
||||
"warning": null,
|
||||
},
|
||||
"bloomfilters": {
|
||||
"description": "Setting for serving Bloom Filters",
|
||||
"name": "Bloom Filters (BIP37)",
|
||||
"spec": {
|
||||
"peerbloomfilters": {
|
||||
"default": false,
|
||||
"description": "Peers have the option of setting filters on each connection they make after the version handshake has completed. Bloom filters are for clients implementing SPV (Simplified Payment Verification) that want to check that block headers connect together correctly, without needing to verify the full blockchain. The client must trust that the transactions in the chain are in fact valid. It is highly recommended AGAINST using for anything except Bisq integration.",
|
||||
"disabled": false,
|
||||
"immutable": false,
|
||||
"name": "Serve Bloom Filters to Peers",
|
||||
"type": "toggle",
|
||||
"warning": "This is ONLY for use with Bisq integration, please use Block Filters for all other applications.",
|
||||
},
|
||||
},
|
||||
"type": "object",
|
||||
"warning": null,
|
||||
},
|
||||
"dbcache": {
|
||||
"default": null,
|
||||
"description": "How much RAM to allocate for caching the TXO set. Higher values improve syncing performance, but increase your chance of using up all your system's memory or corrupting your database in the event of an ungraceful shutdown. Set this high but comfortably below your system's total RAM during IBD, then turn down to 450 (or leave blank) once the sync completes.",
|
||||
"disabled": false,
|
||||
"immutable": false,
|
||||
"integer": true,
|
||||
"max": null,
|
||||
"min": null,
|
||||
"name": "Database Cache",
|
||||
"placeholder": null,
|
||||
"required": false,
|
||||
"step": null,
|
||||
"type": "number",
|
||||
"units": "MiB",
|
||||
"warning": "WARNING: Increasing this value results in a higher chance of ungraceful shutdowns, which can leave your node unusable if it happens during the initial block download. Use this setting with caution. Be sure to set this back to the default (450 or leave blank) once your node is synced. DO NOT press the STOP button if your dbcache is large. Instead, set this number back to the default, hit save, and wait for bitcoind to restart on its own.",
|
||||
},
|
||||
"mempool": {
|
||||
"description": "Mempool Settings",
|
||||
"name": "Mempool",
|
||||
"spec": {
|
||||
"datacarrier": {
|
||||
"default": true,
|
||||
"description": "Relay transactions with OP_RETURN outputs",
|
||||
"disabled": false,
|
||||
"immutable": false,
|
||||
"name": "Relay OP_RETURN Transactions",
|
||||
"type": "toggle",
|
||||
"warning": null,
|
||||
},
|
||||
"datacarriersize": {
|
||||
"default": 83,
|
||||
"description": "Maximum size of data in OP_RETURN outputs to relay",
|
||||
"disabled": false,
|
||||
"immutable": false,
|
||||
"integer": true,
|
||||
"max": 10000,
|
||||
"min": null,
|
||||
"name": "Max OP_RETURN Size",
|
||||
"placeholder": null,
|
||||
"required": true,
|
||||
"step": null,
|
||||
"type": "number",
|
||||
"units": "bytes",
|
||||
"warning": null,
|
||||
},
|
||||
"maxmempool": {
|
||||
"default": 300,
|
||||
"description": "Keep the transaction memory pool below <n> megabytes.",
|
||||
"disabled": false,
|
||||
"immutable": false,
|
||||
"integer": true,
|
||||
"max": null,
|
||||
"min": 1,
|
||||
"name": "Max Mempool Size",
|
||||
"placeholder": null,
|
||||
"required": true,
|
||||
"step": null,
|
||||
"type": "number",
|
||||
"units": "MiB",
|
||||
"warning": null,
|
||||
},
|
||||
"mempoolexpiry": {
|
||||
"default": 336,
|
||||
"description": "Do not keep transactions in the mempool longer than <n> hours.",
|
||||
"disabled": false,
|
||||
"immutable": false,
|
||||
"integer": true,
|
||||
"max": null,
|
||||
"min": 1,
|
||||
"name": "Mempool Expiration",
|
||||
"placeholder": null,
|
||||
"required": true,
|
||||
"step": null,
|
||||
"type": "number",
|
||||
"units": "Hr",
|
||||
"warning": null,
|
||||
},
|
||||
"mempoolfullrbf": {
|
||||
"default": true,
|
||||
"description": "Policy for your node to use for relaying and mining unconfirmed transactions. For details, see https://github.com/bitcoin/bitcoin/blob/master/doc/release-notes/release-notes-24.0.1.md#notice-of-new-option-for-transaction-replacement-policies",
|
||||
"disabled": false,
|
||||
"immutable": false,
|
||||
"name": "Enable Full RBF",
|
||||
"type": "toggle",
|
||||
"warning": null,
|
||||
},
|
||||
"permitbaremultisig": {
|
||||
"default": true,
|
||||
"description": "Relay non-P2SH multisig transactions",
|
||||
"disabled": false,
|
||||
"immutable": false,
|
||||
"name": "Permit Bare Multisig",
|
||||
"type": "toggle",
|
||||
"warning": null,
|
||||
},
|
||||
"persistmempool": {
|
||||
"default": true,
|
||||
"description": "Save the mempool on shutdown and load on restart.",
|
||||
"disabled": false,
|
||||
"immutable": false,
|
||||
"name": "Persist Mempool",
|
||||
"type": "toggle",
|
||||
"warning": null,
|
||||
},
|
||||
},
|
||||
"type": "object",
|
||||
"warning": null,
|
||||
},
|
||||
"peers": {
|
||||
"description": "Peer Connection Settings",
|
||||
"name": "Peers",
|
||||
"spec": {
|
||||
"addnode": {
|
||||
"default": [],
|
||||
"description": "Add addresses of nodes to connect to.",
|
||||
"disabled": false,
|
||||
"maxLength": null,
|
||||
"minLength": null,
|
||||
"name": "Add Nodes",
|
||||
"spec": {
|
||||
"displayAs": null,
|
||||
"spec": {
|
||||
"hostname": {
|
||||
"default": null,
|
||||
"description": "Domain or IP address of bitcoin peer",
|
||||
"disabled": false,
|
||||
"generate": null,
|
||||
"immutable": false,
|
||||
"inputmode": "text",
|
||||
"masked": false,
|
||||
"maxLength": null,
|
||||
"minLength": null,
|
||||
"name": "Hostname",
|
||||
"patterns": [
|
||||
{
|
||||
"description": "Must be either a domain name, or an IPv4 or IPv6 address. Do not include protocol scheme (eg 'http://') or port.",
|
||||
"regex": "(^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$)|((^(([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]))$)|(^[a-z2-7]{16}\\.onion$)|(^([a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?\\.)+[a-z0-9][a-z0-9-]{0,61}[a-z0-9]$))",
|
||||
},
|
||||
],
|
||||
"placeholder": null,
|
||||
"required": true,
|
||||
"type": "text",
|
||||
"warning": null,
|
||||
},
|
||||
"port": {
|
||||
"default": null,
|
||||
"description": "Port that peer is listening on for inbound p2p connections",
|
||||
"disabled": false,
|
||||
"immutable": false,
|
||||
"integer": true,
|
||||
"max": 65535,
|
||||
"min": null,
|
||||
"name": "Port",
|
||||
"placeholder": null,
|
||||
"required": false,
|
||||
"step": null,
|
||||
"type": "number",
|
||||
"units": null,
|
||||
"warning": null,
|
||||
},
|
||||
},
|
||||
"type": "object",
|
||||
"uniqueBy": null,
|
||||
},
|
||||
"type": "list",
|
||||
"warning": null,
|
||||
},
|
||||
"listen": {
|
||||
"default": true,
|
||||
"description": "Allow other nodes to find your server on the network.",
|
||||
"disabled": false,
|
||||
"immutable": false,
|
||||
"name": "Make Public",
|
||||
"type": "toggle",
|
||||
"warning": null,
|
||||
},
|
||||
"onlyconnect": {
|
||||
"default": false,
|
||||
"description": "Only connect to specified peers.",
|
||||
"disabled": false,
|
||||
"immutable": false,
|
||||
"name": "Disable Peer Discovery",
|
||||
"type": "toggle",
|
||||
"warning": null,
|
||||
},
|
||||
"onlyonion": {
|
||||
"default": false,
|
||||
"description": "Only connect to peers over Tor.",
|
||||
"disabled": false,
|
||||
"immutable": false,
|
||||
"name": "Disable Clearnet",
|
||||
"type": "toggle",
|
||||
"warning": null,
|
||||
},
|
||||
"v2transport": {
|
||||
"default": false,
|
||||
"description": "Enable or disable the use of BIP324 V2 P2P transport protocol.",
|
||||
"disabled": false,
|
||||
"immutable": false,
|
||||
"name": "Use V2 P2P Transport Protocol",
|
||||
"type": "toggle",
|
||||
"warning": null,
|
||||
},
|
||||
},
|
||||
"type": "object",
|
||||
"warning": null,
|
||||
},
|
||||
"pruning": {
|
||||
"default": "disabled",
|
||||
"description": "- Disabled: Disable pruning
|
||||
- Automatic: Limit blockchain size on disk to a certain number of megabytes
|
||||
",
|
||||
"disabled": false,
|
||||
"immutable": false,
|
||||
"name": "Pruning Mode",
|
||||
"required": true,
|
||||
"type": "union",
|
||||
"variants": {
|
||||
"automatic": {
|
||||
"name": "Automatic",
|
||||
"spec": {
|
||||
"size": {
|
||||
"default": 550,
|
||||
"description": "Limit of blockchain size on disk.",
|
||||
"disabled": false,
|
||||
"immutable": false,
|
||||
"integer": true,
|
||||
"max": 999999,
|
||||
"min": 550,
|
||||
"name": "Max Chain Size",
|
||||
"placeholder": null,
|
||||
"required": true,
|
||||
"step": null,
|
||||
"type": "number",
|
||||
"units": "MiB",
|
||||
"warning": "Increasing this value will require re-syncing your node.",
|
||||
},
|
||||
},
|
||||
},
|
||||
"disabled": {
|
||||
"name": "Disabled",
|
||||
"spec": {},
|
||||
},
|
||||
},
|
||||
"warning": null,
|
||||
},
|
||||
},
|
||||
"type": "object",
|
||||
"warning": null,
|
||||
},
|
||||
"coinstatsindex": {
|
||||
"default": false,
|
||||
"description": "Enabling Coinstats Index reduces the time for the gettxoutsetinfo RPC to complete at the cost of using additional disk space",
|
||||
"disabled": false,
|
||||
"immutable": false,
|
||||
"name": "Coinstats Index",
|
||||
"type": "toggle",
|
||||
"warning": null,
|
||||
},
|
||||
"rpc": {
|
||||
"description": "RPC configuration options.",
|
||||
"name": "RPC Settings",
|
||||
"spec": {
|
||||
"advanced": {
|
||||
"description": "Advanced RPC Settings",
|
||||
"name": "Advanced",
|
||||
"spec": {
|
||||
"auth": {
|
||||
"default": [],
|
||||
"description": "Username and hashed password for JSON-RPC connections. RPC clients connect using the usual http basic authentication.",
|
||||
"disabled": false,
|
||||
"maxLength": null,
|
||||
"minLength": null,
|
||||
"name": "Authorization",
|
||||
"spec": {
|
||||
"generate": null,
|
||||
"inputmode": "text",
|
||||
"masked": false,
|
||||
"maxLength": null,
|
||||
"minLength": null,
|
||||
"patterns": [
|
||||
{
|
||||
"description": "Each item must be of the form "<USERNAME>:<SALT>$<HASH>".",
|
||||
"regex": "^[a-zA-Z0-9_-]+:([0-9a-fA-F]{2})+\\$([0-9a-fA-F]{2})+$",
|
||||
},
|
||||
],
|
||||
"placeholder": null,
|
||||
"type": "text",
|
||||
},
|
||||
"type": "list",
|
||||
"warning": null,
|
||||
},
|
||||
"servertimeout": {
|
||||
"default": 30,
|
||||
"description": "Number of seconds after which an uncompleted RPC call will time out.",
|
||||
"disabled": false,
|
||||
"immutable": false,
|
||||
"integer": true,
|
||||
"max": 300,
|
||||
"min": 5,
|
||||
"name": "Rpc Server Timeout",
|
||||
"placeholder": null,
|
||||
"required": true,
|
||||
"step": null,
|
||||
"type": "number",
|
||||
"units": "seconds",
|
||||
"warning": null,
|
||||
},
|
||||
"threads": {
|
||||
"default": 16,
|
||||
"description": "Set the number of threads for handling RPC calls. You may wish to increase this if you are making lots of calls via an integration.",
|
||||
"disabled": false,
|
||||
"immutable": false,
|
||||
"integer": true,
|
||||
"max": 64,
|
||||
"min": 1,
|
||||
"name": "Threads",
|
||||
"placeholder": null,
|
||||
"required": true,
|
||||
"step": null,
|
||||
"type": "number",
|
||||
"units": null,
|
||||
"warning": null,
|
||||
},
|
||||
"workqueue": {
|
||||
"default": 128,
|
||||
"description": "Set the depth of the work queue to service RPC calls. Determines how long the backlog of RPC requests can get before it just rejects new ones.",
|
||||
"disabled": false,
|
||||
"immutable": false,
|
||||
"integer": true,
|
||||
"max": 256,
|
||||
"min": 8,
|
||||
"name": "Work Queue",
|
||||
"placeholder": null,
|
||||
"required": true,
|
||||
"step": null,
|
||||
"type": "number",
|
||||
"units": "requests",
|
||||
"warning": null,
|
||||
},
|
||||
},
|
||||
"type": "object",
|
||||
"warning": null,
|
||||
},
|
||||
"enable": {
|
||||
"default": true,
|
||||
"description": "Allow remote RPC requests.",
|
||||
"disabled": false,
|
||||
"immutable": false,
|
||||
"name": "Enable",
|
||||
"type": "toggle",
|
||||
"warning": null,
|
||||
},
|
||||
"password": {
|
||||
"default": {
|
||||
"charset": "a-z,2-7",
|
||||
"len": 20,
|
||||
},
|
||||
"description": "The password for connecting to Bitcoin over RPC.",
|
||||
"disabled": false,
|
||||
"generate": null,
|
||||
"immutable": false,
|
||||
"inputmode": "text",
|
||||
"masked": true,
|
||||
"maxLength": null,
|
||||
"minLength": null,
|
||||
"name": "RPC Password",
|
||||
"patterns": [
|
||||
{
|
||||
"description": "Must be alphanumeric (can contain underscore).",
|
||||
"regex": "^[a-zA-Z0-9_]+$",
|
||||
},
|
||||
],
|
||||
"placeholder": null,
|
||||
"required": true,
|
||||
"type": "text",
|
||||
"warning": "You will need to restart all services that depend on Bitcoin.",
|
||||
},
|
||||
"username": {
|
||||
"default": "bitcoin",
|
||||
"description": "The username for connecting to Bitcoin over RPC.",
|
||||
"disabled": false,
|
||||
"generate": null,
|
||||
"immutable": false,
|
||||
"inputmode": "text",
|
||||
"masked": true,
|
||||
"maxLength": null,
|
||||
"minLength": null,
|
||||
"name": "Username",
|
||||
"patterns": [
|
||||
{
|
||||
"description": "Must be alphanumeric (can contain underscore).",
|
||||
"regex": "^[a-zA-Z0-9_]+$",
|
||||
},
|
||||
],
|
||||
"placeholder": null,
|
||||
"required": true,
|
||||
"type": "text",
|
||||
"warning": "You will need to restart all services that depend on Bitcoin.",
|
||||
},
|
||||
},
|
||||
"type": "object",
|
||||
"warning": null,
|
||||
},
|
||||
"txindex": {
|
||||
"default": true,
|
||||
"description": "By enabling Transaction Index (txindex) Bitcoin Core will build a complete transaction index. This allows Bitcoin Core to access any transaction with commands like \`gettransaction\`.",
|
||||
"disabled": false,
|
||||
"immutable": false,
|
||||
"name": "Transaction Index",
|
||||
"type": "toggle",
|
||||
"warning": null,
|
||||
},
|
||||
"wallet": {
|
||||
"description": "Wallet Settings",
|
||||
"name": "Wallet",
|
||||
"spec": {
|
||||
"avoidpartialspends": {
|
||||
"default": true,
|
||||
"description": "Group outputs by address, selecting all or none, instead of selecting on a per-output basis. This improves privacy at the expense of higher transaction fees.",
|
||||
"disabled": false,
|
||||
"immutable": false,
|
||||
"name": "Avoid Partial Spends",
|
||||
"type": "toggle",
|
||||
"warning": null,
|
||||
},
|
||||
"discardfee": {
|
||||
"default": 0.0001,
|
||||
"description": "The fee rate (in BTC/kB) that indicates your tolerance for discarding change by adding it to the fee.",
|
||||
"disabled": false,
|
||||
"immutable": false,
|
||||
"integer": false,
|
||||
"max": 0.01,
|
||||
"min": null,
|
||||
"name": "Discard Change Tolerance",
|
||||
"placeholder": null,
|
||||
"required": true,
|
||||
"step": null,
|
||||
"type": "number",
|
||||
"units": "BTC/kB",
|
||||
"warning": null,
|
||||
},
|
||||
"enable": {
|
||||
"default": true,
|
||||
"description": "Load the wallet and enable wallet RPC calls.",
|
||||
"disabled": false,
|
||||
"immutable": false,
|
||||
"name": "Enable Wallet",
|
||||
"type": "toggle",
|
||||
"warning": null,
|
||||
},
|
||||
},
|
||||
"type": "object",
|
||||
"warning": null,
|
||||
},
|
||||
"zmq-enabled": {
|
||||
"default": true,
|
||||
"description": "The ZeroMQ interface is useful for some applications which might require data related to block and transaction events from Bitcoin Core. For example, LND requires ZeroMQ be enabled for LND to get the latest block data",
|
||||
"disabled": false,
|
||||
"immutable": false,
|
||||
"name": "ZeroMQ Enabled",
|
||||
"type": "toggle",
|
||||
"warning": null,
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`transformConfigSpec transformConfigSpec(embassyPages) 1`] = `
|
||||
{
|
||||
"homepage": {
|
||||
"default": "welcome",
|
||||
"description": null,
|
||||
"disabled": false,
|
||||
"immutable": false,
|
||||
"name": "Type",
|
||||
"required": true,
|
||||
"type": "union",
|
||||
"variants": {
|
||||
"index": {
|
||||
"name": "Table of Contents",
|
||||
"spec": {},
|
||||
},
|
||||
"redirect": {
|
||||
"name": "Redirect",
|
||||
"spec": {
|
||||
"target": {
|
||||
"default": null,
|
||||
"description": "The name of the subdomain to redirect users to. This must be a valid subdomain site within your Start9 Pages.",
|
||||
"disabled": false,
|
||||
"generate": null,
|
||||
"immutable": false,
|
||||
"inputmode": "text",
|
||||
"masked": false,
|
||||
"maxLength": null,
|
||||
"minLength": null,
|
||||
"name": "Target Subdomain",
|
||||
"patterns": [
|
||||
{
|
||||
"description": "May contain only lowercase characters and hyphens.",
|
||||
"regex": "^[a-z-]+$",
|
||||
},
|
||||
],
|
||||
"placeholder": null,
|
||||
"required": true,
|
||||
"type": "text",
|
||||
"warning": null,
|
||||
},
|
||||
},
|
||||
},
|
||||
"web-page": {
|
||||
"name": "Web Page",
|
||||
"spec": {
|
||||
"folder": {
|
||||
"default": null,
|
||||
"description": "The path to the folder that contains the static files of your website. For example, a value of "projects/resume" would tell Start9 Pages to look for that folder path in the selected service.",
|
||||
"disabled": false,
|
||||
"generate": null,
|
||||
"immutable": false,
|
||||
"inputmode": "text",
|
||||
"masked": false,
|
||||
"maxLength": null,
|
||||
"minLength": null,
|
||||
"name": "Folder Path",
|
||||
"patterns": [
|
||||
{
|
||||
"description": "Must be a valid relative file path",
|
||||
"regex": "^(\\.|[a-zA-Z0-9_ -][a-zA-Z0-9_ .-]*|([a-zA-Z0-9_ .-][a-zA-Z0-9_ -]+\\.*)+)(/[a-zA-Z0-9_ -][a-zA-Z0-9_ .-]*|/([a-zA-Z0-9_ .-][a-zA-Z0-9_ -]+\\.*)+)*/?$",
|
||||
},
|
||||
],
|
||||
"placeholder": "e.g. websites/resume",
|
||||
"required": true,
|
||||
"type": "text",
|
||||
"warning": null,
|
||||
},
|
||||
"source": {
|
||||
"default": "nextcloud",
|
||||
"description": "The service that contains your website files.",
|
||||
"disabled": false,
|
||||
"immutable": false,
|
||||
"name": "Folder Location",
|
||||
"required": false,
|
||||
"type": "select",
|
||||
"values": {
|
||||
"filebrowser": "filebrowser",
|
||||
"nextcloud": "nextcloud",
|
||||
},
|
||||
"warning": null,
|
||||
},
|
||||
},
|
||||
},
|
||||
"welcome": {
|
||||
"name": "Welcome",
|
||||
"spec": {},
|
||||
},
|
||||
},
|
||||
"warning": null,
|
||||
},
|
||||
"subdomains": {
|
||||
"default": [],
|
||||
"description": "The websites you want to serve.",
|
||||
"disabled": false,
|
||||
"maxLength": null,
|
||||
"minLength": null,
|
||||
"name": "Subdomains",
|
||||
"spec": {
|
||||
"displayAs": "{{name}}",
|
||||
"spec": {
|
||||
"name": {
|
||||
"default": null,
|
||||
"description": "The subdomain of your Start9 Pages .onion address to host the website on. For example, a value of "me" would produce a website hosted at http://me.xxxxxx.onion.",
|
||||
"disabled": false,
|
||||
"generate": null,
|
||||
"immutable": false,
|
||||
"inputmode": "text",
|
||||
"masked": false,
|
||||
"maxLength": null,
|
||||
"minLength": null,
|
||||
"name": "Subdomain name",
|
||||
"patterns": [
|
||||
{
|
||||
"description": "May contain only lowercase characters and hyphens",
|
||||
"regex": "^[a-z-]+$",
|
||||
},
|
||||
],
|
||||
"placeholder": null,
|
||||
"required": true,
|
||||
"type": "text",
|
||||
"warning": null,
|
||||
},
|
||||
"settings": {
|
||||
"default": "web-page",
|
||||
"description": null,
|
||||
"disabled": false,
|
||||
"immutable": false,
|
||||
"name": "Type",
|
||||
"required": true,
|
||||
"type": "union",
|
||||
"variants": {
|
||||
"redirect": {
|
||||
"name": "Redirect",
|
||||
"spec": {
|
||||
"target": {
|
||||
"default": null,
|
||||
"description": "The subdomain of your Start9 Pages .onion address to redirect to. This should be the name of another subdomain on Start9 Pages. Leave empty to redirect to the homepage.",
|
||||
"disabled": false,
|
||||
"generate": null,
|
||||
"immutable": false,
|
||||
"inputmode": "text",
|
||||
"masked": false,
|
||||
"maxLength": null,
|
||||
"minLength": null,
|
||||
"name": "Target Subdomain",
|
||||
"patterns": [
|
||||
{
|
||||
"description": "May contain only lowercase characters and hyphens.",
|
||||
"regex": "^[a-z-]+$",
|
||||
},
|
||||
],
|
||||
"placeholder": null,
|
||||
"required": true,
|
||||
"type": "text",
|
||||
"warning": null,
|
||||
},
|
||||
},
|
||||
},
|
||||
"web-page": {
|
||||
"name": "Web Page",
|
||||
"spec": {
|
||||
"folder": {
|
||||
"default": null,
|
||||
"description": "The path to the folder that contains the website files. For example, a value of "projects/resume" would tell Start9 Pages to look for that folder path in the selected service.",
|
||||
"disabled": false,
|
||||
"generate": null,
|
||||
"immutable": false,
|
||||
"inputmode": "text",
|
||||
"masked": false,
|
||||
"maxLength": null,
|
||||
"minLength": null,
|
||||
"name": "Folder Path",
|
||||
"patterns": [
|
||||
{
|
||||
"description": "Must be a valid relative file path",
|
||||
"regex": "^(\\.|[a-zA-Z0-9_ -][a-zA-Z0-9_ .-]*|([a-zA-Z0-9_ .-][a-zA-Z0-9_ -]+\\.*)+)(/[a-zA-Z0-9_ -][a-zA-Z0-9_ .-]*|/([a-zA-Z0-9_ .-][a-zA-Z0-9_ -]+\\.*)+)*/?$",
|
||||
},
|
||||
],
|
||||
"placeholder": "e.g. websites/resume",
|
||||
"required": true,
|
||||
"type": "text",
|
||||
"warning": null,
|
||||
},
|
||||
"source": {
|
||||
"default": "nextcloud",
|
||||
"description": "The service that contains your website files.",
|
||||
"disabled": false,
|
||||
"immutable": false,
|
||||
"name": "Folder Location",
|
||||
"required": false,
|
||||
"type": "select",
|
||||
"values": {
|
||||
"filebrowser": "filebrowser",
|
||||
"nextcloud": "nextcloud",
|
||||
},
|
||||
"warning": null,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"warning": null,
|
||||
},
|
||||
},
|
||||
"type": "object",
|
||||
"uniqueBy": "name",
|
||||
},
|
||||
"type": "list",
|
||||
"warning": null,
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`transformConfigSpec transformConfigSpec(nostr) 1`] = `
|
||||
{
|
||||
"nostr-relay": {
|
||||
"default": "wss://relay.getalby.com/v1",
|
||||
"description": "The Nostr Relay to use for Nostr Wallet Connect connections",
|
||||
"disabled": false,
|
||||
"generate": null,
|
||||
"immutable": false,
|
||||
"inputmode": "text",
|
||||
"masked": false,
|
||||
"maxLength": null,
|
||||
"minLength": null,
|
||||
"name": "Nostr Relay",
|
||||
"patterns": [],
|
||||
"placeholder": null,
|
||||
"required": true,
|
||||
"type": "text",
|
||||
"warning": null,
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`transformConfigSpec transformConfigSpec(searNXG) 1`] = `
|
||||
{
|
||||
"enable-metrics": {
|
||||
"default": true,
|
||||
"description": "Your SearXNG instance will collect anonymous stats about its own usage and performance. You can view these metrics by appending \`/stats\` or \`/stats/errors\` to your SearXNG URL.",
|
||||
"disabled": false,
|
||||
"immutable": false,
|
||||
"name": "Enable Stats",
|
||||
"type": "toggle",
|
||||
"warning": null,
|
||||
},
|
||||
"instance-name": {
|
||||
"default": "My SearXNG Engine",
|
||||
"description": "Enter a name for your SearXNG instance. This is the name that will be listed if you want to share your SearXNG engine publicly.",
|
||||
"disabled": false,
|
||||
"generate": null,
|
||||
"immutable": false,
|
||||
"inputmode": "text",
|
||||
"masked": false,
|
||||
"maxLength": null,
|
||||
"minLength": null,
|
||||
"name": "SearXNG Instance Name",
|
||||
"patterns": [],
|
||||
"placeholder": "Uncle Jim SearXNG Engine",
|
||||
"required": true,
|
||||
"type": "text",
|
||||
"warning": null,
|
||||
},
|
||||
"tor-url": {
|
||||
"default": false,
|
||||
"description": "Activates the utilization of a .onion address as the primary URL, particularly beneficial for publicly hosted instances over the Tor network.",
|
||||
"disabled": false,
|
||||
"immutable": false,
|
||||
"name": "Enable Tor address as the base URL",
|
||||
"type": "toggle",
|
||||
"warning": null,
|
||||
},
|
||||
}
|
||||
`;
|
||||
File diff suppressed because it is too large
Load Diff
@@ -57,6 +57,7 @@ export const matchManifest = object(
|
||||
matchProcedure,
|
||||
object({
|
||||
name: string,
|
||||
["success-message"]: string,
|
||||
}),
|
||||
),
|
||||
]),
|
||||
@@ -68,13 +69,25 @@ export const matchManifest = object(
|
||||
volumes: dictionary([string, matchVolume]),
|
||||
interfaces: dictionary([
|
||||
string,
|
||||
object({
|
||||
name: string,
|
||||
"tor-config": object({}),
|
||||
"lan-config": object({}),
|
||||
ui: boolean,
|
||||
protocols: array(string),
|
||||
}),
|
||||
object(
|
||||
{
|
||||
name: string,
|
||||
description: string,
|
||||
"tor-config": object({
|
||||
"port-mapping": dictionary([string, string]),
|
||||
}),
|
||||
"lan-config": dictionary([
|
||||
string,
|
||||
object({
|
||||
ssl: boolean,
|
||||
internal: number,
|
||||
}),
|
||||
]),
|
||||
ui: boolean,
|
||||
protocols: array(string),
|
||||
},
|
||||
["lan-config", "tor-config"],
|
||||
),
|
||||
]),
|
||||
backup: object({
|
||||
create: matchProcedure,
|
||||
|
||||
@@ -99,17 +99,8 @@ export type Effects = {
|
||||
/** Sandbox mode lets us read but not write */
|
||||
is_sandboxed(): boolean
|
||||
|
||||
// Does a volume and path exist?
|
||||
exists(input: { volumeId: string; path: string }): Promise<boolean>
|
||||
bindLocal(options: {
|
||||
internalPort: number
|
||||
name: string
|
||||
externalPort: number
|
||||
}): Promise<string>
|
||||
bindTor(options: {
|
||||
internalPort: number
|
||||
name: string
|
||||
externalPort: number
|
||||
}): Promise<string>
|
||||
|
||||
fetch(
|
||||
url: string,
|
||||
@@ -129,6 +120,10 @@ export type Effects = {
|
||||
/// Returns the body as a json
|
||||
json(): Promise<unknown>
|
||||
}>
|
||||
diskUsage(options?: {
|
||||
volumeId: string
|
||||
path: string
|
||||
}): Promise<{ used: number; total: number }>
|
||||
|
||||
runRsync(options: {
|
||||
srcVolume: string
|
||||
|
||||
@@ -3,213 +3,434 @@ import * as oet from "./oldEmbassyTypes"
|
||||
import { Volume } from "../../../Models/Volume"
|
||||
import * as child_process from "child_process"
|
||||
import { promisify } from "util"
|
||||
import { startSdk } from "@start9labs/start-sdk"
|
||||
import { HostSystemStartOs } from "../../HostSystemStartOs"
|
||||
import { daemons, startSdk, T } from "@start9labs/start-sdk"
|
||||
import "isomorphic-fetch"
|
||||
import { Manifest } from "./matchManifest"
|
||||
|
||||
const execFile = promisify(child_process.execFile)
|
||||
|
||||
export class PolyfillEffects implements oet.Effects {
|
||||
constructor(
|
||||
readonly effects: HostSystemStartOs,
|
||||
private manifest: Manifest,
|
||||
) {}
|
||||
async writeFile(input: {
|
||||
path: string
|
||||
volumeId: string
|
||||
toWrite: string
|
||||
}): Promise<void> {
|
||||
await fs.writeFile(
|
||||
new Volume(input.volumeId, input.path).path,
|
||||
input.toWrite,
|
||||
)
|
||||
}
|
||||
async readFile(input: { volumeId: string; path: string }): Promise<string> {
|
||||
return (
|
||||
await fs.readFile(new Volume(input.volumeId, input.path).path)
|
||||
).toString()
|
||||
}
|
||||
async metadata(input: {
|
||||
volumeId: string
|
||||
path: string
|
||||
}): Promise<oet.Metadata> {
|
||||
const stats = await fs.stat(new Volume(input.volumeId, input.path).path)
|
||||
return {
|
||||
fileType: stats.isFile() ? "file" : "directory",
|
||||
gid: stats.gid,
|
||||
uid: stats.uid,
|
||||
mode: stats.mode,
|
||||
isDir: stats.isDirectory(),
|
||||
isFile: stats.isFile(),
|
||||
isSymlink: stats.isSymbolicLink(),
|
||||
len: stats.size,
|
||||
readonly: (stats.mode & 0o200) > 0,
|
||||
}
|
||||
}
|
||||
async createDir(input: { volumeId: string; path: string }): Promise<string> {
|
||||
const path = new Volume(input.volumeId, input.path).path
|
||||
await fs.mkdir(path, { recursive: true })
|
||||
return path
|
||||
}
|
||||
async readDir(input: { volumeId: string; path: string }): Promise<string[]> {
|
||||
return fs.readdir(new Volume(input.volumeId, input.path).path)
|
||||
}
|
||||
async removeDir(input: { volumeId: string; path: string }): Promise<string> {
|
||||
const path = new Volume(input.volumeId, input.path).path
|
||||
await fs.rmdir(new Volume(input.volumeId, input.path).path, {
|
||||
recursive: true,
|
||||
})
|
||||
return path
|
||||
}
|
||||
removeFile(input: { volumeId: string; path: string }): Promise<void> {
|
||||
return fs.rm(new Volume(input.volumeId, input.path).path)
|
||||
}
|
||||
async writeJsonFile(input: {
|
||||
volumeId: string
|
||||
path: string
|
||||
toWrite: Record<string, unknown>
|
||||
}): Promise<void> {
|
||||
await fs.writeFile(
|
||||
new Volume(input.volumeId, input.path).path,
|
||||
JSON.stringify(input.toWrite),
|
||||
)
|
||||
}
|
||||
async readJsonFile(input: {
|
||||
volumeId: string
|
||||
path: string
|
||||
}): Promise<Record<string, unknown>> {
|
||||
return JSON.parse(
|
||||
(
|
||||
await fs.readFile(new Volume(input.volumeId, input.path).path)
|
||||
).toString(),
|
||||
)
|
||||
}
|
||||
runCommand({
|
||||
command,
|
||||
args,
|
||||
timeoutMillis,
|
||||
}: {
|
||||
command: string
|
||||
args?: string[] | undefined
|
||||
timeoutMillis?: number | undefined
|
||||
}): Promise<oet.ResultType<string>> {
|
||||
return startSdk
|
||||
.runCommand(
|
||||
this.effects,
|
||||
this.manifest.main.image,
|
||||
[command, ...(args || [])],
|
||||
{},
|
||||
import { DockerProcedureContainer } from "./DockerProcedureContainer"
|
||||
import * as cp from "child_process"
|
||||
import { Effects } from "../../../Models/Effects"
|
||||
export const execFile = promisify(cp.execFile)
|
||||
export const polyfillEffects = (
|
||||
effects: Effects,
|
||||
manifest: Manifest,
|
||||
): oet.Effects => {
|
||||
const self = {
|
||||
effects,
|
||||
manifest,
|
||||
async writeFile(input: {
|
||||
path: string
|
||||
volumeId: string
|
||||
toWrite: string
|
||||
}): Promise<void> {
|
||||
await fs.writeFile(
|
||||
new Volume(input.volumeId, input.path).path,
|
||||
input.toWrite,
|
||||
)
|
||||
.then((x: any) => ({
|
||||
stderr: x.stderr.toString(),
|
||||
stdout: x.stdout.toString(),
|
||||
}))
|
||||
.then((x) => (!!x.stderr ? { error: x.stderr } : { result: x.stdout }))
|
||||
}
|
||||
runDaemon(input: { command: string; args?: string[] | undefined }): {
|
||||
wait(): Promise<oet.ResultType<string>>
|
||||
term(): Promise<void>
|
||||
} {
|
||||
throw new Error("Method not implemented.")
|
||||
}
|
||||
chown(input: { volumeId: string; path: string; uid: string }): Promise<null> {
|
||||
throw new Error("Method not implemented.")
|
||||
}
|
||||
chmod(input: {
|
||||
volumeId: string
|
||||
path: string
|
||||
mode: string
|
||||
}): Promise<null> {
|
||||
throw new Error("Method not implemented.")
|
||||
}
|
||||
sleep(timeMs: number): Promise<null> {
|
||||
return new Promise((resolve) => setTimeout(resolve, timeMs))
|
||||
}
|
||||
trace(whatToPrint: string): void {
|
||||
console.trace(whatToPrint)
|
||||
}
|
||||
warn(whatToPrint: string): void {
|
||||
console.warn(whatToPrint)
|
||||
}
|
||||
error(whatToPrint: string): void {
|
||||
console.error(whatToPrint)
|
||||
}
|
||||
debug(whatToPrint: string): void {
|
||||
console.debug(whatToPrint)
|
||||
}
|
||||
info(whatToPrint: string): void {
|
||||
console.log(false)
|
||||
}
|
||||
is_sandboxed(): boolean {
|
||||
return false
|
||||
}
|
||||
exists(input: { volumeId: string; path: string }): Promise<boolean> {
|
||||
return this.metadata(input)
|
||||
.then(() => true)
|
||||
.catch(() => false)
|
||||
}
|
||||
bindLocal(options: {
|
||||
internalPort: number
|
||||
name: string
|
||||
externalPort: number
|
||||
}): Promise<string> {
|
||||
throw new Error("Method not implemented.")
|
||||
}
|
||||
bindTor(options: {
|
||||
internalPort: number
|
||||
name: string
|
||||
externalPort: number
|
||||
}): Promise<string> {
|
||||
throw new Error("Method not implemented.")
|
||||
}
|
||||
async fetch(
|
||||
url: string,
|
||||
options?:
|
||||
| {
|
||||
method?:
|
||||
| "GET"
|
||||
| "POST"
|
||||
| "PUT"
|
||||
| "DELETE"
|
||||
| "HEAD"
|
||||
| "PATCH"
|
||||
| undefined
|
||||
headers?: Record<string, string> | undefined
|
||||
body?: string | undefined
|
||||
},
|
||||
async readFile(input: { volumeId: string; path: string }): Promise<string> {
|
||||
return (
|
||||
await fs.readFile(new Volume(input.volumeId, input.path).path)
|
||||
).toString()
|
||||
},
|
||||
async metadata(input: {
|
||||
volumeId: string
|
||||
path: string
|
||||
}): Promise<oet.Metadata> {
|
||||
const stats = await fs.stat(new Volume(input.volumeId, input.path).path)
|
||||
return {
|
||||
fileType: stats.isFile() ? "file" : "directory",
|
||||
gid: stats.gid,
|
||||
uid: stats.uid,
|
||||
mode: stats.mode,
|
||||
isDir: stats.isDirectory(),
|
||||
isFile: stats.isFile(),
|
||||
isSymlink: stats.isSymbolicLink(),
|
||||
len: stats.size,
|
||||
readonly: (stats.mode & 0o200) > 0,
|
||||
}
|
||||
},
|
||||
async createDir(input: {
|
||||
volumeId: string
|
||||
path: string
|
||||
}): Promise<string> {
|
||||
const path = new Volume(input.volumeId, input.path).path
|
||||
await fs.mkdir(path, { recursive: true })
|
||||
return path
|
||||
},
|
||||
async readDir(input: {
|
||||
volumeId: string
|
||||
path: string
|
||||
}): Promise<string[]> {
|
||||
return fs.readdir(new Volume(input.volumeId, input.path).path)
|
||||
},
|
||||
async removeDir(input: {
|
||||
volumeId: string
|
||||
path: string
|
||||
}): Promise<string> {
|
||||
const path = new Volume(input.volumeId, input.path).path
|
||||
await fs.rmdir(new Volume(input.volumeId, input.path).path, {
|
||||
recursive: true,
|
||||
})
|
||||
return path
|
||||
},
|
||||
removeFile(input: { volumeId: string; path: string }): Promise<void> {
|
||||
return fs.rm(new Volume(input.volumeId, input.path).path)
|
||||
},
|
||||
async writeJsonFile(input: {
|
||||
volumeId: string
|
||||
path: string
|
||||
toWrite: Record<string, unknown>
|
||||
}): Promise<void> {
|
||||
await fs.writeFile(
|
||||
new Volume(input.volumeId, input.path).path,
|
||||
JSON.stringify(input.toWrite),
|
||||
)
|
||||
},
|
||||
async readJsonFile(input: {
|
||||
volumeId: string
|
||||
path: string
|
||||
}): Promise<Record<string, unknown>> {
|
||||
return JSON.parse(
|
||||
(
|
||||
await fs.readFile(new Volume(input.volumeId, input.path).path)
|
||||
).toString(),
|
||||
)
|
||||
},
|
||||
runCommand({
|
||||
command,
|
||||
args,
|
||||
timeoutMillis,
|
||||
}: {
|
||||
command: string
|
||||
args?: string[] | undefined
|
||||
timeoutMillis?: number | undefined
|
||||
}): Promise<oet.ResultType<string>> {
|
||||
return startSdk
|
||||
.runCommand(
|
||||
effects,
|
||||
{ id: manifest.main.image },
|
||||
[command, ...(args || [])],
|
||||
{},
|
||||
)
|
||||
.then((x: any) => ({
|
||||
stderr: x.stderr.toString(),
|
||||
stdout: x.stdout.toString(),
|
||||
}))
|
||||
.then((x: any) =>
|
||||
!!x.stderr ? { error: x.stderr } : { result: x.stdout },
|
||||
)
|
||||
},
|
||||
runDaemon(input: { command: string; args?: string[] | undefined }): {
|
||||
wait(): Promise<oet.ResultType<string>>
|
||||
term(): Promise<void>
|
||||
} {
|
||||
const dockerProcedureContainer = DockerProcedureContainer.of(
|
||||
effects,
|
||||
manifest.id,
|
||||
manifest.main,
|
||||
manifest.volumes,
|
||||
)
|
||||
const daemon = dockerProcedureContainer.then((dockerProcedureContainer) =>
|
||||
daemons.runCommand()(
|
||||
effects,
|
||||
{ id: manifest.main.image },
|
||||
[input.command, ...(input.args || [])],
|
||||
{
|
||||
overlay: dockerProcedureContainer.overlay,
|
||||
},
|
||||
),
|
||||
)
|
||||
return {
|
||||
wait: () =>
|
||||
daemon.then((daemon) =>
|
||||
daemon.wait().then(() => {
|
||||
return { result: "" }
|
||||
}),
|
||||
),
|
||||
term: () => daemon.then((daemon) => daemon.term()),
|
||||
}
|
||||
},
|
||||
async chown(input: {
|
||||
volumeId: string
|
||||
path: string
|
||||
uid: string
|
||||
}): Promise<null> {
|
||||
await startSdk
|
||||
.runCommand(
|
||||
effects,
|
||||
{ id: manifest.main.image },
|
||||
["chown", "--recursive", input.uid, `/drive/${input.path}`],
|
||||
{
|
||||
mounts: [
|
||||
{
|
||||
path: "/drive",
|
||||
options: {
|
||||
type: "volume",
|
||||
id: input.volumeId,
|
||||
subpath: null,
|
||||
readonly: false,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
)
|
||||
.then((x: any) => ({
|
||||
stderr: x.stderr.toString(),
|
||||
stdout: x.stdout.toString(),
|
||||
}))
|
||||
.then((x: any) => {
|
||||
if (!!x.stderr) {
|
||||
throw new Error(x.stderr)
|
||||
}
|
||||
})
|
||||
return null
|
||||
},
|
||||
async chmod(input: {
|
||||
volumeId: string
|
||||
path: string
|
||||
mode: string
|
||||
}): Promise<null> {
|
||||
await startSdk
|
||||
.runCommand(
|
||||
effects,
|
||||
{ id: manifest.main.image },
|
||||
["chmod", "--recursive", input.mode, `/drive/${input.path}`],
|
||||
{
|
||||
mounts: [
|
||||
{
|
||||
path: "/drive",
|
||||
options: {
|
||||
type: "volume",
|
||||
id: input.volumeId,
|
||||
subpath: null,
|
||||
readonly: false,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
)
|
||||
.then((x: any) => ({
|
||||
stderr: x.stderr.toString(),
|
||||
stdout: x.stdout.toString(),
|
||||
}))
|
||||
.then((x: any) => {
|
||||
if (!!x.stderr) {
|
||||
throw new Error(x.stderr)
|
||||
}
|
||||
})
|
||||
return null
|
||||
},
|
||||
sleep(timeMs: number): Promise<null> {
|
||||
return new Promise((resolve) => setTimeout(resolve, timeMs))
|
||||
},
|
||||
trace(whatToPrint: string): void {
|
||||
console.trace(whatToPrint)
|
||||
},
|
||||
warn(whatToPrint: string): void {
|
||||
console.warn(whatToPrint)
|
||||
},
|
||||
error(whatToPrint: string): void {
|
||||
console.error(whatToPrint)
|
||||
},
|
||||
debug(whatToPrint: string): void {
|
||||
console.debug(whatToPrint)
|
||||
},
|
||||
info(whatToPrint: string): void {
|
||||
console.log(false)
|
||||
},
|
||||
is_sandboxed(): boolean {
|
||||
return false
|
||||
},
|
||||
exists(input: { volumeId: string; path: string }): Promise<boolean> {
|
||||
return self
|
||||
.metadata(input)
|
||||
.then(() => true)
|
||||
.catch(() => false)
|
||||
},
|
||||
async fetch(
|
||||
url: string,
|
||||
options?:
|
||||
| {
|
||||
method?:
|
||||
| "GET"
|
||||
| "POST"
|
||||
| "PUT"
|
||||
| "DELETE"
|
||||
| "HEAD"
|
||||
| "PATCH"
|
||||
| undefined
|
||||
headers?: Record<string, string> | undefined
|
||||
body?: string | undefined
|
||||
}
|
||||
| undefined,
|
||||
): Promise<{
|
||||
method: string
|
||||
ok: boolean
|
||||
status: number
|
||||
headers: Record<string, string>
|
||||
body?: string | null | undefined
|
||||
text(): Promise<string>
|
||||
json(): Promise<unknown>
|
||||
}> {
|
||||
const fetched = await fetch(url, options)
|
||||
return {
|
||||
method: fetched.type,
|
||||
ok: fetched.ok,
|
||||
status: fetched.status,
|
||||
headers: Object.fromEntries(fetched.headers.entries()),
|
||||
body: await fetched.text(),
|
||||
text: () => fetched.text(),
|
||||
json: () => fetched.json(),
|
||||
}
|
||||
},
|
||||
|
||||
runRsync(rsyncOptions: {
|
||||
srcVolume: string
|
||||
dstVolume: string
|
||||
srcPath: string
|
||||
dstPath: string
|
||||
options: oet.BackupOptions
|
||||
}): {
|
||||
id: () => Promise<string>
|
||||
wait: () => Promise<null>
|
||||
progress: () => Promise<number>
|
||||
} {
|
||||
let secondRun: ReturnType<typeof self._runRsync> | undefined
|
||||
let firstRun = self._runRsync(rsyncOptions)
|
||||
let waitValue = firstRun.wait().then((x) => {
|
||||
secondRun = self._runRsync(rsyncOptions)
|
||||
return secondRun.wait()
|
||||
})
|
||||
const id = async () => {
|
||||
return secondRun?.id?.() ?? firstRun.id()
|
||||
}
|
||||
const wait = () => waitValue
|
||||
const progress = async () => {
|
||||
const secondProgress = secondRun?.progress?.()
|
||||
if (secondProgress) {
|
||||
return (await secondProgress) / 2.0 + 0.5
|
||||
}
|
||||
| undefined,
|
||||
): Promise<{
|
||||
method: string
|
||||
ok: boolean
|
||||
status: number
|
||||
headers: Record<string, string>
|
||||
body?: string | null | undefined
|
||||
text(): Promise<string>
|
||||
json(): Promise<unknown>
|
||||
}> {
|
||||
const fetched = await fetch(url, options)
|
||||
return {
|
||||
method: fetched.type,
|
||||
ok: fetched.ok,
|
||||
status: fetched.status,
|
||||
headers: Object.fromEntries(fetched.headers.entries()),
|
||||
body: await fetched.text(),
|
||||
text: () => fetched.text(),
|
||||
json: () => fetched.json(),
|
||||
}
|
||||
}
|
||||
runRsync(options: {
|
||||
srcVolume: string
|
||||
dstVolume: string
|
||||
srcPath: string
|
||||
dstPath: string
|
||||
options: oet.BackupOptions
|
||||
}): {
|
||||
id: () => Promise<string>
|
||||
wait: () => Promise<null>
|
||||
progress: () => Promise<number>
|
||||
} {
|
||||
throw new Error("Method not implemented.")
|
||||
return (await firstRun.progress()) / 2.0
|
||||
}
|
||||
return { id, wait, progress }
|
||||
},
|
||||
_runRsync(rsyncOptions: {
|
||||
srcVolume: string
|
||||
dstVolume: string
|
||||
srcPath: string
|
||||
dstPath: string
|
||||
options: oet.BackupOptions
|
||||
}): {
|
||||
id: () => Promise<string>
|
||||
wait: () => Promise<null>
|
||||
progress: () => Promise<number>
|
||||
} {
|
||||
const { srcVolume, dstVolume, srcPath, dstPath, options } = rsyncOptions
|
||||
const command = "rsync"
|
||||
const args: string[] = []
|
||||
if (options.delete) {
|
||||
args.push("--delete")
|
||||
}
|
||||
if (options.force) {
|
||||
args.push("--force")
|
||||
}
|
||||
if (options.ignoreExisting) {
|
||||
args.push("--ignore-existing")
|
||||
}
|
||||
for (const exclude of options.exclude) {
|
||||
args.push(`--exclude=${exclude}`)
|
||||
}
|
||||
args.push("-actAXH")
|
||||
args.push("--info=progress2")
|
||||
args.push("--no-inc-recursive")
|
||||
args.push(new Volume(srcVolume, srcPath).path)
|
||||
args.push(new Volume(dstVolume, dstPath).path)
|
||||
const spawned = child_process.spawn(command, args, { detached: true })
|
||||
let percentage = 0.0
|
||||
spawned.stdout.on("data", (data: unknown) => {
|
||||
const lines = String(data).replace("\r", "\n").split("\n")
|
||||
for (const line of lines) {
|
||||
const parsed = /$([0-9.]+)%/.exec(line)?.[1]
|
||||
if (!parsed) continue
|
||||
percentage = Number.parseFloat(parsed)
|
||||
}
|
||||
})
|
||||
|
||||
spawned.stderr.on("data", (data: unknown) => {
|
||||
console.error(String(data))
|
||||
})
|
||||
|
||||
const id = async () => {
|
||||
const pid = spawned.pid
|
||||
if (pid === undefined) {
|
||||
throw new Error("rsync process has no pid")
|
||||
}
|
||||
return String(pid)
|
||||
}
|
||||
const waitPromise = new Promise<null>((resolve, reject) => {
|
||||
spawned.on("exit", (code: any) => {
|
||||
if (code === 0) {
|
||||
resolve(null)
|
||||
} else {
|
||||
reject(new Error(`rsync exited with code ${code}`))
|
||||
}
|
||||
})
|
||||
})
|
||||
const wait = () => waitPromise
|
||||
const progress = () => Promise.resolve(percentage)
|
||||
return { id, wait, progress }
|
||||
},
|
||||
async diskUsage(
|
||||
options?: { volumeId: string; path: string } | undefined,
|
||||
): Promise<{ used: number; total: number }> {
|
||||
const output = await execFile("df", ["--block-size=1", "-P", "/"])
|
||||
.then((x: any) => ({
|
||||
stderr: x.stderr.toString(),
|
||||
stdout: x.stdout.toString(),
|
||||
}))
|
||||
.then((x: any) => {
|
||||
if (!!x.stderr) {
|
||||
throw new Error(x.stderr)
|
||||
}
|
||||
return parseDfOutput(x.stdout)
|
||||
})
|
||||
if (!!options) {
|
||||
const used = await execFile("du", [
|
||||
"-s",
|
||||
"--block-size=1",
|
||||
"-P",
|
||||
new Volume(options.volumeId, options.path).path,
|
||||
])
|
||||
.then((x: any) => ({
|
||||
stderr: x.stderr.toString(),
|
||||
stdout: x.stdout.toString(),
|
||||
}))
|
||||
.then((x: any) => {
|
||||
if (!!x.stderr) {
|
||||
throw new Error(x.stderr)
|
||||
}
|
||||
return Number.parseInt(x.stdout.split(/\s+/)[0])
|
||||
})
|
||||
return {
|
||||
...output,
|
||||
used,
|
||||
}
|
||||
}
|
||||
return output
|
||||
},
|
||||
}
|
||||
return self
|
||||
}
|
||||
|
||||
function parseDfOutput(output: string): { used: number; total: number } {
|
||||
const lines = output
|
||||
.split("\n")
|
||||
.filter((x) => x.length)
|
||||
.map((x) => x.split(/\s+/))
|
||||
const index = lines.splice(0, 1)[0].map((x) => x.toLowerCase())
|
||||
const usedIndex = index.indexOf("used")
|
||||
const availableIndex = index.indexOf("available")
|
||||
const used = lines.map((x) => Number.parseInt(x[usedIndex]))[0] || 0
|
||||
const total = lines.map((x) => Number.parseInt(x[availableIndex]))[0] || 0
|
||||
return { used, total }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import { matchOldConfigSpec, transformConfigSpec } from "./transformConfigSpec"
|
||||
import fixtureEmbasyPagesConfig from "./__fixtures__/embasyPagesConfig"
|
||||
import searNXG from "./__fixtures__/searNXG"
|
||||
import bitcoind from "./__fixtures__/bitcoind"
|
||||
import nostr from "./__fixtures__/nostr"
|
||||
|
||||
describe("transformConfigSpec", () => {
|
||||
test("matchOldConfigSpec(embassyPages.homepage.variants[web-page])", () => {
|
||||
matchOldConfigSpec.unsafeCast(
|
||||
fixtureEmbasyPagesConfig.homepage.variants["web-page"],
|
||||
)
|
||||
})
|
||||
test("matchOldConfigSpec(embassyPages)", () => {
|
||||
matchOldConfigSpec.unsafeCast(fixtureEmbasyPagesConfig)
|
||||
})
|
||||
test("transformConfigSpec(embassyPages)", () => {
|
||||
const spec = matchOldConfigSpec.unsafeCast(fixtureEmbasyPagesConfig)
|
||||
expect(transformConfigSpec(spec)).toMatchSnapshot()
|
||||
})
|
||||
|
||||
test("transformConfigSpec(searNXG)", () => {
|
||||
const spec = matchOldConfigSpec.unsafeCast(searNXG)
|
||||
expect(transformConfigSpec(spec)).toMatchSnapshot()
|
||||
})
|
||||
test("transformConfigSpec(bitcoind)", () => {
|
||||
const spec = matchOldConfigSpec.unsafeCast(bitcoind)
|
||||
expect(transformConfigSpec(spec)).toMatchSnapshot()
|
||||
})
|
||||
test("transformConfigSpec(nostr)", () => {
|
||||
const spec = matchOldConfigSpec.unsafeCast(nostr)
|
||||
expect(transformConfigSpec(spec)).toMatchSnapshot()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,589 @@
|
||||
import { CT } from "@start9labs/start-sdk"
|
||||
import {
|
||||
dictionary,
|
||||
object,
|
||||
anyOf,
|
||||
string,
|
||||
literals,
|
||||
array,
|
||||
number,
|
||||
boolean,
|
||||
Parser,
|
||||
deferred,
|
||||
every,
|
||||
nill,
|
||||
literal,
|
||||
} from "ts-matches"
|
||||
|
||||
export function transformConfigSpec(oldSpec: OldConfigSpec): CT.InputSpec {
|
||||
return Object.entries(oldSpec).reduce((inputSpec, [key, oldVal]) => {
|
||||
let newVal: CT.ValueSpec
|
||||
|
||||
if (oldVal.type === "boolean") {
|
||||
newVal = {
|
||||
type: "toggle",
|
||||
name: oldVal.name,
|
||||
default: oldVal.default,
|
||||
description: oldVal.description || null,
|
||||
warning: oldVal.warning || null,
|
||||
disabled: false,
|
||||
immutable: false,
|
||||
}
|
||||
} else if (oldVal.type === "enum") {
|
||||
newVal = {
|
||||
type: "select",
|
||||
name: oldVal.name,
|
||||
description: oldVal.description || null,
|
||||
warning: oldVal.warning || null,
|
||||
default: oldVal.default,
|
||||
values: oldVal.values.reduce(
|
||||
(obj, curr) => ({
|
||||
...obj,
|
||||
[curr]: oldVal["value-names"][curr] || curr,
|
||||
}),
|
||||
{},
|
||||
),
|
||||
required: false,
|
||||
disabled: false,
|
||||
immutable: false,
|
||||
}
|
||||
} else if (oldVal.type === "list") {
|
||||
newVal = getListSpec(oldVal)
|
||||
} else if (oldVal.type === "number") {
|
||||
const range = Range.from(oldVal.range)
|
||||
|
||||
newVal = {
|
||||
type: "number",
|
||||
name: oldVal.name,
|
||||
default: oldVal.default || null,
|
||||
description: oldVal.description || null,
|
||||
warning: oldVal.warning || null,
|
||||
disabled: false,
|
||||
immutable: false,
|
||||
required: !oldVal.nullable,
|
||||
min: range.min
|
||||
? range.minInclusive
|
||||
? range.min
|
||||
: range.min + 1
|
||||
: null,
|
||||
max: range.max
|
||||
? range.maxInclusive
|
||||
? range.max
|
||||
: range.max - 1
|
||||
: null,
|
||||
integer: oldVal.integral,
|
||||
step: null,
|
||||
units: oldVal.units || null,
|
||||
placeholder: oldVal.placeholder || null,
|
||||
}
|
||||
} else if (oldVal.type === "object") {
|
||||
newVal = {
|
||||
type: "object",
|
||||
name: oldVal.name,
|
||||
description: oldVal.description || null,
|
||||
warning: oldVal.warning || null,
|
||||
spec: transformConfigSpec(matchOldConfigSpec.unsafeCast(oldVal.spec)),
|
||||
}
|
||||
} else if (oldVal.type === "string") {
|
||||
newVal = {
|
||||
type: "text",
|
||||
name: oldVal.name,
|
||||
default: oldVal.default || null,
|
||||
description: oldVal.description || null,
|
||||
warning: oldVal.warning || null,
|
||||
disabled: false,
|
||||
immutable: false,
|
||||
required: !oldVal.nullable,
|
||||
patterns:
|
||||
oldVal.pattern && oldVal["pattern-description"]
|
||||
? [
|
||||
{
|
||||
regex: oldVal.pattern,
|
||||
description: oldVal["pattern-description"],
|
||||
},
|
||||
]
|
||||
: [],
|
||||
minLength: null,
|
||||
maxLength: null,
|
||||
masked: oldVal.masked || false,
|
||||
generate: null,
|
||||
inputmode: "text",
|
||||
placeholder: oldVal.placeholder || null,
|
||||
}
|
||||
} else if (oldVal.type === "union") {
|
||||
newVal = {
|
||||
type: "union",
|
||||
name: oldVal.tag.name,
|
||||
description: oldVal.tag.description || null,
|
||||
warning: oldVal.tag.warning || null,
|
||||
variants: Object.entries(oldVal.variants).reduce(
|
||||
(obj, [id, spec]) => ({
|
||||
...obj,
|
||||
[id]: {
|
||||
name: oldVal.tag["variant-names"][id] || id,
|
||||
spec: transformConfigSpec(matchOldConfigSpec.unsafeCast(spec)),
|
||||
},
|
||||
}),
|
||||
{} as Record<string, { name: string; spec: CT.InputSpec }>,
|
||||
),
|
||||
disabled: false,
|
||||
required: true,
|
||||
default: oldVal.default,
|
||||
immutable: false,
|
||||
}
|
||||
} else if (oldVal.type === "pointer") {
|
||||
return inputSpec
|
||||
} else {
|
||||
throw new Error(`unknown spec ${JSON.stringify(oldVal)}`)
|
||||
}
|
||||
|
||||
return {
|
||||
...inputSpec,
|
||||
[key]: newVal,
|
||||
}
|
||||
}, {} as CT.InputSpec)
|
||||
}
|
||||
|
||||
export function transformOldConfigToNew(
|
||||
spec: OldConfigSpec,
|
||||
config: Record<string, any>,
|
||||
): Record<string, any> {
|
||||
return Object.entries(spec).reduce((obj, [key, val]) => {
|
||||
let newVal = config[key]
|
||||
|
||||
if (isObject(val)) {
|
||||
newVal = transformOldConfigToNew(
|
||||
matchOldConfigSpec.unsafeCast(val.spec),
|
||||
config[key],
|
||||
)
|
||||
}
|
||||
|
||||
if (isUnion(val)) {
|
||||
const selection = config[key][val.tag.id]
|
||||
delete config[key][val.tag.id]
|
||||
|
||||
newVal = {
|
||||
selection,
|
||||
value: transformOldConfigToNew(
|
||||
matchOldConfigSpec.unsafeCast(val.variants[selection]),
|
||||
config[key],
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
if (isList(val) && isObjectList(val)) {
|
||||
newVal = (config[key] as object[]).map((obj) =>
|
||||
transformOldConfigToNew(
|
||||
matchOldConfigSpec.unsafeCast(val.spec.spec),
|
||||
obj,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
if (isPointer(val)) {
|
||||
return obj
|
||||
}
|
||||
|
||||
return {
|
||||
...obj,
|
||||
[key]: newVal,
|
||||
}
|
||||
}, {})
|
||||
}
|
||||
|
||||
export function transformNewConfigToOld(
|
||||
spec: OldConfigSpec,
|
||||
config: Record<string, any>,
|
||||
): Record<string, any> {
|
||||
return Object.entries(spec).reduce((obj, [key, val]) => {
|
||||
let newVal = config[key]
|
||||
|
||||
if (isObject(val)) {
|
||||
newVal = transformNewConfigToOld(
|
||||
matchOldConfigSpec.unsafeCast(val.spec),
|
||||
config[key],
|
||||
)
|
||||
}
|
||||
|
||||
if (isUnion(val)) {
|
||||
newVal = {
|
||||
[val.tag.id]: config[key].selection,
|
||||
...transformNewConfigToOld(
|
||||
matchOldConfigSpec.unsafeCast(val.variants[config[key].selection]),
|
||||
config[key].value,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
if (isList(val) && isObjectList(val)) {
|
||||
newVal = (config[key] as object[]).map((obj) =>
|
||||
transformNewConfigToOld(
|
||||
matchOldConfigSpec.unsafeCast(val.spec.spec),
|
||||
obj,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
return {
|
||||
...obj,
|
||||
[key]: newVal,
|
||||
}
|
||||
}, {})
|
||||
}
|
||||
|
||||
function getListSpec(
|
||||
oldVal: OldValueSpecList,
|
||||
): CT.ValueSpecMultiselect | CT.ValueSpecList {
|
||||
const range = Range.from(oldVal.range)
|
||||
|
||||
let partial: Omit<CT.ValueSpecList, "type" | "spec" | "default"> = {
|
||||
name: oldVal.name,
|
||||
description: oldVal.description || null,
|
||||
warning: oldVal.warning || null,
|
||||
minLength: range.min
|
||||
? range.minInclusive
|
||||
? range.min
|
||||
: range.min + 1
|
||||
: null,
|
||||
maxLength: range.max
|
||||
? range.maxInclusive
|
||||
? range.max
|
||||
: range.max - 1
|
||||
: null,
|
||||
disabled: false,
|
||||
}
|
||||
|
||||
if (isEnumList(oldVal)) {
|
||||
return {
|
||||
...partial,
|
||||
type: "multiselect",
|
||||
default: oldVal.default as string[],
|
||||
immutable: false,
|
||||
values: oldVal.spec.values.reduce(
|
||||
(obj, curr) => ({
|
||||
...obj,
|
||||
[curr]: oldVal.spec["value-names"][curr],
|
||||
}),
|
||||
{},
|
||||
),
|
||||
}
|
||||
} else if (isStringList(oldVal)) {
|
||||
return {
|
||||
...partial,
|
||||
type: "list",
|
||||
default: oldVal.default as string[],
|
||||
spec: {
|
||||
type: "text",
|
||||
patterns:
|
||||
oldVal.spec.pattern && oldVal.spec["pattern-description"]
|
||||
? [
|
||||
{
|
||||
regex: oldVal.spec.pattern,
|
||||
description: oldVal.spec["pattern-description"],
|
||||
},
|
||||
]
|
||||
: [],
|
||||
minLength: null,
|
||||
maxLength: null,
|
||||
masked: oldVal.spec.masked || false,
|
||||
generate: null,
|
||||
inputmode: "text",
|
||||
placeholder: oldVal.spec.placeholder || null,
|
||||
},
|
||||
}
|
||||
} else if (isObjectList(oldVal)) {
|
||||
return {
|
||||
...partial,
|
||||
type: "list",
|
||||
default: oldVal.default as Record<string, unknown>[],
|
||||
spec: {
|
||||
type: "object",
|
||||
spec: transformConfigSpec(
|
||||
matchOldConfigSpec.unsafeCast(oldVal.spec.spec),
|
||||
),
|
||||
uniqueBy: oldVal.spec["unique-by"] || null,
|
||||
displayAs: oldVal.spec["display-as"] || null,
|
||||
},
|
||||
}
|
||||
} else {
|
||||
throw new Error("Invalid list subtype. enum, string, and object permitted.")
|
||||
}
|
||||
}
|
||||
|
||||
function isObject(val: OldValueSpec): val is OldValueSpecObject {
|
||||
return val.type === "object"
|
||||
}
|
||||
|
||||
function isUnion(val: OldValueSpec): val is OldValueSpecUnion {
|
||||
return val.type === "union"
|
||||
}
|
||||
|
||||
function isList(val: OldValueSpec): val is OldValueSpecList {
|
||||
return val.type === "list"
|
||||
}
|
||||
|
||||
function isPointer(val: OldValueSpec): val is OldValueSpecPointer {
|
||||
return val.type === "pointer"
|
||||
}
|
||||
|
||||
function isEnumList(
|
||||
val: OldValueSpecList,
|
||||
): val is OldValueSpecList & { subtype: "enum" } {
|
||||
return val.subtype === "enum"
|
||||
}
|
||||
|
||||
function isStringList(
|
||||
val: OldValueSpecList,
|
||||
): val is OldValueSpecList & { subtype: "string" } {
|
||||
return val.subtype === "string"
|
||||
}
|
||||
|
||||
function isObjectList(
|
||||
val: OldValueSpecList,
|
||||
): val is OldValueSpecList & { subtype: "object" } {
|
||||
if (["number", "union"].includes(val.subtype)) {
|
||||
throw new Error("Invalid list subtype. enum, string, and object permitted.")
|
||||
}
|
||||
return val.subtype === "object"
|
||||
}
|
||||
export type OldConfigSpec = Record<string, OldValueSpec>
|
||||
const [_matchOldConfigSpec, setMatchOldConfigSpec] = deferred<unknown>()
|
||||
export const matchOldConfigSpec = _matchOldConfigSpec as Parser<
|
||||
unknown,
|
||||
OldConfigSpec
|
||||
>
|
||||
export const matchOldDefaultString = anyOf(
|
||||
string,
|
||||
object({ charset: string, len: number }),
|
||||
)
|
||||
type OldDefaultString = typeof matchOldDefaultString._TYPE
|
||||
|
||||
export const matchOldValueSpecString = object(
|
||||
{
|
||||
type: literals("string"),
|
||||
name: string,
|
||||
masked: boolean,
|
||||
copyable: boolean,
|
||||
nullable: boolean,
|
||||
placeholder: string,
|
||||
pattern: string,
|
||||
"pattern-description": string,
|
||||
default: matchOldDefaultString,
|
||||
textarea: boolean,
|
||||
description: string,
|
||||
warning: string,
|
||||
},
|
||||
[
|
||||
"masked",
|
||||
"copyable",
|
||||
"nullable",
|
||||
"placeholder",
|
||||
"pattern",
|
||||
"pattern-description",
|
||||
"default",
|
||||
"textarea",
|
||||
"description",
|
||||
"warning",
|
||||
],
|
||||
)
|
||||
|
||||
export const matchOldValueSpecNumber = object(
|
||||
{
|
||||
type: literals("number"),
|
||||
nullable: boolean,
|
||||
name: string,
|
||||
range: string,
|
||||
integral: boolean,
|
||||
default: number,
|
||||
description: string,
|
||||
warning: string,
|
||||
units: string,
|
||||
placeholder: string,
|
||||
},
|
||||
["default", "description", "warning", "units", "placeholder"],
|
||||
)
|
||||
type OldValueSpecNumber = typeof matchOldValueSpecNumber._TYPE
|
||||
|
||||
export const matchOldValueSpecBoolean = object(
|
||||
{
|
||||
type: literals("boolean"),
|
||||
default: boolean,
|
||||
name: string,
|
||||
description: string,
|
||||
warning: string,
|
||||
},
|
||||
["description", "warning"],
|
||||
)
|
||||
type OldValueSpecBoolean = typeof matchOldValueSpecBoolean._TYPE
|
||||
|
||||
const matchOldValueSpecObject = object(
|
||||
{
|
||||
type: literals("object"),
|
||||
spec: _matchOldConfigSpec,
|
||||
name: string,
|
||||
description: string,
|
||||
warning: string,
|
||||
},
|
||||
["description", "warning"],
|
||||
)
|
||||
type OldValueSpecObject = typeof matchOldValueSpecObject._TYPE
|
||||
|
||||
const matchOldValueSpecEnum = object(
|
||||
{
|
||||
values: array(string),
|
||||
"value-names": dictionary([string, string]),
|
||||
type: literals("enum"),
|
||||
default: string,
|
||||
name: string,
|
||||
description: string,
|
||||
warning: string,
|
||||
},
|
||||
["description", "warning"],
|
||||
)
|
||||
type OldValueSpecEnum = typeof matchOldValueSpecEnum._TYPE
|
||||
|
||||
const matchOldUnionTagSpec = object(
|
||||
{
|
||||
id: string, // The name of the field containing one of the union variants
|
||||
"variant-names": dictionary([string, string]), // The name of each variant
|
||||
name: string,
|
||||
description: string,
|
||||
warning: string,
|
||||
},
|
||||
["description", "warning"],
|
||||
)
|
||||
const matchOldValueSpecUnion = object({
|
||||
type: literals("union"),
|
||||
tag: matchOldUnionTagSpec,
|
||||
variants: dictionary([string, _matchOldConfigSpec]),
|
||||
default: string,
|
||||
})
|
||||
type OldValueSpecUnion = typeof matchOldValueSpecUnion._TYPE
|
||||
|
||||
const [matchOldUniqueBy, setOldUniqueBy] = deferred<OldUniqueBy>()
|
||||
type OldUniqueBy =
|
||||
| null
|
||||
| string
|
||||
| { any: OldUniqueBy[] }
|
||||
| { all: OldUniqueBy[] }
|
||||
|
||||
setOldUniqueBy(
|
||||
anyOf(
|
||||
nill,
|
||||
string,
|
||||
object({ any: array(matchOldUniqueBy) }),
|
||||
object({ all: array(matchOldUniqueBy) }),
|
||||
),
|
||||
)
|
||||
|
||||
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
|
||||
"unique-by": matchOldUniqueBy, // indicates whether duplicates can be permitted in the list
|
||||
"display-as": string, // this should be a handlebars template which can make use of the entire config which corresponds to 'spec'
|
||||
},
|
||||
["display-as", "unique-by"],
|
||||
)
|
||||
const matchOldListValueSpecString = object(
|
||||
{
|
||||
masked: boolean,
|
||||
copyable: boolean,
|
||||
pattern: string,
|
||||
"pattern-description": string,
|
||||
placeholder: string,
|
||||
},
|
||||
["pattern", "pattern-description", "placeholder", "copyable", "masked"],
|
||||
)
|
||||
|
||||
const matchOldListValueSpecEnum = object({
|
||||
values: array(string),
|
||||
"value-names": dictionary([string, string]),
|
||||
})
|
||||
|
||||
// represents a spec for a list
|
||||
const matchOldValueSpecList = every(
|
||||
object(
|
||||
{
|
||||
type: literals("list"),
|
||||
range: string, // '[0,1]' (inclusive) OR '[0,*)' (right unbounded), normal math rules
|
||||
default: anyOf(
|
||||
array(string),
|
||||
array(number),
|
||||
array(matchOldDefaultString),
|
||||
array(object),
|
||||
),
|
||||
name: string,
|
||||
description: string,
|
||||
warning: string,
|
||||
},
|
||||
["description", "warning"],
|
||||
),
|
||||
anyOf(
|
||||
object({
|
||||
subtype: literals("string"),
|
||||
spec: matchOldListValueSpecString,
|
||||
}),
|
||||
object({
|
||||
subtype: literals("enum"),
|
||||
spec: matchOldListValueSpecEnum,
|
||||
}),
|
||||
object({
|
||||
subtype: literals("object"),
|
||||
spec: matchOldListValueSpecObject,
|
||||
}),
|
||||
),
|
||||
)
|
||||
type OldValueSpecList = typeof matchOldValueSpecList._TYPE
|
||||
|
||||
const matchOldValueSpecPointer = every(
|
||||
object({
|
||||
type: literal("pointer"),
|
||||
}),
|
||||
anyOf(
|
||||
object({
|
||||
subtype: literal("package"),
|
||||
target: literals("tor-key", "tor-address", "lan-address"),
|
||||
"package-id": string,
|
||||
interface: string,
|
||||
}),
|
||||
object({
|
||||
subtype: literal("package"),
|
||||
target: literals("config"),
|
||||
"package-id": string,
|
||||
selector: string,
|
||||
multi: boolean,
|
||||
}),
|
||||
),
|
||||
)
|
||||
type OldValueSpecPointer = typeof matchOldValueSpecPointer._TYPE
|
||||
|
||||
export const matchOldValueSpec = anyOf(
|
||||
matchOldValueSpecString,
|
||||
matchOldValueSpecNumber,
|
||||
matchOldValueSpecBoolean,
|
||||
matchOldValueSpecObject,
|
||||
matchOldValueSpecEnum,
|
||||
matchOldValueSpecList,
|
||||
matchOldValueSpecUnion,
|
||||
matchOldValueSpecPointer,
|
||||
)
|
||||
type OldValueSpec = typeof matchOldValueSpec._TYPE
|
||||
|
||||
setMatchOldConfigSpec(dictionary([string, matchOldValueSpec]))
|
||||
|
||||
export class Range {
|
||||
min?: number
|
||||
max?: number
|
||||
minInclusive!: boolean
|
||||
maxInclusive!: boolean
|
||||
|
||||
static from(s: string = "(*,*)"): Range {
|
||||
const r = new Range()
|
||||
r.minInclusive = s.startsWith("[")
|
||||
r.maxInclusive = s.endsWith("]")
|
||||
const [minStr, maxStr] = s.split(",").map((a) => a.trim())
|
||||
r.min = minStr === "(*" ? undefined : Number(minStr.slice(1))
|
||||
r.max = maxStr === "*)" ? undefined : Number(maxStr.slice(0, -1))
|
||||
return r
|
||||
}
|
||||
}
|
||||
@@ -1,152 +1,226 @@
|
||||
import { ExecuteResult, System } from "../../Interfaces/System"
|
||||
import { ExecuteResult, Procedure, System } from "../../Interfaces/System"
|
||||
import { unNestPath } from "../../Models/JsonPath"
|
||||
import { string } from "ts-matches"
|
||||
import { HostSystemStartOs } from "../HostSystemStartOs"
|
||||
import matches, { any, number, object, string, tuple } from "ts-matches"
|
||||
import { Effects } from "../../Models/Effects"
|
||||
import { RpcResult } from "../RpcListener"
|
||||
import { RpcResult, matchRpcResult } from "../RpcListener"
|
||||
import { duration } from "../../Models/Duration"
|
||||
const LOCATION = "/usr/lib/startos/package/startos"
|
||||
import { T } from "@start9labs/start-sdk"
|
||||
import { Volume } from "../../Models/Volume"
|
||||
import { MainEffects } from "@start9labs/start-sdk/cjs/lib/StartSdk"
|
||||
import { CallbackHolder } from "../../Models/CallbackHolder"
|
||||
|
||||
export const STARTOS_JS_LOCATION = "/usr/lib/startos/package/index.js"
|
||||
|
||||
type RunningMain = {
|
||||
effects: MainEffects
|
||||
stop: () => Promise<void>
|
||||
callbacks: CallbackHolder
|
||||
}
|
||||
|
||||
export class SystemForStartOs implements System {
|
||||
private onTerm: (() => Promise<void>) | undefined
|
||||
private runningMain: RunningMain | undefined
|
||||
|
||||
static of() {
|
||||
return new SystemForStartOs()
|
||||
return new SystemForStartOs(require(STARTOS_JS_LOCATION))
|
||||
}
|
||||
constructor() {}
|
||||
|
||||
constructor(readonly abi: T.ABI) {}
|
||||
|
||||
async init(): Promise<void> {}
|
||||
|
||||
async exit(): Promise<void> {}
|
||||
|
||||
async start(effects: MainEffects): Promise<void> {
|
||||
if (this.runningMain) await this.stop()
|
||||
let mainOnTerm: () => Promise<void> | undefined
|
||||
const started = async (onTerm: () => Promise<void>) => {
|
||||
await effects.setMainStatus({ status: "running" })
|
||||
mainOnTerm = onTerm
|
||||
}
|
||||
const daemons = await (
|
||||
await this.abi.main({
|
||||
effects: effects as MainEffects,
|
||||
started,
|
||||
})
|
||||
).build()
|
||||
this.runningMain = {
|
||||
effects,
|
||||
stop: async () => {
|
||||
if (mainOnTerm) await mainOnTerm()
|
||||
await daemons.term()
|
||||
},
|
||||
callbacks: new CallbackHolder(),
|
||||
}
|
||||
}
|
||||
|
||||
callCallback(callback: number, args: any[]): void {
|
||||
if (this.runningMain) {
|
||||
this.runningMain.callbacks
|
||||
.callCallback(callback, args)
|
||||
.catch((error) => console.error(`callback ${callback} failed`, error))
|
||||
} else {
|
||||
console.warn(`callback ${callback} ignored because system is not running`)
|
||||
}
|
||||
}
|
||||
|
||||
async stop(): Promise<void> {
|
||||
if (this.runningMain) {
|
||||
await this.runningMain.stop()
|
||||
await this.runningMain.effects.clearCallbacks()
|
||||
this.runningMain = undefined
|
||||
}
|
||||
}
|
||||
|
||||
async execute(
|
||||
effects: HostSystemStartOs,
|
||||
effects: Effects,
|
||||
options: {
|
||||
procedure:
|
||||
| "/init"
|
||||
| "/uninit"
|
||||
| "/main/start"
|
||||
| "/main/stop"
|
||||
| "/config/set"
|
||||
| "/config/get"
|
||||
| "/backup/create"
|
||||
| "/backup/restore"
|
||||
| "/actions/metadata"
|
||||
| `/actions/${string}/get`
|
||||
| `/actions/${string}/run`
|
||||
| `/dependencies/${string}/query`
|
||||
| `/dependencies/${string}/update`
|
||||
input: unknown
|
||||
procedure: Procedure
|
||||
input?: unknown
|
||||
timeout?: number | undefined
|
||||
},
|
||||
): Promise<RpcResult> {
|
||||
return { result: await this._execute(effects, options) }
|
||||
return this._execute(effects, options)
|
||||
.then((x) =>
|
||||
matches(x)
|
||||
.when(
|
||||
object({
|
||||
result: any,
|
||||
}),
|
||||
(x) => x,
|
||||
)
|
||||
.when(
|
||||
object({
|
||||
error: string,
|
||||
}),
|
||||
(x) => ({
|
||||
error: {
|
||||
code: 0,
|
||||
message: x.error,
|
||||
},
|
||||
}),
|
||||
)
|
||||
.when(
|
||||
object({
|
||||
"error-code": tuple(number, string),
|
||||
}),
|
||||
({ "error-code": [code, message] }) => ({
|
||||
error: {
|
||||
code,
|
||||
message,
|
||||
},
|
||||
}),
|
||||
)
|
||||
.defaultTo({ result: x }),
|
||||
)
|
||||
.catch((error: unknown) => {
|
||||
if (error instanceof Error)
|
||||
return {
|
||||
error: {
|
||||
code: 0,
|
||||
message: error.name,
|
||||
data: {
|
||||
details: error.message,
|
||||
debug: `${error?.cause ?? "[noCause]"}:${error?.stack ?? "[noStack]"}`,
|
||||
},
|
||||
},
|
||||
}
|
||||
if (matchRpcResult.test(error)) return error
|
||||
return {
|
||||
error: {
|
||||
code: 0,
|
||||
message: String(error),
|
||||
},
|
||||
}
|
||||
})
|
||||
}
|
||||
async _execute(
|
||||
effects: Effects,
|
||||
effects: Effects | MainEffects,
|
||||
options: {
|
||||
procedure:
|
||||
| "/init"
|
||||
| "/uninit"
|
||||
| "/main/start"
|
||||
| "/main/stop"
|
||||
| "/config/set"
|
||||
| "/config/get"
|
||||
| "/backup/create"
|
||||
| "/backup/restore"
|
||||
| "/actions/metadata"
|
||||
| `/actions/${string}/get`
|
||||
| `/actions/${string}/run`
|
||||
| `/dependencies/${string}/query`
|
||||
| `/dependencies/${string}/update`
|
||||
input: unknown
|
||||
procedure: Procedure
|
||||
input?: unknown
|
||||
timeout?: number | undefined
|
||||
},
|
||||
): Promise<unknown> {
|
||||
switch (options.procedure) {
|
||||
case "/init": {
|
||||
const path = `${LOCATION}/procedures/init`
|
||||
const procedure: any = await import(path).catch(() => require(path))
|
||||
const previousVersion = string.optional().unsafeCast(options)
|
||||
return procedure.init({ effects, previousVersion })
|
||||
const previousVersion =
|
||||
string.optional().unsafeCast(options.input) || null
|
||||
return this.abi.init({ effects, previousVersion })
|
||||
}
|
||||
case "/uninit": {
|
||||
const path = `${LOCATION}/procedures/init`
|
||||
const procedure: any = await import(path).catch(() => require(path))
|
||||
const nextVersion = string.optional().unsafeCast(options)
|
||||
return procedure.uninit({ effects, nextVersion })
|
||||
}
|
||||
case "/main/start": {
|
||||
const path = `${LOCATION}/procedures/main`
|
||||
const procedure: any = await import(path).catch(() => require(path))
|
||||
const started = async (onTerm: () => Promise<void>) => {
|
||||
await effects.setMainStatus({ status: "running" })
|
||||
if (this.onTerm) await this.onTerm()
|
||||
this.onTerm = onTerm
|
||||
}
|
||||
return procedure.main({ effects, started })
|
||||
}
|
||||
case "/main/stop": {
|
||||
await effects.setMainStatus({ status: "stopped" })
|
||||
if (this.onTerm) await this.onTerm()
|
||||
delete this.onTerm
|
||||
return duration(30, "s")
|
||||
const nextVersion = string.optional().unsafeCast(options.input) || null
|
||||
return this.abi.uninit({ effects, nextVersion })
|
||||
}
|
||||
// case "/main/start": {
|
||||
//
|
||||
// }
|
||||
// case "/main/stop": {
|
||||
// if (this.onTerm) await this.onTerm()
|
||||
// await effects.setMainStatus({ status: "stopped" })
|
||||
// delete this.onTerm
|
||||
// return duration(30, "s")
|
||||
// }
|
||||
case "/config/set": {
|
||||
const path = `${LOCATION}/procedures/config`
|
||||
const procedure: any = await import(path).catch(() => require(path))
|
||||
const input = options.input
|
||||
return procedure.setConfig({ effects, input })
|
||||
const input = options.input as any // TODO
|
||||
return this.abi.setConfig({ effects, input })
|
||||
}
|
||||
case "/config/get": {
|
||||
const path = `${LOCATION}/procedures/config`
|
||||
const procedure: any = await import(path).catch(() => require(path))
|
||||
return procedure.getConfig({ effects })
|
||||
return this.abi.getConfig({ effects })
|
||||
}
|
||||
case "/backup/create":
|
||||
return this.abi.createBackup({
|
||||
effects,
|
||||
pathMaker: ((options) =>
|
||||
new Volume(options.volume, options.path).path) as T.PathMaker,
|
||||
})
|
||||
case "/backup/restore":
|
||||
throw new Error("this should be called with the init/unit")
|
||||
return this.abi.restoreBackup({
|
||||
effects,
|
||||
pathMaker: ((options) =>
|
||||
new Volume(options.volume, options.path).path) as T.PathMaker,
|
||||
})
|
||||
case "/actions/metadata": {
|
||||
const path = `${LOCATION}/procedures/actions`
|
||||
const procedure: any = await import(path).catch(() => require(path))
|
||||
return procedure.actionsMetadata({ effects })
|
||||
return this.abi.actionsMetadata({ effects })
|
||||
}
|
||||
case "/properties": {
|
||||
throw new Error("TODO")
|
||||
}
|
||||
default:
|
||||
const procedures = unNestPath(options.procedure)
|
||||
const id = procedures[2]
|
||||
switch (true) {
|
||||
case procedures[1] === "actions" && procedures[3] === "get": {
|
||||
const path = `${LOCATION}/procedures/actions`
|
||||
const action: any = (await import(path).catch(() => require(path)))
|
||||
.actions[id]
|
||||
const action = (await this.abi.actions({ effects }))[id]
|
||||
if (!action) throw new Error(`Action ${id} not found`)
|
||||
return action.get({ effects })
|
||||
return action.getConfig({ effects })
|
||||
}
|
||||
case procedures[1] === "actions" && procedures[3] === "run": {
|
||||
const path = `${LOCATION}/procedures/actions`
|
||||
const action: any = (await import(path).catch(() => require(path)))
|
||||
.actions[id]
|
||||
const action = (await this.abi.actions({ effects }))[id]
|
||||
if (!action) throw new Error(`Action ${id} not found`)
|
||||
const input = options.input
|
||||
return action.run({ effects, input })
|
||||
return action.run({ effects, input: options.input as any }) // TODO
|
||||
}
|
||||
case procedures[1] === "dependencies" && procedures[3] === "query": {
|
||||
const path = `${LOCATION}/procedures/dependencies`
|
||||
const dependencyConfig: any = (
|
||||
await import(path).catch(() => require(path))
|
||||
).dependencyConfig[id]
|
||||
const dependencyConfig = this.abi.dependencyConfig[id]
|
||||
if (!dependencyConfig)
|
||||
throw new Error(`dependencyConfig ${id} not found`)
|
||||
const localConfig = options.input
|
||||
return dependencyConfig.query({ effects, localConfig })
|
||||
return dependencyConfig.query({ effects })
|
||||
}
|
||||
case procedures[1] === "dependencies" && procedures[3] === "update": {
|
||||
const path = `${LOCATION}/procedures/dependencies`
|
||||
const dependencyConfig: any = (
|
||||
await import(path).catch(() => require(path))
|
||||
).dependencyConfig[id]
|
||||
const dependencyConfig = this.abi.dependencyConfig[id]
|
||||
if (!dependencyConfig)
|
||||
throw new Error(`dependencyConfig ${id} not found`)
|
||||
return dependencyConfig.update(options.input)
|
||||
return dependencyConfig.update(options.input as any) // TODO
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
throw new Error("Method not implemented.")
|
||||
}
|
||||
exit(effects: Effects): Promise<void> {
|
||||
throw new Error("Method not implemented.")
|
||||
|
||||
async sandbox(
|
||||
effects: Effects,
|
||||
options: { procedure: Procedure; input?: unknown; timeout?: number },
|
||||
): Promise<RpcResult> {
|
||||
return this.execute(effects, options)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,22 @@
|
||||
import * as fs from "node:fs/promises"
|
||||
import { System } from "../../Interfaces/System"
|
||||
import { SystemForEmbassy } from "./SystemForEmbassy"
|
||||
import { SystemForStartOs } from "./SystemForStartOs"
|
||||
import { EMBASSY_JS_LOCATION, SystemForEmbassy } from "./SystemForEmbassy"
|
||||
import { STARTOS_JS_LOCATION, SystemForStartOs } from "./SystemForStartOs"
|
||||
export async function getSystem(): Promise<System> {
|
||||
return SystemForEmbassy.of()
|
||||
if (
|
||||
await fs.access(STARTOS_JS_LOCATION).then(
|
||||
() => true,
|
||||
() => false,
|
||||
)
|
||||
) {
|
||||
return SystemForStartOs.of()
|
||||
} else if (
|
||||
await fs.access(EMBASSY_JS_LOCATION).then(
|
||||
() => true,
|
||||
() => false,
|
||||
)
|
||||
) {
|
||||
return SystemForEmbassy.of()
|
||||
}
|
||||
throw new Error(`${STARTOS_JS_LOCATION} not found`)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { GetDependency } from "./GetDependency"
|
||||
import { System } from "./System"
|
||||
import { GetHostSystem, HostSystem } from "./HostSystem"
|
||||
import { MakeMainEffects, MakeProcedureEffects } from "./MakeEffects"
|
||||
|
||||
export type AllGetDependencies = GetDependency<"system", Promise<System>> &
|
||||
GetDependency<"hostSystem", GetHostSystem>
|
||||
GetDependency<"makeProcedureEffects", MakeProcedureEffects> &
|
||||
GetDependency<"makeMainEffects", MakeMainEffects>
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
import { types as T } from "@start9labs/start-sdk"
|
||||
|
||||
import { CallbackHolder } from "../Models/CallbackHolder"
|
||||
import { Effects } from "../Models/Effects"
|
||||
|
||||
export type HostSystem = Effects
|
||||
export type GetHostSystem = (callbackHolder: CallbackHolder) => HostSystem
|
||||
4
container-runtime/src/Interfaces/MakeEffects.ts
Normal file
4
container-runtime/src/Interfaces/MakeEffects.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import { Effects } from "../Models/Effects"
|
||||
import { MainEffects } from "@start9labs/start-sdk/cjs/lib/StartSdk"
|
||||
export type MakeProcedureEffects = (procedureId: string) => Effects
|
||||
export type MakeMainEffects = () => MainEffects
|
||||
@@ -1,32 +1,54 @@
|
||||
import { types as T } from "@start9labs/start-sdk"
|
||||
import { JsonPath } from "../Models/JsonPath"
|
||||
import { HostSystemStartOs } from "../Adapters/HostSystemStartOs"
|
||||
import { RpcResult } from "../Adapters/RpcListener"
|
||||
import { Effects } from "../Models/Effects"
|
||||
import { CallbackHolder } from "../Models/CallbackHolder"
|
||||
import { MainEffects } from "@start9labs/start-sdk/cjs/lib/StartSdk"
|
||||
|
||||
export type Procedure =
|
||||
| "/init"
|
||||
| "/uninit"
|
||||
| "/config/set"
|
||||
| "/config/get"
|
||||
| "/backup/create"
|
||||
| "/backup/restore"
|
||||
| "/actions/metadata"
|
||||
| "/properties"
|
||||
| `/actions/${string}/get`
|
||||
| `/actions/${string}/run`
|
||||
| `/dependencies/${string}/query`
|
||||
| `/dependencies/${string}/update`
|
||||
|
||||
export type ExecuteResult =
|
||||
| { ok: unknown }
|
||||
| { err: { code: number; message: string } }
|
||||
export interface System {
|
||||
// init(effects: Effects): Promise<void>
|
||||
// exit(effects: Effects): Promise<void>
|
||||
// start(effects: Effects): Promise<void>
|
||||
// stop(effects: Effects, options: { timeout: number, signal?: number }): Promise<void>
|
||||
export type System = {
|
||||
init(): Promise<void>
|
||||
|
||||
start(effects: MainEffects): Promise<void>
|
||||
callCallback(callback: number, args: any[]): void
|
||||
stop(): Promise<void>
|
||||
|
||||
execute(
|
||||
effects: T.Effects,
|
||||
effects: Effects,
|
||||
options: {
|
||||
procedure: JsonPath
|
||||
procedure: Procedure
|
||||
input: unknown
|
||||
timeout?: number
|
||||
},
|
||||
): Promise<RpcResult>
|
||||
sandbox(
|
||||
effects: Effects,
|
||||
options: {
|
||||
procedure: Procedure
|
||||
input: unknown
|
||||
timeout?: number
|
||||
},
|
||||
): Promise<RpcResult>
|
||||
// sandbox(
|
||||
// effects: Effects,
|
||||
// options: {
|
||||
// procedure: JsonPath
|
||||
// input: unknown
|
||||
// timeout?: number
|
||||
// },
|
||||
// ): Promise<unknown>
|
||||
|
||||
exit(effects: T.Effects): Promise<void>
|
||||
exit(): Promise<void>
|
||||
}
|
||||
|
||||
export type RunningMain = {
|
||||
callbacks: CallbackHolder
|
||||
stop(): Promise<void>
|
||||
}
|
||||
|
||||
@@ -1,17 +1,19 @@
|
||||
export class CallbackHolder {
|
||||
constructor() {}
|
||||
private root = (Math.random() + 1).toString(36).substring(7)
|
||||
private inc = 0
|
||||
private callbacks = new Map<string, Function>()
|
||||
private callbacks = new Map<number, Function>()
|
||||
private newId() {
|
||||
return this.root + (this.inc++).toString(36)
|
||||
return this.inc++
|
||||
}
|
||||
addCallback(callback: Function) {
|
||||
addCallback(callback?: Function) {
|
||||
if (!callback) {
|
||||
return
|
||||
}
|
||||
const id = this.newId()
|
||||
this.callbacks.set(id, callback)
|
||||
return id
|
||||
}
|
||||
callCallback(index: string, args: any[]): Promise<unknown> {
|
||||
callCallback(index: number, args: any[]): Promise<unknown> {
|
||||
const callback = this.callbacks.get(index)
|
||||
if (!callback) throw new Error(`Callback ${index} does not exist`)
|
||||
this.callbacks.delete(index)
|
||||
|
||||
@@ -8,7 +8,9 @@ import {
|
||||
literals,
|
||||
number,
|
||||
Parser,
|
||||
some,
|
||||
} from "ts-matches"
|
||||
import { matchDuration } from "./Duration"
|
||||
|
||||
const VolumeId = string
|
||||
const Path = string
|
||||
@@ -31,7 +33,7 @@ export const matchDockerProcedure = object(
|
||||
"toml",
|
||||
"toml-pretty",
|
||||
),
|
||||
"sigterm-timeout": number,
|
||||
"sigterm-timeout": some(number, matchDuration),
|
||||
inject: boolean,
|
||||
},
|
||||
["io-format", "sigterm-timeout", "system", "args", "inject", "mounts"],
|
||||
|
||||
@@ -1,6 +1,30 @@
|
||||
export type TimeUnit = "d" | "h" | "s" | "ms"
|
||||
import { string } from "ts-matches"
|
||||
|
||||
export type TimeUnit = "d" | "h" | "s" | "ms" | "m" | "µs" | "ns"
|
||||
export type Duration = `${number}${TimeUnit}`
|
||||
|
||||
export function duration(timeValue: number, timeUnit: TimeUnit = "s") {
|
||||
return `${timeValue}${timeUnit}` as Duration
|
||||
const durationRegex = /^([0-9]*(\.[0-9]+)?)(ns|µs|ms|s|m|d)$/
|
||||
|
||||
export const matchDuration = string.refine(isDuration)
|
||||
export function isDuration(value: string): value is Duration {
|
||||
return durationRegex.test(value)
|
||||
}
|
||||
|
||||
export function duration(timeValue: number, timeUnit: TimeUnit = "s") {
|
||||
return `${timeValue > 0 ? timeValue : 0}${timeUnit}` as Duration
|
||||
}
|
||||
const unitsToSeconds: Record<string, number> = {
|
||||
ns: 1e-9,
|
||||
µs: 1e-6,
|
||||
ms: 0.001,
|
||||
s: 1,
|
||||
m: 60,
|
||||
h: 3600,
|
||||
d: 86400,
|
||||
}
|
||||
|
||||
export function fromDuration(duration: Duration | number): number {
|
||||
if (typeof duration === "number") return duration
|
||||
const [, num, , unit] = duration.match(durationRegex) || []
|
||||
return Number(num) * unitsToSeconds[unit]
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { types as T } from "@start9labs/start-sdk"
|
||||
|
||||
export type Effects = T.Effects & {
|
||||
setMainStatus(o: { status: "running" | "stopped" }): Promise<void>
|
||||
}
|
||||
export type Effects = T.Effects
|
||||
|
||||
@@ -28,8 +28,6 @@ export const jsonPath = some(
|
||||
literals(
|
||||
"/init",
|
||||
"/uninit",
|
||||
"/main/start",
|
||||
"/main/stop",
|
||||
"/config/set",
|
||||
"/config/get",
|
||||
"/backup/create",
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
import * as fs from "node:fs/promises"
|
||||
|
||||
export const BACKUP = "backup"
|
||||
export class Volume {
|
||||
readonly path: string
|
||||
constructor(
|
||||
readonly volumeId: string,
|
||||
_path = "",
|
||||
) {
|
||||
const path = (this.path = `/media/startos/volumes/${volumeId}${
|
||||
!_path ? "" : `/${_path}`
|
||||
}`)
|
||||
if (volumeId.toLowerCase() === BACKUP) {
|
||||
this.path = `/media/startos/backup${!_path ? "" : `/${_path}`}`
|
||||
} else {
|
||||
this.path = `/media/startos/volumes/${volumeId}${!_path ? "" : `/${_path}`}`
|
||||
}
|
||||
}
|
||||
async exists() {
|
||||
return fs.stat(this.path).then(
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import { RpcListener } from "./Adapters/RpcListener"
|
||||
import { SystemForEmbassy } from "./Adapters/Systems/SystemForEmbassy"
|
||||
import { HostSystemStartOs } from "./Adapters/HostSystemStartOs"
|
||||
import { makeMainEffects, makeProcedureEffects } from "./Adapters/EffectCreator"
|
||||
import { AllGetDependencies } from "./Interfaces/AllGetDependencies"
|
||||
import { getSystem } from "./Adapters/Systems"
|
||||
|
||||
const getDependencies: AllGetDependencies = {
|
||||
system: getSystem,
|
||||
hostSystem: () => HostSystemStartOs.of,
|
||||
makeProcedureEffects: () => makeProcedureEffects,
|
||||
makeMainEffects: () => makeMainEffects,
|
||||
}
|
||||
|
||||
new RpcListener(getDependencies)
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
{
|
||||
"include": [
|
||||
"./**/*.mjs",
|
||||
"./**/*.js",
|
||||
"src/Adapters/RpcListener.ts",
|
||||
"src/index.ts",
|
||||
"effects.ts"
|
||||
],
|
||||
"include": ["./**/*.ts"],
|
||||
"exclude": ["dist"],
|
||||
"inputs": ["./src/index.ts"],
|
||||
"compilerOptions": {
|
||||
@@ -19,9 +13,10 @@
|
||||
"declaration": true,
|
||||
"noImplicitAny": true,
|
||||
"esModuleInterop": true,
|
||||
"types": ["node"],
|
||||
"types": ["node", "jest"],
|
||||
"moduleResolution": "Node16",
|
||||
"skipLibCheck": true
|
||||
"skipLibCheck": true,
|
||||
"resolveJsonModule": true
|
||||
},
|
||||
"ts-node": {
|
||||
"compilerOptions": {
|
||||
|
||||
@@ -4,12 +4,15 @@ cd "$(dirname "${BASH_SOURCE[0]}")"
|
||||
|
||||
set -e
|
||||
|
||||
|
||||
|
||||
if mountpoint tmp/combined; then sudo umount tmp/combined; fi
|
||||
if mountpoint tmp/combined; then sudo umount -R tmp/combined; fi
|
||||
if mountpoint tmp/lower; then sudo umount tmp/lower; fi
|
||||
sudo rm -rf tmp
|
||||
mkdir -p tmp/lower tmp/upper tmp/work tmp/combined
|
||||
sudo mount alpine.${ARCH}.squashfs tmp/lower
|
||||
if which squashfuse > /dev/null; then
|
||||
sudo squashfuse debian.${ARCH}.squashfs tmp/lower
|
||||
else
|
||||
sudo mount debian.${ARCH}.squashfs tmp/lower
|
||||
fi
|
||||
sudo mount -t overlay -olowerdir=tmp/lower,upperdir=tmp/upper,workdir=tmp/work overlay tmp/combined
|
||||
|
||||
QEMU=
|
||||
@@ -18,21 +21,21 @@ if [ "$ARCH" != "$(uname -m)" ]; then
|
||||
sudo cp $(which qemu-$ARCH-static) tmp/combined${QEMU}
|
||||
fi
|
||||
|
||||
echo "nameserver 8.8.8.8" | sudo tee tmp/combined/etc/resolv.conf # TODO - delegate to host resolver?
|
||||
sudo chroot tmp/combined $QEMU /sbin/apk add nodejs
|
||||
sudo mkdir -p tmp/combined/usr/lib/startos/
|
||||
sudo rsync -a --copy-unsafe-links dist/ tmp/combined/usr/lib/startos/init/
|
||||
sudo cp containerRuntime.rc tmp/combined/etc/init.d/containerRuntime
|
||||
sudo chown -R 0:0 tmp/combined/usr/lib/startos/
|
||||
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 cp ../core/target/$ARCH-unknown-linux-musl/release/containerbox tmp/combined/usr/bin/start-cli
|
||||
sudo chmod +x tmp/combined/etc/init.d/containerRuntime
|
||||
sudo chroot tmp/combined $QEMU /sbin/rc-update add containerRuntime default
|
||||
sudo chown 0:0 tmp/combined/usr/bin/start-cli
|
||||
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
|
||||
sudo truncate -s 0 tmp/combined/etc/machine-id
|
||||
|
||||
if [ -n "$QEMU" ]; then
|
||||
sudo rm tmp/combined${QEMU}
|
||||
fi
|
||||
|
||||
sudo truncate -s 0 tmp/combined/etc/resolv.conf
|
||||
sudo chown -R 0:0 tmp/combined
|
||||
rm -f rootfs.${ARCH}.squashfs
|
||||
mkdir -p ../build/lib/container-runtime
|
||||
sudo mksquashfs tmp/combined rootfs.${ARCH}.squashfs
|
||||
|
||||
Reference in New Issue
Block a user