chore: log context update

This commit is contained in:
Ben Allfree 2023-06-14 11:52:07 -07:00
parent ac5948be40
commit 56aff5db0a
28 changed files with 449 additions and 324 deletions

View File

@ -21,6 +21,7 @@ export const createWatchHelper = (config: WatchHelperConfig) => {
const watchById = safeCatch( const watchById = safeCatch(
`watchById`, `watchById`,
logger(),
async <TRec>( async <TRec>(
collectionName: string, collectionName: string,
id: RecordId, id: RecordId,
@ -93,6 +94,7 @@ export const createWatchHelper = (config: WatchHelperConfig) => {
const watchAllById = safeCatch( const watchAllById = safeCatch(
`watchAllById`, `watchAllById`,
logger(),
async <TRec extends BaseFields>( async <TRec extends BaseFields>(
collectionName: string, collectionName: string,
idName: keyof TRec, idName: keyof TRec,

View File

@ -1,9 +1,10 @@
import { ClientResponseError } from 'pocketbase' import { ClientResponseError } from 'pocketbase'
import { logger } from './Logger' import { Logger } from './Logger'
let c = 0 let c = 0
export const safeCatch = <TIn extends any[], TOut>( export const safeCatch = <TIn extends any[], TOut>(
name: string, name: string,
logger: Logger,
cb: (...args: TIn) => Promise<TOut>, cb: (...args: TIn) => Promise<TOut>,
timeoutMs = 5000 timeoutMs = 5000
) => { ) => {
@ -11,7 +12,7 @@ export const safeCatch = <TIn extends any[], TOut>(
const _c = c++ const _c = c++
const uuid = `${name}:${_c}` const uuid = `${name}:${_c}`
const pfx = `safeCatch:${uuid}` const pfx = `safeCatch:${uuid}`
const { raw, error, warn, dbg } = logger().create(pfx) const { raw, error, warn, dbg } = logger.create(pfx)
raw(`args`, args) raw(`args`, args)
const tid = setTimeout(() => { const tid = setTimeout(() => {
error(`timeout ${timeoutMs}ms waiting for ${pfx}`) error(`timeout ${timeoutMs}ms waiting for ${pfx}`)

View File

@ -16,17 +16,18 @@ import {
rpcService, rpcService,
sqliteService, sqliteService,
} from '$services' } from '$services'
import { logger } from '@pockethost/common' import { logger as loggerService } from '@pockethost/common'
import { exec } from 'child_process' import { exec } from 'child_process'
import { centralDbService } from './services/CentralDbService' import { centralDbService } from './services/CentralDbService'
import { instanceLoggerService } from './services/InstanceLoggerService' import { instanceLoggerService } from './services/InstanceLoggerService'
logger({ debug: DEBUG, trace: TRACE, errorTrace: !DEBUG }) loggerService({ debug: DEBUG, trace: TRACE, errorTrace: !DEBUG })
// npm install eventsource --save // npm install eventsource --save
global.EventSource = require('eventsource') global.EventSource = require('eventsource')
;(async () => { ;(async () => {
const { dbg, error, info, warn } = logger().create(`server.ts`) const logger = loggerService().create(`server.ts`)
const { dbg, error, info, warn } = logger
info(`Starting`) info(`Starting`)
/** /**
@ -45,6 +46,7 @@ global.EventSource = require('eventsource')
const pbService = await pocketbase({ const pbService = await pocketbase({
cachePath: PH_BIN_CACHE, cachePath: PH_BIN_CACHE,
checkIntervalMs: 1000 * 5 * 60, checkIntervalMs: 1000 * 5 * 60,
logger,
}) })
/** /**
@ -73,18 +75,18 @@ global.EventSource = require('eventsource')
/** /**
* Launch services * Launch services
*/ */
await clientService(url) await clientService({ url, logger })
ftpService({}) ftpService({ logger })
await rpcService({}) await rpcService({ logger })
await proxyService({ await proxyService({
logger,
coreInternalUrl: url, coreInternalUrl: url,
}) })
await instanceLoggerService({}) await instanceLoggerService({ logger })
await sqliteService({}) await sqliteService({ logger })
await realtimeLog({}) await realtimeLog({ logger })
await instanceService({}) await instanceService({ logger })
await centralDbService({}) await centralDbService({ logger })
await backupService({})
info(`Hooking into process exit event`) info(`Hooking into process exit event`)

View File

@ -1,28 +1,34 @@
import { PUBLIC_APP_DB } from '$constants' import { PUBLIC_APP_DB } from '$constants'
import { logger, mkSingleton } from '@pockethost/common' import { mkSingleton, SingletonBaseConfig } from '@pockethost/common'
import { proxyService } from './ProxyService' import { proxyService } from './ProxyService'
export const centralDbService = mkSingleton(async () => { export type CentralDbServiceConfig = SingletonBaseConfig
const { dbg } = logger().create(`centralDbService`)
;(await proxyService()).use( export const centralDbService = mkSingleton(
PUBLIC_APP_DB, async (config: CentralDbServiceConfig) => {
['/api(/*)', '/_(/*)', '/'], const { logger } = config
(req, res, meta) => { const { dbg } = logger.create(`centralDbService`)
const { subdomain, coreInternalUrl, proxy } = meta
if (subdomain !== PUBLIC_APP_DB) return ;(await proxyService()).use(
PUBLIC_APP_DB,
['/api(/*)', '/_(/*)', '/'],
(req, res, meta, logger) => {
const { dbg } = logger
const { subdomain, coreInternalUrl, proxy } = meta
const target = coreInternalUrl if (subdomain !== PUBLIC_APP_DB) return
dbg(
`Forwarding proxy request for ${req.url} to central instance ${target}`
)
proxy.web(req, res, { target })
},
`CentralDbService`
)
return { const target = coreInternalUrl
shutdown() {}, dbg(
`Forwarding proxy request for ${req.url} to central instance ${target}`
)
proxy.web(req, res, { target })
},
`CentralDbService`
)
return {
shutdown() {},
}
} }
}) )

View File

@ -7,12 +7,12 @@ import {
SSL_KEY, SSL_KEY,
} from '$constants' } from '$constants'
import { clientService, createPbClient } from '$services' import { clientService, createPbClient } from '$services'
import { logger, mkSingleton } from '@pockethost/common' import { mkSingleton, SingletonBaseConfig } from '@pockethost/common'
import { readFileSync } from 'fs' import { readFileSync } from 'fs'
import { FtpSrv } from 'ftp-srv' import { FtpSrv } from 'ftp-srv'
import { PhFs } from './PhFs' import { PhFs } from './PhFs'
export type FtpConfig = {} export type FtpConfig = SingletonBaseConfig & {}
export enum FolderNames { export enum FolderNames {
PbData = 'pb_data', PbData = 'pb_data',
@ -49,13 +49,14 @@ const tls = {
} }
export const ftpService = mkSingleton((config: FtpConfig) => { export const ftpService = mkSingleton((config: FtpConfig) => {
const log = logger().create('FtpService') const { logger } = config
const { dbg, info } = log const _ftpServiceLogger = logger.create('FtpService')
const { dbg, info } = _ftpServiceLogger
const ftpServer = new FtpSrv({ const ftpServer = new FtpSrv({
url: 'ftp://0.0.0.0:' + PH_FTP_PORT, url: 'ftp://0.0.0.0:' + PH_FTP_PORT,
anonymous: false, anonymous: false,
log: log.create(`ftpServer`, { errorTrace: false }), log: _ftpServiceLogger.create(`ftpServer`, { errorTrace: false }),
tls, tls,
pasv_url: PH_FTP_PASV_IP, pasv_url: PH_FTP_PASV_IP,
pasv_max: PH_FTP_PASV_PORT_MAX, pasv_max: PH_FTP_PASV_PORT_MAX,
@ -66,13 +67,13 @@ export const ftpService = mkSingleton((config: FtpConfig) => {
'login', 'login',
async ({ connection, username, password }, resolve, reject) => { async ({ connection, username, password }, resolve, reject) => {
const url = (await clientService()).client.url const url = (await clientService()).client.url
const client = createPbClient(url) const client = createPbClient(url, _ftpServiceLogger)
try { try {
await client.client await client.client
.collection('users') .collection('users')
.authWithPassword(username, password) .authWithPassword(username, password)
dbg(`Logged in`) dbg(`Logged in`)
const fs = new PhFs(connection, client) const fs = new PhFs(connection, client, _ftpServiceLogger)
resolve({ fs }) resolve({ fs })
} catch (e) { } catch (e) {
reject(new Error(`Invalid username or password`)) reject(new Error(`Invalid username or password`))

View File

@ -1,5 +1,5 @@
import { DAEMON_PB_DATA_DIR } from '$constants' import { DAEMON_PB_DATA_DIR } from '$constants'
import { Logger, logger } from '@pockethost/common' import { Logger } from '@pockethost/common'
import { existsSync, mkdirSync } from 'fs' import { existsSync, mkdirSync } from 'fs'
import { FileStat, FileSystem, FtpConnection } from 'ftp-srv' import { FileStat, FileSystem, FtpConnection } from 'ftp-srv'
import { join } from 'path' import { join } from 'path'
@ -16,10 +16,14 @@ export class PhFs extends FileSystem {
private log: Logger private log: Logger
private client: PocketbaseClientApi private client: PocketbaseClientApi
constructor(connection: FtpConnection, client: PocketbaseClientApi) { constructor(
connection: FtpConnection,
client: PocketbaseClientApi,
logger: Logger
) {
super(connection, { root: '/', cwd: '/' }) super(connection, { root: '/', cwd: '/' })
this.client = client this.client = client
this.log = logger().create(`PhFs`) this.log = logger.create(`PhFs`)
} }
async chdir(path?: string | undefined): Promise<string> { async chdir(path?: string | undefined): Promise<string> {

View File

@ -0,0 +1,5 @@
import { Logger } from '@pockethost/common'
export type DaemonContext = {
parentLogger: Logger
}

View File

@ -0,0 +1,74 @@
import { SqliteChangeEvent, sqliteService } from '$services'
import {
InstanceLogFields,
InstanceLogFields_Create,
newId,
pocketNow,
RecordId,
safeCatch,
StreamNames,
} from '@pockethost/common'
import knex from 'knex'
import { AsyncReturnType } from 'type-fest'
import { DaemonContext } from './DaemonContext'
export type SqliteLogger = AsyncReturnType<typeof createSqliteLogger>
export const createSqliteLogger = async (
logDbPath: string,
context: DaemonContext
) => {
const { parentLogger } = context
const _dbLogger = parentLogger.create(`${logDbPath}`)
const { dbg } = _dbLogger
const { getDatabase } = sqliteService()
const db = await getDatabase(logDbPath)
const conn = knex({
client: 'sqlite3',
connection: {
filename: logDbPath,
},
useNullAsDefault: true,
})
const write = safeCatch(
`write`,
_dbLogger,
async (message: string, stream: StreamNames = StreamNames.Info) => {
const _in: InstanceLogFields_Create = {
id: newId(),
message,
stream,
created: pocketNow(),
updated: pocketNow(),
}
const sql = conn('logs').insert(_in).toString()
dbg(`Writing log ${JSON.stringify(_in)} ${sql}`)
await db.exec(sql)
}
)
const subscribe = (cb: (e: SqliteChangeEvent<InstanceLogFields>) => void) => {
let _seenIds: { [_: RecordId]: boolean } | undefined = {}
const unsub = db.subscribe<InstanceLogFields>((e) => {
// dbg(`Caught db modification ${logDbPath}`, e)
const { table, record } = e
if (table !== 'logs') return
if (_seenIds) {
_seenIds[record.id] = true
}
cb(e)
})
return unsub
}
const fetch = async (limit: number = 100) => {
return db.all<InstanceLogFields[]>(
`select * from logs order by created desc limit ${limit}`
)
}
return { write, subscribe, fetch }
}

View File

@ -1,85 +1,30 @@
import { DAEMON_PB_DATA_DIR } from '$constants' import { DAEMON_PB_DATA_DIR } from '$constants'
import { SqliteChangeEvent, sqliteService } from '$services' import { sqliteService } from '$services'
import { import {
InstanceId, InstanceId,
InstanceLogFields,
InstanceLogFields_Create,
logger,
mkSingleton, mkSingleton,
newId, SingletonBaseConfig,
pocketNow,
RecordId,
safeCatch,
StreamNames, StreamNames,
} from '@pockethost/common' } from '@pockethost/common'
import { mkdirSync } from 'fs' import { mkdirSync } from 'fs'
import knex from 'knex'
import { dirname, join } from 'path' import { dirname, join } from 'path'
import { AsyncReturnType } from 'type-fest' import { DaemonContext } from './DaemonContext'
import { createSqliteLogger, SqliteLogger } from './SqliteLogger'
export type InstanceLogger = AsyncReturnType<typeof mkApi>
const mkApi = async (logDbPath: string) => {
const { dbg } = logger().create(`InstanceLogger ${logDbPath}`)
const { getDatabase } = sqliteService()
const db = await getDatabase(logDbPath)
const conn = knex({
client: 'sqlite3',
connection: {
filename: logDbPath,
},
useNullAsDefault: true,
})
const write = safeCatch(
`workerLogger:write`,
async (message: string, stream: StreamNames = StreamNames.Info) => {
const _in: InstanceLogFields_Create = {
id: newId(),
message,
stream,
created: pocketNow(),
updated: pocketNow(),
}
const sql = conn('logs').insert(_in).toString()
dbg(`Writing log ${JSON.stringify(_in)} ${sql}`)
await db.exec(sql)
}
)
const subscribe = (cb: (e: SqliteChangeEvent<InstanceLogFields>) => void) => {
let _seenIds: { [_: RecordId]: boolean } | undefined = {}
const unsub = db.subscribe<InstanceLogFields>((e) => {
// dbg(`Caught db modification ${logDbPath}`, e)
const { table, record } = e
if (table !== 'logs') return
if (_seenIds) {
_seenIds[record.id] = true
}
cb(e)
})
return unsub
}
const fetch = async (limit: number = 100) => {
return db.all<InstanceLogFields[]>(
`select * from logs order by created desc limit ${limit}`
)
}
return { write, subscribe, fetch }
}
const instances: { const instances: {
[instanceId: InstanceId]: Promise<InstanceLogger> [instanceId: InstanceId]: Promise<SqliteLogger>
} = {} } = {}
export const createInstanceLogger = (instanceId: InstanceId) => { export const createInstanceLogger = (
instanceId: InstanceId,
context: DaemonContext
) => {
const { parentLogger } = context
if (!instances[instanceId]) { if (!instances[instanceId]) {
instances[instanceId] = new Promise<InstanceLogger>(async (resolve) => { instances[instanceId] = new Promise<SqliteLogger>(async (resolve) => {
const { dbg } = logger().create(`WorkerLogger:${instanceId}`) const _instanceLogger = parentLogger.create(`InstanceLogger`)
const { dbg } = _instanceLogger
const logDbPath = join( const logDbPath = join(
DAEMON_PB_DATA_DIR, DAEMON_PB_DATA_DIR,
@ -98,7 +43,9 @@ export const createInstanceLogger = (instanceId: InstanceId) => {
migrationsPath: join(__dirname, 'migrations'), migrationsPath: join(__dirname, 'migrations'),
}) })
const api = await mkApi(logDbPath) const api = await createSqliteLogger(logDbPath, {
parentLogger: _instanceLogger,
})
await api.write(`Ran migrations`, StreamNames.System) await api.write(`Ran migrations`, StreamNames.System)
resolve(api) resolve(api)
}) })
@ -107,13 +54,18 @@ export const createInstanceLogger = (instanceId: InstanceId) => {
return instances[instanceId]! return instances[instanceId]!
} }
export const instanceLoggerService = mkSingleton(() => { export type InstanceLoggerServiceConfig = SingletonBaseConfig
const { dbg } = logger().create(`InstanceLoggerService`)
dbg(`Starting up`) export const instanceLoggerService = mkSingleton(
return { (config: InstanceLoggerServiceConfig) => {
get: createInstanceLogger, const { logger } = config
shutdown() { const { dbg } = logger.create(`InstanceLoggerService`)
dbg(`Shutting down`) dbg(`Starting up`)
}, return {
get: createInstanceLogger,
shutdown() {
dbg(`Shutting down`)
},
}
} }
}) )

View File

@ -1,5 +1,5 @@
import { DAEMON_PB_DATA_DIR, DENO_PATH } from '$constants' import { DAEMON_PB_DATA_DIR, DENO_PATH } from '$constants'
import { InstanceFields, logger, StreamNames } from '@pockethost/common' import { InstanceFields, Logger, StreamNames } from '@pockethost/common'
import { keys } from '@s-libs/micro-dash' import { keys } from '@s-libs/micro-dash'
import { spawn } from 'child_process' import { spawn } from 'child_process'
import { join } from 'path' import { join } from 'path'
@ -11,12 +11,15 @@ export type DenoProcessConfig = {
path: string path: string
port: number port: number
instance: InstanceFields instance: InstanceFields
logger: Logger
} }
export type DenoApi = AsyncReturnType<typeof createDenoProcess> export type DenoApi = AsyncReturnType<typeof createDenoProcess>
export const createDenoProcess = async (config: DenoProcessConfig) => { export const createDenoProcess = async (config: DenoProcessConfig) => {
const { dbg, error } = logger().create(`DenoProcess.ts`) const { logger } = config
const _denoLogger = logger.create(`DenoProcess.ts`)
const { dbg, error } = _denoLogger
const { instance, port, path } = config const { instance, port, path } = config
const internalUrl = mkInternalUrl(port) const internalUrl = mkInternalUrl(port)
@ -34,7 +37,9 @@ export const createDenoProcess = async (config: DenoProcessConfig) => {
path, path,
] ]
const denoLogger = await instanceLoggerService().get(instance.id) const denoLogger = await instanceLoggerService().get(instance.id, {
parentLogger: _denoLogger,
})
const denoWrite = ( const denoWrite = (
message: string, message: string,

View File

@ -16,13 +16,13 @@ import {
createTimerManager, createTimerManager,
InstanceId, InstanceId,
InstanceStatus, InstanceStatus,
logger,
mkSingleton, mkSingleton,
RpcCommands, RpcCommands,
safeCatch, safeCatch,
SaveSecretsPayload, SaveSecretsPayload,
SaveSecretsPayloadSchema, SaveSecretsPayloadSchema,
SaveSecretsResult, SaveSecretsResult,
SingletonBaseConfig,
} from '@pockethost/common' } from '@pockethost/common'
import { forEachRight, map } from '@s-libs/micro-dash' import { forEachRight, map } from '@s-libs/micro-dash'
import Bottleneck from 'bottleneck' import Bottleneck from 'bottleneck'
@ -42,12 +42,14 @@ type InstanceApi = {
startRequest: () => () => void startRequest: () => () => void
} }
export type InstanceServiceConfig = {} export type InstanceServiceConfig = SingletonBaseConfig & {}
export type InstanceServiceApi = AsyncReturnType<typeof instanceService> export type InstanceServiceApi = AsyncReturnType<typeof instanceService>
export const instanceService = mkSingleton( export const instanceService = mkSingleton(
async (config: InstanceServiceConfig) => { async (config: InstanceServiceConfig) => {
const { dbg, raw, error, warn } = logger().create('InstanceService') const { logger } = config
const _instanceLogger = logger.create('InstanceService')
const { dbg, raw, error, warn } = _instanceLogger
const { client } = await clientService() const { client } = await clientService()
const { registerCommand } = await rpcService() const { registerCommand } = await rpcService()
@ -91,9 +93,8 @@ export const instanceService = mkSingleton(
const getInstance = (subdomain: string) => const getInstance = (subdomain: string) =>
instanceLimiter instanceLimiter
.schedule(async () => { .schedule(async () => {
const { dbg, warn, error } = logger().create( const _subdomainLogger = _instanceLogger.create(subdomain)
`InstanceService ${subdomain}` const { dbg, warn, error } = _subdomainLogger
)
dbg(`Getting instance`) dbg(`Getting instance`)
{ {
const instance = instances[subdomain] const instance = instances[subdomain]
@ -112,6 +113,7 @@ export const instanceService = mkSingleton(
) )
} }
dbg(`Instance found`) dbg(`Instance found`)
_subdomainLogger.breadcrumb(instance.id)
dbg(`Checking for verified account`) dbg(`Checking for verified account`)
if (!owner?.verified) { if (!owner?.verified) {
@ -132,9 +134,13 @@ export const instanceService = mkSingleton(
error(`Failed to get port for ${subdomain}`) error(`Failed to get port for ${subdomain}`)
throw e throw e
}) })
_subdomainLogger.breadcrumb(newPort.toString())
dbg(`Found port: ${newPort}`) dbg(`Found port: ${newPort}`)
const instanceLogger = await instanceLoggerService().get(instance.id) const instanceLogger = await instanceLoggerService().get(
instance.id,
{ parentLogger: _subdomainLogger }
)
await clientLimiter.schedule(() => { await clientLimiter.schedule(() => {
dbg(`Instance status: starting`) dbg(`Instance status: starting`)
@ -146,16 +152,25 @@ export const instanceService = mkSingleton(
dbg(`Starting instance`) dbg(`Starting instance`)
await instanceLogger.write(`Starting instance`) await instanceLogger.write(`Starting instance`)
const childProcess = await pbService.spawn({ const childProcess = await (async () => {
command: 'serve', try {
slug: instance.id, const cp = await pbService.spawn({
port: newPort, command: 'serve',
version: instance.version, slug: instance.id,
onUnexpectedStop: (code) => { port: newPort,
warn(`${subdomain} exited unexpectedly with ${code}`) version: instance.version,
api.shutdown() onUnexpectedStop: (code) => {
}, warn(`${subdomain} exited unexpectedly with ${code}`)
}) api.shutdown()
},
})
return cp
} catch (e) {
throw new Error(
`Could not launch PocketBase ${instance.version}. It may be time to upgrade.`
)
}
})()
const { pid } = childProcess const { pid } = childProcess
assertTruthy(pid, `Expected PID here but got ${pid}`) assertTruthy(pid, `Expected PID here but got ${pid}`)
@ -190,6 +205,7 @@ export const instanceService = mkSingleton(
path: workerPath, path: workerPath,
port: newPort, port: newPort,
instance, instance,
logger: _instanceLogger,
}) })
return api return api
} else { } else {
@ -210,6 +226,7 @@ export const instanceService = mkSingleton(
port: newPort, port: newPort,
shutdown: safeCatch( shutdown: safeCatch(
`Instance ${subdomain} invocation ${invocation.id} pid ${pid} shutdown`, `Instance ${subdomain} invocation ${invocation.id} pid ${pid} shutdown`,
_subdomainLogger,
async () => { async () => {
dbg(`Shutting down`) dbg(`Shutting down`)
await instanceLogger.write(`Shutting down instance`) await instanceLogger.write(`Shutting down instance`)
@ -247,7 +264,7 @@ export const instanceService = mkSingleton(
{ {
tm.repeat( tm.repeat(
safeCatch(`idleCheck`, async () => { safeCatch(`idleCheck`, _subdomainLogger, async () => {
raw( raw(
`${subdomain} idle check: ${openRequestCount} open requests` `${subdomain} idle check: ${openRequestCount} open requests`
) )
@ -272,7 +289,7 @@ export const instanceService = mkSingleton(
} }
{ {
const uptime = safeCatch(`uptime`, async () => { const uptime = safeCatch(`uptime`, _subdomainLogger, async () => {
raw(`${subdomain} uptime`) raw(`${subdomain} uptime`)
await clientLimiter.schedule(() => await clientLimiter.schedule(() =>
client.pingInvocation(invocation) client.pingInvocation(invocation)
@ -314,7 +331,7 @@ export const instanceService = mkSingleton(
;(await proxyService()).use( ;(await proxyService()).use(
(subdomain) => subdomain !== PUBLIC_APP_DB, (subdomain) => subdomain !== PUBLIC_APP_DB,
['/api(/*)', '/_(/*)', '(/*)'], ['/api(/*)', '/_(/*)', '(/*)'],
async (req, res, meta) => { async (req, res, meta, logger) => {
const { subdomain, host, proxy } = meta const { subdomain, host, proxy } = meta
// Do not handle central db requests, that is handled separately // Do not handle central db requests, that is handled separately

View File

@ -9,10 +9,12 @@ import {
import { import {
createCleanupManager, createCleanupManager,
createTimerManager, createTimerManager,
logger,
safeCatch, safeCatch,
} from '@pockethost/common' } from '@pockethost/common'
import { mkSingleton } from '@pockethost/common/src/mkSingleton' import {
mkSingleton,
SingletonBaseConfig,
} from '@pockethost/common/src/mkSingleton'
import { keys } from '@s-libs/micro-dash' import { keys } from '@s-libs/micro-dash'
import { spawn } from 'child_process' import { spawn } from 'child_process'
import { chmodSync, existsSync } from 'fs' import { chmodSync, existsSync } from 'fs'
@ -35,7 +37,7 @@ export type PocketbaseServiceApi = AsyncReturnType<
typeof createPocketbaseService typeof createPocketbaseService
> >
export type PocketbaseServiceConfig = { export type PocketbaseServiceConfig = SingletonBaseConfig & {
cachePath: string cachePath: string
checkIntervalMs: number checkIntervalMs: number
} }
@ -60,7 +62,9 @@ export type Releases = Release[]
export const createPocketbaseService = async ( export const createPocketbaseService = async (
config: PocketbaseServiceConfig config: PocketbaseServiceConfig
) => { ) => {
const { dbg, error } = logger().create('PocketbaseService') const { logger } = config
const _serviceLogger = logger.create('PocketbaseService')
const { dbg, error } = _serviceLogger
const { cachePath, checkIntervalMs } = config const { cachePath, checkIntervalMs } = config
@ -100,7 +104,7 @@ export const createPocketbaseService = async (
versions[sanitizedTagName] = Promise.resolve('') versions[sanitizedTagName] = Promise.resolve('')
return return
} }
await downloadAndExtract(url, binPath) await downloadAndExtract(url, binPath, _serviceLogger)
resolve(binPath) resolve(binPath)
}) })
@ -138,85 +142,89 @@ export const createPocketbaseService = async (
} }
} }
const _spawn = safeCatch(`spawnInstance`, async (cfg: SpawnConfig) => { const _spawn = safeCatch(
const _cfg: Required<SpawnConfig> = { `spawnInstance`,
version: maxVersion, _serviceLogger,
port: await getPort(), async (cfg: SpawnConfig) => {
isMothership: false, const _cfg: Required<SpawnConfig> = {
onUnexpectedStop: (code) => { version: maxVersion,
dbg(`Unexpected stop default handler. Exit code: ${code}`) port: await getPort(),
}, isMothership: false,
...cfg, onUnexpectedStop: (code) => {
} dbg(`Unexpected stop default handler. Exit code: ${code}`)
const { version, command, slug, port, onUnexpectedStop, isMothership } = },
_cfg ...cfg,
const _version = version || maxVersion // If _version is blank, we use the max version available }
const bin = (await getVersion(_version)).binPath const { version, command, slug, port, onUnexpectedStop, isMothership } =
if (!existsSync(bin)) { _cfg
throw new Error( const _version = version || maxVersion // If _version is blank, we use the max version available
`PocketBase binary (${bin}) not found. Contact pockethost.io.` const bin = (await getVersion(_version)).binPath
) if (!existsSync(bin)) {
} throw new Error(
`PocketBase binary (${bin}) not found. Contact pockethost.io.`
)
}
const args = [ const args = [
command, command,
`--dir`, `--dir`,
`${DAEMON_PB_DATA_DIR}/${slug}/pb_data`, `${DAEMON_PB_DATA_DIR}/${slug}/pb_data`,
`--publicDir`, `--publicDir`,
`${DAEMON_PB_DATA_DIR}/${slug}/pb_static`, `${DAEMON_PB_DATA_DIR}/${slug}/pb_static`,
`--migrationsDir`, `--migrationsDir`,
isMothership isMothership
? DAEMON_PB_MIGRATIONS_DIR ? DAEMON_PB_MIGRATIONS_DIR
: `${DAEMON_PB_DATA_DIR}/${slug}/pb_migrations`, : `${DAEMON_PB_DATA_DIR}/${slug}/pb_migrations`,
] ]
if (command === 'serve') { if (command === 'serve') {
args.push(`--http`) args.push(`--http`)
args.push(mkInternalAddress(port)) args.push(mkInternalAddress(port))
} }
let isRunning = true let isRunning = true
dbg(`Spawning ${slug}`, { bin, args, cli: [bin, ...args].join(' ') }) dbg(`Spawning ${slug}`, { bin, args, cli: [bin, ...args].join(' ') })
const ls = spawn(bin, args) const ls = spawn(bin, args)
cm.add(() => ls.kill()) cm.add(() => ls.kill())
ls.stdout.on('data', (data) => { ls.stdout.on('data', (data) => {
dbg(`${slug} stdout: ${data}`) dbg(`${slug} stdout: ${data}`)
})
ls.stderr.on('data', (data) => {
error(`${slug} stderr: ${data}`)
})
ls.on('close', (code) => {
dbg(`${slug} closed with code ${code}`)
})
const exited = new Promise<number | null>((resolve) => {
ls.on('exit', (code) => {
dbg(`${slug} exited with code ${code}`)
isRunning = false
if (code) onUnexpectedStop?.(code)
resolve(code)
}) })
})
ls.on('error', (err) => { ls.stderr.on('data', (data) => {
dbg(`${slug} had error ${err}`) error(`${slug} stderr: ${data}`)
}) })
const url = mkInternalUrl(port) ls.on('close', (code) => {
if (command === 'serve') { dbg(`${slug} closed with code ${code}`)
await tryFetch(url, async () => isRunning) })
const exited = new Promise<number | null>((resolve) => {
ls.on('exit', (code) => {
dbg(`${slug} exited with code ${code}`)
isRunning = false
if (code) onUnexpectedStop?.(code)
resolve(code)
})
})
ls.on('error', (err) => {
dbg(`${slug} had error ${err}`)
})
const url = mkInternalUrl(port)
if (command === 'serve') {
await tryFetch(_serviceLogger)(url, async () => isRunning)
}
const api: PocketbaseProcess = {
url,
exited,
pid: ls.pid,
kill: () => ls.kill(),
}
return api
} }
const api: PocketbaseProcess = { )
url,
exited,
pid: ls.pid,
kill: () => ls.kill(),
}
return api
})
const shutdown = () => { const shutdown = () => {
dbg(`Shutting down pocketbaseService`) dbg(`Shutting down pocketbaseService`)

View File

@ -1,5 +1,5 @@
import { PUBLIC_APP_DOMAIN } from '$constants' import { PUBLIC_APP_DOMAIN } from '$constants'
import { logger, mkSingleton } from '@pockethost/common' import { Logger, mkSingleton, SingletonBaseConfig } from '@pockethost/common'
import { isFunction } from '@s-libs/micro-dash' import { isFunction } from '@s-libs/micro-dash'
import { import {
createServer, createServer,
@ -21,14 +21,17 @@ export type ProxyMiddleware = (
coreInternalUrl: string coreInternalUrl: string
proxy: Server proxy: Server
host: string host: string
} },
logger: Logger
) => void | Promise<void> ) => void | Promise<void>
export type ProxyServiceConfig = { export type ProxyServiceConfig = SingletonBaseConfig & {
coreInternalUrl: string coreInternalUrl: string
} }
export const proxyService = mkSingleton(async (config: ProxyServiceConfig) => { export const proxyService = mkSingleton(async (config: ProxyServiceConfig) => {
const { dbg, error, info, trace, warn } = logger().create('ProxyService') const { logger } = config
const _proxyLogger = logger.create('ProxyService')
const { dbg, error, info, trace, warn } = _proxyLogger
const { coreInternalUrl } = config const { coreInternalUrl } = config
@ -38,7 +41,7 @@ export const proxyService = mkSingleton(async (config: ProxyServiceConfig) => {
}) })
const server = createServer(async (req, res) => { const server = createServer(async (req, res) => {
dbg(`Incoming request ${req.headers.host}/${req.url}`) dbg(`Incoming request ${req.method} ${req.headers.host}/${req.url}`)
if (!req.headers.host?.endsWith(PUBLIC_APP_DOMAIN)) { if (!req.headers.host?.endsWith(PUBLIC_APP_DOMAIN)) {
warn( warn(
`Request for ${req.headers.host} rejected because host does not end in ${PUBLIC_APP_DOMAIN}` `Request for ${req.headers.host} rejected because host does not end in ${PUBLIC_APP_DOMAIN}`
@ -89,7 +92,8 @@ export const proxyService = mkSingleton(async (config: ProxyServiceConfig) => {
handler: ProxyMiddleware, handler: ProxyMiddleware,
handlerName: string handlerName: string
) => { ) => {
const { dbg, trace } = logger().create(`ProxyService:${handlerName}`) const _handlerLogger = _proxyLogger.create(`${handlerName}`)
const { dbg, trace } = _handlerLogger
dbg({ subdomainFilter, urlFilters }) dbg({ subdomainFilter, urlFilters })
const _urlFilters = Array.isArray(urlFilters) const _urlFilters = Array.isArray(urlFilters)
@ -105,6 +109,10 @@ export const proxyService = mkSingleton(async (config: ProxyServiceConfig) => {
if (!host) { if (!host) {
throw new Error(`Host not found`) throw new Error(`Host not found`)
} }
const _requestLogger = _handlerLogger.create(host)
const { dbg, trace } = _requestLogger
_requestLogger.breadcrumb(req.method)
_requestLogger.breadcrumb(req.url)
const [subdomain, ...junk] = host.split('.') const [subdomain, ...junk] = host.split('.')
if (!subdomain) { if (!subdomain) {
throw new Error(`${host} has no subdomain.`) throw new Error(`${host} has no subdomain.`)
@ -133,7 +141,12 @@ export const proxyService = mkSingleton(async (config: ProxyServiceConfig) => {
return return
} }
dbg(`${url} matches ${urlFilters}, sending to handler`) dbg(`${url} matches ${urlFilters}, sending to handler`)
return handler(req, res, { host, subdomain, coreInternalUrl, proxy }) return handler(
req,
res,
{ host, subdomain, coreInternalUrl, proxy },
_requestLogger
)
}) })
} }

View File

@ -1,9 +1,9 @@
import { PUBLIC_APP_DB } from '$src/constants' import { PUBLIC_APP_DB } from '$src/constants'
import { import {
InstanceFields, InstanceFields,
logger,
mkSingleton, mkSingleton,
RecordId, RecordId,
SingletonBaseConfig,
} from '@pockethost/common' } from '@pockethost/common'
import Bottleneck from 'bottleneck' import Bottleneck from 'bottleneck'
import { text } from 'node:stream/consumers' import { text } from 'node:stream/consumers'
@ -12,7 +12,7 @@ import { JsonifiableObject } from 'type-fest/source/jsonifiable'
import { instanceLoggerService } from './InstanceLoggerService' import { instanceLoggerService } from './InstanceLoggerService'
import { proxyService } from './ProxyService' import { proxyService } from './ProxyService'
export type RealtimeLogConfig = {} export type RealtimeLogConfig = SingletonBaseConfig & {}
const mkEvent = (name: string, data: JsonifiableObject) => { const mkEvent = (name: string, data: JsonifiableObject) => {
return `event: ${name}\ndata: ${JSON.stringify(data)}\n\n` return `event: ${name}\ndata: ${JSON.stringify(data)}\n\n`
@ -20,20 +20,21 @@ const mkEvent = (name: string, data: JsonifiableObject) => {
export type RealtimeLog = ReturnType<typeof realtimeLog> export type RealtimeLog = ReturnType<typeof realtimeLog>
export const realtimeLog = mkSingleton(async (config: RealtimeLogConfig) => { export const realtimeLog = mkSingleton(async (config: RealtimeLogConfig) => {
const { dbg, error } = logger().create(`RealtimeLog.ts`) const { logger } = config
const _realtimeLogger = logger.create(`RealtimeLog`)
const { dbg, error } = _realtimeLogger
;(await proxyService()).use( ;(await proxyService()).use(
PUBLIC_APP_DB, PUBLIC_APP_DB,
'/logs', '/logs',
async (req, res, meta) => { async (req, res, meta, logger) => {
const { subdomain, host, coreInternalUrl } = meta const { subdomain, host, coreInternalUrl } = meta
if (!req.url?.startsWith('/logs')) { if (!req.url?.startsWith('/logs')) {
return return
} }
const { dbg, error, trace } = logger().create( const _requestLogger = logger.create(`${subdomain}`)
`RealtimeLog:${subdomain}:${host}` const { dbg, error, trace } = _requestLogger
)
const write = async (data: any) => { const write = async (data: any) => {
return new Promise<void>((resolve) => { return new Promise<void>((resolve) => {
@ -115,7 +116,9 @@ export const realtimeLog = mkSingleton(async (config: RealtimeLogConfig) => {
/** /**
* Get a database connection * Get a database connection
*/ */
const instanceLogger = await instanceLoggerService().get(instanceId) const instanceLogger = await instanceLoggerService().get(instanceId, {
parentLogger: _requestLogger,
})
const { subscribe } = instanceLogger const { subscribe } = instanceLogger
/** /**
@ -182,8 +185,6 @@ export const realtimeLog = mkSingleton(async (config: RealtimeLogConfig) => {
}) })
.catch(error) .catch(error)
} }
return true
}, },
`RealtimeLogService` `RealtimeLogService`
) )

View File

@ -1,12 +1,12 @@
import { clientService } from '$services' import { clientService } from '$services'
import { import {
assertTruthy, assertTruthy,
logger,
mkSingleton, mkSingleton,
RpcCommands, RpcCommands,
RpcFields, RpcFields,
RpcStatus, RpcStatus,
RPC_COMMANDS, RPC_COMMANDS,
SingletonBaseConfig,
} from '@pockethost/common' } from '@pockethost/common'
import { isObject } from '@s-libs/micro-dash' import { isObject } from '@s-libs/micro-dash'
import Ajv, { JSONSchemaType, ValidateFunction } from 'ajv' import Ajv, { JSONSchemaType, ValidateFunction } from 'ajv'
@ -29,10 +29,11 @@ export type RpcRunner<
TResult extends JsonObject TResult extends JsonObject
> = (job: RpcFields<TPayload, TResult>) => Promise<TResult> > = (job: RpcFields<TPayload, TResult>) => Promise<TResult>
export type RpcServiceConfig = {} export type RpcServiceConfig = SingletonBaseConfig & {}
export const rpcService = mkSingleton(async (config: RpcServiceConfig) => { export const rpcService = mkSingleton(async (config: RpcServiceConfig) => {
const { dbg, error } = logger().create('RpcService') const { logger } = config
const { dbg, error } = logger.create('RpcService')
const { client } = await clientService() const { client } = await clientService()
const limiter = new Bottleneck({ maxConcurrent: 1 }) const limiter = new Bottleneck({ maxConcurrent: 1 })

View File

@ -1,8 +1,8 @@
import { import {
createCleanupManager, createCleanupManager,
createEvent, createEvent,
logger,
mkSingleton, mkSingleton,
SingletonBaseConfig,
} from '@pockethost/common' } from '@pockethost/common'
import Bottleneck from 'bottleneck' import Bottleneck from 'bottleneck'
import { Database as SqliteDatabase, open } from 'sqlite' import { Database as SqliteDatabase, open } from 'sqlite'
@ -28,12 +28,13 @@ export type SqliteServiceApi = {
cb: SqliteChangeHandler<TRecord> cb: SqliteChangeHandler<TRecord>
) => SqliteUnsubscribe ) => SqliteUnsubscribe
} }
export type SqliteServiceConfig = {} export type SqliteServiceConfig = SingletonBaseConfig & {}
export type SqliteService = ReturnType<typeof sqliteService> export type SqliteService = ReturnType<typeof sqliteService>
export const sqliteService = mkSingleton((config: SqliteServiceConfig) => { export const sqliteService = mkSingleton((config: SqliteServiceConfig) => {
const { dbg, trace } = logger().create(`sqliteService`) const { logger } = config
const { dbg, trace } = logger.create(`sqliteService`)
const connections: { [_: string]: Promise<SqliteServiceApi> } = {} const connections: { [_: string]: Promise<SqliteServiceApi> } = {}
const cm = createCleanupManager() const cm = createCleanupManager()
@ -41,7 +42,8 @@ export const sqliteService = mkSingleton((config: SqliteServiceConfig) => {
const limiter = new Bottleneck({ maxConcurrent: 1 }) const limiter = new Bottleneck({ maxConcurrent: 1 })
const getDatabase = async (filename: string): Promise<SqliteServiceApi> => { const getDatabase = async (filename: string): Promise<SqliteServiceApi> => {
const { dbg } = logger().create(`sqliteService:${filename}`) const _dbLogger = logger.create(`${filename}`)
const { dbg } = _dbLogger
trace(`Fetching database for ${filename}`, connections) trace(`Fetching database for ${filename}`, connections)
if (!connections[filename]) { if (!connections[filename]) {

View File

@ -4,7 +4,6 @@ import {
InstanceFields_Create, InstanceFields_Create,
InstanceId, InstanceId,
InstanceStatus, InstanceStatus,
logger,
safeCatch, safeCatch,
UserFields, UserFields,
} from '@pockethost/common' } from '@pockethost/common'
@ -16,12 +15,14 @@ import { MixinContext } from './PbClient'
export type InstanceApi = ReturnType<typeof createInstanceMixin> export type InstanceApi = ReturnType<typeof createInstanceMixin>
export const createInstanceMixin = (context: MixinContext) => { export const createInstanceMixin = (context: MixinContext) => {
const { dbg, raw } = logger().create('InstanceMixin') const { logger } = context
const { dbg, raw } = logger.create('InstanceMixin')
const { client, rawDb } = context const { client, rawDb } = context
const createInstance = safeCatch( const createInstance = safeCatch(
`createInstance`, `createInstance`,
logger,
(payload: InstanceFields_Create): Promise<InstanceFields> => { (payload: InstanceFields_Create): Promise<InstanceFields> => {
return client.collection('instances').create<InstanceFields>(payload) return client.collection('instances').create<InstanceFields>(payload)
} }
@ -29,6 +30,7 @@ export const createInstanceMixin = (context: MixinContext) => {
const getInstanceBySubdomain = safeCatch( const getInstanceBySubdomain = safeCatch(
`getInstanceBySubdomain`, `getInstanceBySubdomain`,
logger,
(subdomain: string): Promise<[InstanceFields, UserFields] | []> => (subdomain: string): Promise<[InstanceFields, UserFields] | []> =>
client client
.collection('instances') .collection('instances')
@ -46,6 +48,7 @@ export const createInstanceMixin = (context: MixinContext) => {
const getInstanceById = safeCatch( const getInstanceById = safeCatch(
`getInstanceById`, `getInstanceById`,
logger,
async ( async (
instanceId: InstanceId instanceId: InstanceId
): Promise<[InstanceFields, UserFields] | []> => { ): Promise<[InstanceFields, UserFields] | []> => {
@ -66,6 +69,7 @@ export const createInstanceMixin = (context: MixinContext) => {
const updateInstance = safeCatch( const updateInstance = safeCatch(
`updateInstance`, `updateInstance`,
logger,
async (instanceId: InstanceId, fields: Partial<InstanceFields>) => { async (instanceId: InstanceId, fields: Partial<InstanceFields>) => {
await client.collection('instances').update(instanceId, fields) await client.collection('instances').update(instanceId, fields)
} }
@ -73,6 +77,7 @@ export const createInstanceMixin = (context: MixinContext) => {
const updateInstanceStatus = safeCatch( const updateInstanceStatus = safeCatch(
`updateInstanceStatus`, `updateInstanceStatus`,
logger,
async (instanceId: InstanceId, status: InstanceStatus) => { async (instanceId: InstanceId, status: InstanceStatus) => {
await updateInstance(instanceId, { status }) await updateInstance(instanceId, { status })
} }
@ -80,17 +85,19 @@ export const createInstanceMixin = (context: MixinContext) => {
const getInstance = safeCatch( const getInstance = safeCatch(
`getInstance`, `getInstance`,
logger,
async (instanceId: InstanceId) => { async (instanceId: InstanceId) => {
return client.collection('instances').getOne<InstanceFields>(instanceId) return client.collection('instances').getOne<InstanceFields>(instanceId)
} }
) )
const getInstances = safeCatch(`getInstances`, async () => { const getInstances = safeCatch(`getInstances`, logger, async () => {
return client.collection('instances').getFullList<InstanceFields>() return client.collection('instances').getFullList<InstanceFields>()
}) })
const updateInstances = safeCatch( const updateInstances = safeCatch(
'updateInstances', 'updateInstances',
logger,
async (cb: (rec: InstanceFields) => Partial<InstanceFields>) => { async (cb: (rec: InstanceFields) => Partial<InstanceFields>) => {
const res = await client const res = await client
.collection('instances') .collection('instances')
@ -116,6 +123,7 @@ export const createInstanceMixin = (context: MixinContext) => {
const updateInstanceSeconds = safeCatch( const updateInstanceSeconds = safeCatch(
`updateInstanceSeconds`, `updateInstanceSeconds`,
logger,
async (instanceId: InstanceId, forPeriod = new Date()) => { async (instanceId: InstanceId, forPeriod = new Date()) => {
const startIso = startOfMonth(forPeriod).toISOString() const startIso = startOfMonth(forPeriod).toISOString()
const endIso = endOfMonth(forPeriod).toISOString() const endIso = endOfMonth(forPeriod).toISOString()

View File

@ -1,7 +1,6 @@
import { import {
InstanceFields, InstanceFields,
InvocationFields, InvocationFields,
logger,
pocketNow, pocketNow,
safeCatch, safeCatch,
} from '@pockethost/common' } from '@pockethost/common'
@ -12,12 +11,14 @@ export const createInvocationMixin = (
context: MixinContext, context: MixinContext,
instanceApi: InstanceApi instanceApi: InstanceApi
) => { ) => {
const { dbg } = logger().create('InvocationMixin') const { logger } = context
const { dbg } = logger.create('InvocationMixin')
const { client } = context const { client } = context
const createInvocation = safeCatch( const createInvocation = safeCatch(
`createInvocation`, `createInvocation`,
logger,
async (instance: InstanceFields, pid: number) => { async (instance: InstanceFields, pid: number) => {
const init: Partial<InvocationFields> = { const init: Partial<InvocationFields> = {
startedAt: pocketNow(), startedAt: pocketNow(),
@ -34,6 +35,7 @@ export const createInvocationMixin = (
const pingInvocation = safeCatch( const pingInvocation = safeCatch(
`pingInvocation`, `pingInvocation`,
logger,
async (invocation: InvocationFields) => { async (invocation: InvocationFields) => {
const totalSeconds = const totalSeconds =
(+new Date() - Date.parse(invocation.startedAt)) / 1000 (+new Date() - Date.parse(invocation.startedAt)) / 1000
@ -50,6 +52,7 @@ export const createInvocationMixin = (
const finalizeInvocation = safeCatch( const finalizeInvocation = safeCatch(
`finalizeInvocation`, `finalizeInvocation`,
logger,
async (invocation: InvocationFields) => { async (invocation: InvocationFields) => {
dbg('finalizing') dbg('finalizing')
const totalSeconds = const totalSeconds =

View File

@ -1,8 +1,7 @@
import { DAEMON_PB_DATA_DIR, PUBLIC_APP_DB } from '$constants' import { DAEMON_PB_DATA_DIR, PUBLIC_APP_DB } from '$constants'
import { logger, safeCatch } from '@pockethost/common' import { Logger, safeCatch } from '@pockethost/common'
import { Knex } from 'knex' import { Knex } from 'knex'
import { default as PocketBase, default as pocketbaseEs } from 'pocketbase' import { default as PocketBase, default as pocketbaseEs } from 'pocketbase'
import { createBackupMixin } from './BackupMixin'
import { createInstanceMixin } from './InstanceMIxin' import { createInstanceMixin } from './InstanceMIxin'
import { createInvocationMixin } from './InvocationMixin' import { createInvocationMixin } from './InvocationMixin'
import { createRawPbClient } from './RawPbClient' import { createRawPbClient } from './RawPbClient'
@ -10,26 +9,30 @@ import { createRpcHelper } from './RpcHelper'
export type PocketbaseClientApi = ReturnType<typeof createPbClient> export type PocketbaseClientApi = ReturnType<typeof createPbClient>
export type MixinContext = { client: pocketbaseEs; rawDb: Knex } export type MixinContext = { client: pocketbaseEs; rawDb: Knex; logger: Logger }
export const createPbClient = (url: string) => { export const createPbClient = (url: string, logger: Logger) => {
const { info } = logger().create('PbClient') const _clientLogger = logger.create('PbClient')
const { info } = _clientLogger
info(`Initializing client: ${url}`) info(`Initializing client: ${url}`)
const rawDb = createRawPbClient( const rawDb = createRawPbClient(
`${DAEMON_PB_DATA_DIR}/${PUBLIC_APP_DB}/pb_data/data.db` `${DAEMON_PB_DATA_DIR}/${PUBLIC_APP_DB}/pb_data/data.db`,
_clientLogger
) )
const client = new PocketBase(url) const client = new PocketBase(url)
const adminAuthViaEmail = safeCatch( const adminAuthViaEmail = safeCatch(
`adminAuthViaEmail`, `adminAuthViaEmail`,
_clientLogger,
(email: string, password: string) => (email: string, password: string) =>
client.admins.authWithPassword(email, password) client.admins.authWithPassword(email, password)
) )
const createFirstAdmin = safeCatch( const createFirstAdmin = safeCatch(
`createFirstAdmin`, `createFirstAdmin`,
_clientLogger,
(email: string, password: string) => (email: string, password: string) =>
client.admins client.admins
.create({ email, password, passwordConfirm: password }) .create({ email, password, passwordConfirm: password })
@ -40,10 +43,9 @@ export const createPbClient = (url: string) => {
}) })
) )
const context: MixinContext = { client, rawDb } const context: MixinContext = { client, rawDb, logger: _clientLogger }
const rpcApi = createRpcHelper(context) const rpcApi = createRpcHelper(context)
const instanceApi = createInstanceMixin(context) const instanceApi = createInstanceMixin(context)
const backupApi = createBackupMixin(context)
const invocationApi = createInvocationMixin(context, instanceApi) const invocationApi = createInvocationMixin(context, instanceApi)
const api = { const api = {
@ -55,7 +57,6 @@ export const createPbClient = (url: string) => {
...rpcApi, ...rpcApi,
...instanceApi, ...instanceApi,
...invocationApi, ...invocationApi,
...backupApi,
} }
return api return api

View File

@ -1,9 +1,9 @@
import { logger } from '@pockethost/common' import { Logger } from '@pockethost/common'
import { existsSync } from 'fs' import { existsSync } from 'fs'
import knex from 'knex' import knex from 'knex'
export const createRawPbClient = (filename: string) => { export const createRawPbClient = (filename: string, logger: Logger) => {
const { dbg } = logger().create(`rawPbClient`) const { dbg } = logger.create(`rawPbClient`)
dbg(filename) dbg(filename)
if (!existsSync(filename)) { if (!existsSync(filename)) {

View File

@ -18,9 +18,10 @@ export type RpcHelperConfig = MixinContext
export type RpcHelper = ReturnType<typeof createRpcHelper> export type RpcHelper = ReturnType<typeof createRpcHelper>
export const createRpcHelper = (config: RpcHelperConfig) => { export const createRpcHelper = (config: RpcHelperConfig) => {
const { client, rawDb } = config const { client, rawDb, logger } = config
const onNewRpc = safeCatch( const onNewRpc = safeCatch(
`onNewRpc`, `onNewRpc`,
logger,
async (cb: (e: RpcFields<any, any>) => void) => { async (cb: (e: RpcFields<any, any>) => void) => {
const unsub = await client const unsub = await client
.collection(RPC_COLLECTION) .collection(RPC_COLLECTION)
@ -32,7 +33,7 @@ export const createRpcHelper = (config: RpcHelperConfig) => {
} }
) )
const resetRpcs = safeCatch(`resetRpcs`, async () => const resetRpcs = safeCatch(`resetRpcs`, logger, async () =>
rawDb(RPC_COLLECTION) rawDb(RPC_COLLECTION)
.whereNotIn('status', [ .whereNotIn('status', [
RpcStatus.FinishedError, RpcStatus.FinishedError,
@ -44,7 +45,7 @@ export const createRpcHelper = (config: RpcHelperConfig) => {
}) })
) )
const incompleteRpcs = safeCatch(`incompleteRpcs`, async () => { const incompleteRpcs = safeCatch(`incompleteRpcs`, logger, async () => {
return client return client
.collection(RPC_COLLECTION) .collection(RPC_COLLECTION)
.getFullList<RpcFields<any, any>>(100, { .getFullList<RpcFields<any, any>>(100, {
@ -54,6 +55,7 @@ export const createRpcHelper = (config: RpcHelperConfig) => {
const rejectRpc = safeCatch( const rejectRpc = safeCatch(
`rejectRpc`, `rejectRpc`,
logger,
async (rpc: RpcFields<any, any>, err: Error) => { async (rpc: RpcFields<any, any>, err: Error) => {
const fields: Partial<RpcFields<any, any>> = { const fields: Partial<RpcFields<any, any>> = {
status: RpcStatus.FinishedError, status: RpcStatus.FinishedError,
@ -67,6 +69,7 @@ export const createRpcHelper = (config: RpcHelperConfig) => {
const setRpcStatus = safeCatch( const setRpcStatus = safeCatch(
`setRpcStatus`, `setRpcStatus`,
logger,
async ( async (
rpc: RpcFields<any, any>, rpc: RpcFields<any, any>,
status: RpcStatus, status: RpcStatus,

View File

@ -5,12 +5,19 @@ import {
PUBLIC_APP_DOMAIN, PUBLIC_APP_DOMAIN,
PUBLIC_APP_PROTOCOL, PUBLIC_APP_PROTOCOL,
} from '$constants' } from '$constants'
import { logger, mkSingleton } from '@pockethost/common' import { Logger, mkSingleton } from '@pockethost/common'
import { createPbClient } from './PbClient' import { createPbClient } from './PbClient'
export const clientService = mkSingleton(async (url: string) => { export type ClientServiceConfig = {
const { dbg, error } = logger().create(`client singleton`) logger: Logger
const client = createPbClient(url) url: string
}
export const clientService = mkSingleton(async (cfg: ClientServiceConfig) => {
const { logger, url } = cfg
const _clientLogger = logger.create(`client singleton`)
const { dbg, error } = _clientLogger
const client = createPbClient(url, _clientLogger)
try { try {
await client.adminAuthViaEmail(DAEMON_PB_USERNAME, DAEMON_PB_PASSWORD) await client.adminAuthViaEmail(DAEMON_PB_USERNAME, DAEMON_PB_PASSWORD)

View File

@ -1,4 +1,3 @@
export * from './BackupService'
export * from './clientService/clientService' export * from './clientService/clientService'
export * from './clientService/PbClient' export * from './clientService/PbClient'
export * from './FtpService/FtpService' export * from './FtpService/FtpService'

View File

@ -1,11 +1,15 @@
import { logger } from '@pockethost/common' import { Logger } from '@pockethost/common'
import { chmodSync } from 'fs' import { chmodSync } from 'fs'
import fetch from 'node-fetch' import fetch from 'node-fetch'
import { dirname } from 'path' import { dirname } from 'path'
import { Extract } from 'unzipper' import { Extract } from 'unzipper'
export const downloadAndExtract = async (url: string, binPath: string) => { export const downloadAndExtract = async (
const { dbg, error } = logger().create('downloadAndExtract') url: string,
binPath: string,
logger: Logger
) => {
const { dbg, error } = logger.create('downloadAndExtract')
await new Promise<void>(async (resolve, reject) => { await new Promise<void>(async (resolve, reject) => {
dbg(`Fetching ${url}`) dbg(`Fetching ${url}`)

View File

@ -1,4 +1,3 @@
export * from './backupInstance'
export * from './downloadAndExtract' export * from './downloadAndExtract'
export * from './ensureDirExists' export * from './ensureDirExists'
export * from './env' export * from './env'

View File

@ -1,19 +1,20 @@
import { logger, safeCatch } from '@pockethost/common' import { Logger, safeCatch } from '@pockethost/common'
import { exec } from 'child_process' import { exec } from 'child_process'
export const pexec = safeCatch(`pexec`, (cmd: string) => { export const pexec = (logger: Logger) =>
const { dbg, error } = logger().create('pexec') safeCatch(`pexec`, logger, (cmd: string) => {
return new Promise<void>((resolve, reject) => { const { dbg, error } = logger.create('pexec')
dbg(cmd) return new Promise<void>((resolve, reject) => {
exec(cmd, (err, stdout, stderr) => { dbg(cmd)
dbg(stdout) exec(cmd, (err, stdout, stderr) => {
if (err) { dbg(stdout)
error(`${err}`) if (err) {
error(stderr) error(`${err}`)
reject(err) error(stderr)
return reject(err)
} return
resolve() }
resolve()
})
}) })
}) })
})

View File

@ -1,29 +1,32 @@
import { logger, safeCatch } from '@pockethost/common' import { Logger, safeCatch } from '@pockethost/common'
export const tryFetch = safeCatch( export const tryFetch = (logger: Logger) =>
`tryFetch`, safeCatch(
(url: string, preflight?: () => Promise<boolean>) => { `tryFetch`,
const { dbg } = logger().create('tryFetch') logger,
return new Promise<void>((resolve, reject) => { (url: string, preflight?: () => Promise<boolean>) => {
const tryFetch = async () => { const { dbg } = logger.create('tryFetch')
if (preflight) { return new Promise<void>((resolve, reject) => {
dbg(`Checking preflight`) const tryFetch = async () => {
const shouldFetch = await preflight() if (preflight) {
if (!shouldFetch) { dbg(`Checking preflight`)
throw new Error(`tryFetch failed preflight, aborting`) const shouldFetch = await preflight()
if (!shouldFetch) {
reject(new Error(`tryFetch failed preflight, aborting`))
return
}
}
try {
dbg(`Trying to fetch ${url} `)
const res = await fetch(url)
dbg(`Fetch ${url} successful`)
resolve()
} catch (e) {
dbg(`Could not fetch ${url}, trying again in 1s`)
setTimeout(tryFetch, 1000)
} }
} }
try { tryFetch()
dbg(`Trying to fetch ${url} `) })
const res = await fetch(url) }
dbg(`Fetch ${url} successful`) )
resolve()
} catch (e) {
dbg(`Could not fetch ${url}, trying again in 1s`)
setTimeout(tryFetch, 1000)
}
}
tryFetch()
})
}
)

View File

@ -44,7 +44,8 @@ export type PocketbaseClient = ReturnType<typeof createPocketbaseClient>
export const createPocketbaseClient = (config: PocketbaseClientConfig) => { export const createPocketbaseClient = (config: PocketbaseClientConfig) => {
const { url } = config const { url } = config
const { dbg, error } = logger() const _logger = logger()
const { dbg, error } = _logger
const client = new PocketBase(url) const client = new PocketBase(url)
@ -56,7 +57,7 @@ export const createPocketbaseClient = (config: PocketbaseClientConfig) => {
const logOut = () => authStore.clear() const logOut = () => authStore.clear()
const createUser = safeCatch(`createUser`, (email: string, password: string) => const createUser = safeCatch(`createUser`, _logger, (email: string, password: string) =>
client client
.collection('users') .collection('users')
.create({ .create({
@ -70,7 +71,7 @@ export const createPocketbaseClient = (config: PocketbaseClientConfig) => {
}) })
) )
const confirmVerification = safeCatch(`confirmVerification`, (token: string) => const confirmVerification = safeCatch(`confirmVerification`, _logger, (token: string) =>
client client
.collection('users') .collection('users')
.confirmVerification(token) .confirmVerification(token)
@ -79,7 +80,7 @@ export const createPocketbaseClient = (config: PocketbaseClientConfig) => {
}) })
) )
const requestPasswordReset = safeCatch(`requestPasswordReset`, (email: string) => const requestPasswordReset = safeCatch(`requestPasswordReset`, _logger, (email: string) =>
client client
.collection('users') .collection('users')
.requestPasswordReset(email) .requestPasswordReset(email)
@ -90,6 +91,7 @@ export const createPocketbaseClient = (config: PocketbaseClientConfig) => {
const requestPasswordResetConfirm = safeCatch( const requestPasswordResetConfirm = safeCatch(
`requestPasswordResetConfirm`, `requestPasswordResetConfirm`,
_logger,
(token: string, password: string) => (token: string, password: string) =>
client client
.collection('users') .collection('users')
@ -99,11 +101,11 @@ export const createPocketbaseClient = (config: PocketbaseClientConfig) => {
}) })
) )
const authViaEmail = safeCatch(`authViaEmail`, (email: string, password: string) => const authViaEmail = safeCatch(`authViaEmail`, _logger, (email: string, password: string) =>
client.collection('users').authWithPassword(email, password) client.collection('users').authWithPassword(email, password)
) )
const refreshAuthToken = safeCatch(`refreshAuthToken`, () => const refreshAuthToken = safeCatch(`refreshAuthToken`, _logger, () =>
client.collection('users').authRefresh() client.collection('users').authRefresh()
) )
@ -123,6 +125,7 @@ export const createPocketbaseClient = (config: PocketbaseClientConfig) => {
const getInstanceById = safeCatch( const getInstanceById = safeCatch(
`getInstanceById`, `getInstanceById`,
_logger,
(id: InstanceId): Promise<InstanceFields | undefined> => (id: InstanceId): Promise<InstanceFields | undefined> =>
client.collection('instances').getOne<InstanceFields>(id) client.collection('instances').getOne<InstanceFields>(id)
) )
@ -145,7 +148,7 @@ export const createPocketbaseClient = (config: PocketbaseClientConfig) => {
return map(e.data.data, (v, k) => (v ? v.message : undefined)).filter((v) => !!v) return map(e.data.data, (v, k) => (v ? v.message : undefined)).filter((v) => !!v)
} }
const resendVerificationEmail = safeCatch(`resendVerificationEmail`, async () => { const resendVerificationEmail = safeCatch(`resendVerificationEmail`, _logger, async () => {
const user = client.authStore.model const user = client.authStore.model
assertExists(user, `Login required`) assertExists(user, `Login required`)
await client.collection('users').requestVerification(user.email) await client.collection('users').requestVerification(user.email)
@ -210,7 +213,7 @@ export const createPocketbaseClient = (config: PocketbaseClientConfig) => {
unsub() unsub()
return return
} }
const _check = safeCatch(`_checkVerified`, refreshAuthToken) const _check = safeCatch(`_checkVerified`, _logger, refreshAuthToken)
setTimeout(_check, 1000) setTimeout(_check, 1000)
// FIXME - THIS DOES NOT WORK, WE HAVE TO POLL INSTEAD. FIX IN V0.8 // FIXME - THIS DOES NOT WORK, WE HAVE TO POLL INSTEAD. FIX IN V0.8