233 lines
7.1 KiB
TypeScript
233 lines
7.1 KiB
TypeScript
import { storeJson } from './fileModels/storeJson'
|
|
import { i18n } from './i18n'
|
|
import { sdk } from './sdk'
|
|
import {
|
|
APP_USER,
|
|
BITCOIND_RPC_HOSTNAME,
|
|
BITCOIND_RPC_PORT,
|
|
dataDir,
|
|
SPACED_CHAIN,
|
|
spacedRpcPort,
|
|
uiPort,
|
|
} from './utils'
|
|
|
|
export const main = sdk.setupMain(async ({ effects }) => {
|
|
console.info(i18n('Starting Spaces!'))
|
|
|
|
const store = await storeJson.read().const(effects)
|
|
if (!store?.password || !store?.btcAuth) {
|
|
// taskSetPassword + taskBtcAuth both seed these in init; if they aren't
|
|
// populated yet, init hasn't finished — let StartOS restart us.
|
|
throw new Error('Spaces store.json is not yet populated.')
|
|
}
|
|
|
|
const { password: APP_PASSWORD, btcAuth } = store
|
|
|
|
const spacedEnv = {
|
|
SPACED_CHAIN,
|
|
SPACED_DATA_DIR: dataDir,
|
|
SPACED_RPC_BIND: '127.0.0.1',
|
|
SPACED_RPC_PORT: String(spacedRpcPort),
|
|
SPACED_RPC_URL: `http://127.0.0.1:${spacedRpcPort}`,
|
|
SPACED_BLOCK_INDEX: 'true',
|
|
SPACED_BITCOIN_RPC_URL: `http://${BITCOIND_RPC_HOSTNAME}:${BITCOIND_RPC_PORT}`,
|
|
SPACED_BITCOIN_RPC_USER: btcAuth.username,
|
|
SPACED_BITCOIN_RPC_PASSWORD: btcAuth.password,
|
|
SPACED_RPC_COOKIE: `/data/mainnet/.cookie`,
|
|
// legacy aliases for `bitcoin-cli` / shell helpers that read these names
|
|
BTC_RPC_HOST: BITCOIND_RPC_HOSTNAME,
|
|
BTC_RPC_PORT: String(BITCOIND_RPC_PORT),
|
|
BTC_RPC_USER: btcAuth.username,
|
|
BTC_RPC_PASSWORD: btcAuth.password,
|
|
APP_USER,
|
|
APP_PASSWORD,
|
|
}
|
|
|
|
const mounts = sdk.Mounts.of().mountVolume({
|
|
volumeId: 'main',
|
|
subpath: null,
|
|
mountpoint: dataDir,
|
|
readonly: false,
|
|
})
|
|
|
|
const spacedSub = await sdk.SubContainer.of(
|
|
effects,
|
|
{ imageId: 'spaces' },
|
|
mounts,
|
|
'spaced-sub',
|
|
)
|
|
|
|
const termSub = await sdk.SubContainer.of(
|
|
effects,
|
|
{ imageId: 'spaces' },
|
|
mounts,
|
|
'terminal-sub',
|
|
)
|
|
|
|
const bashrc = [
|
|
'export PATH=/root/.cargo/bin:/data/bin:/usr/local/bin:/usr/bin:/bin',
|
|
"export PS1='spaces:\\w$ '",
|
|
`alias spaces='space-cli --chain ${SPACED_CHAIN} --rpc-cookie ${dataDir}/${SPACED_CHAIN}/.cookie '`,
|
|
'cat <<EOF',
|
|
'',
|
|
'┌─ Spaces ─────────────────────────────────────────────────┐',
|
|
'│ spaced is managed by StartOS — do not run it manually. │',
|
|
'│ Use the `spaces` alias to call space-cli, e.g. │',
|
|
'│ spaces getserverinfo │',
|
|
'│ Docs: https://docs.spacesprotocol.org/ │',
|
|
'└──────────────────────────────────────────────────────────┘',
|
|
'',
|
|
'EOF',
|
|
].join('\n')
|
|
|
|
return sdk.Daemons.of(effects)
|
|
.addOneshot('bashrc', {
|
|
subcontainer: termSub,
|
|
exec: {
|
|
command: ['bash', '-c', `cat > /root/.bashrc <<'SPACES_BASHRC_EOF'
|
|
${bashrc}
|
|
SPACES_BASHRC_EOF`],
|
|
user: 'root',
|
|
},
|
|
requires: [],
|
|
})
|
|
.addDaemon('spaced', {
|
|
subcontainer: spacedSub,
|
|
exec: {
|
|
command: ['/root/.cargo/bin/spaced'],
|
|
env: spacedEnv,
|
|
},
|
|
ready: {
|
|
display: i18n('Spaced RPC'),
|
|
fn: () =>
|
|
sdk.healthCheck.checkPortListening(effects, spacedRpcPort, {
|
|
successMessage: i18n('spaced RPC is ready'),
|
|
errorMessage: i18n('spaced RPC is not ready'),
|
|
}),
|
|
gracePeriod: 120_000,
|
|
},
|
|
requires: [],
|
|
})
|
|
.addDaemon('web-terminal', {
|
|
subcontainer: termSub,
|
|
exec: {
|
|
command: [
|
|
'gotty',
|
|
'--port',
|
|
String(uiPort),
|
|
'-c',
|
|
`${APP_USER}:${APP_PASSWORD}`,
|
|
'--permit-write',
|
|
'--reconnect',
|
|
'/bin/bash',
|
|
],
|
|
env: spacedEnv,
|
|
},
|
|
ready: {
|
|
display: i18n('Web Interface'),
|
|
fn: () =>
|
|
sdk.healthCheck.checkPortListening(effects, uiPort, {
|
|
successMessage: i18n('The web terminal is ready'),
|
|
errorMessage: i18n('The web terminal is not ready'),
|
|
}),
|
|
},
|
|
requires: ['bashrc'],
|
|
})
|
|
.addHealthCheck('sync', {
|
|
ready: {
|
|
display: i18n('Spaced Sync'),
|
|
fn: async () => {
|
|
try {
|
|
const probe = await spacedSub.exec(
|
|
[
|
|
'/root/.cargo/bin/space-cli',
|
|
'--chain',
|
|
SPACED_CHAIN,
|
|
'--rpc-cookie',
|
|
`${dataDir}/${SPACED_CHAIN}/.cookie`,
|
|
'--output-format',
|
|
'json',
|
|
'getserverinfo',
|
|
],
|
|
{},
|
|
)
|
|
|
|
const stderr = (probe.stderr ?? '').toString().trim()
|
|
const stdoutText = (probe.stdout ?? '').toString()
|
|
const stdoutTrimmed = stdoutText.trim()
|
|
|
|
if (probe.exitCode !== 0) {
|
|
return {
|
|
result: 'failure',
|
|
message: i18n('space-cli exited ${code}: ${error}', {
|
|
code: String(probe.exitCode),
|
|
error: (stderr || stdoutTrimmed || '<no output>').slice(0, 200),
|
|
}),
|
|
}
|
|
}
|
|
|
|
let parsed: {
|
|
ready?: boolean
|
|
progress?: number
|
|
chain?: { blocks?: number; headers?: number }
|
|
}
|
|
try {
|
|
parsed = JSON.parse(stdoutText)
|
|
} catch {
|
|
return {
|
|
result: 'failure',
|
|
message: i18n(
|
|
'getserverinfo non-JSON. stdout=${stdout} stderr=${stderr}',
|
|
{
|
|
stdout: (stdoutTrimmed || '<empty>').slice(0, 160),
|
|
stderr: (stderr || '<empty>').slice(0, 160),
|
|
},
|
|
),
|
|
}
|
|
}
|
|
|
|
const progress = Math.min(
|
|
100,
|
|
Math.max(0, Math.round((parsed.progress ?? 0) * 100)),
|
|
)
|
|
const blocks = parsed.chain?.blocks ?? 0
|
|
const headers = parsed.chain?.headers ?? 0
|
|
|
|
if (parsed.ready === true && progress >= 100) {
|
|
return {
|
|
result: 'success',
|
|
message: i18n(
|
|
'spaced is fully synced (blocks ${blocks} / headers ${headers}).',
|
|
{ blocks: String(blocks), headers: String(headers) },
|
|
),
|
|
}
|
|
}
|
|
|
|
return {
|
|
result: 'loading',
|
|
message: i18n(
|
|
'spaced is indexing: ${pct}% (blocks ${blocks} / headers ${headers}).',
|
|
{
|
|
pct: String(progress),
|
|
blocks: String(blocks),
|
|
headers: String(headers),
|
|
},
|
|
),
|
|
}
|
|
} catch (e) {
|
|
return {
|
|
result: 'failure',
|
|
message: i18n(
|
|
'Spaced Sync health check crashed: ${error}',
|
|
{ error: (e as Error)?.message ?? String(e) },
|
|
),
|
|
}
|
|
}
|
|
},
|
|
gracePeriod: 30_000,
|
|
trigger: sdk.trigger.cooldownTrigger(30_000),
|
|
},
|
|
requires: ['spaced'],
|
|
})
|
|
})
|