feat: Add in overlay

This commit is contained in:
J H
2024-01-30 17:52:58 -07:00
parent 01ad2421b4
commit ac51aa1924
10 changed files with 218 additions and 56 deletions

View File

@@ -89,7 +89,7 @@ export class StartSdk<Manifest extends SDKManifest, Store> {
}, },
fn: (options: { fn: (options: {
effects: Effects effects: Effects
utils: Utils<Store> utils: Utils<Manifest, Store>
input: Type input: Type
}) => Promise<ActionResult>, }) => Promise<ActionResult>,
) => { ) => {
@@ -105,11 +105,11 @@ export class StartSdk<Manifest extends SDKManifest, Store> {
>( >(
metaData: (options: { metaData: (options: {
effects: Effects effects: Effects
utils: Utils<Store> utils: Utils<Manifest, Store>
}) => MaybePromise<Omit<ActionMetadata, "input">>, }) => MaybePromise<Omit<ActionMetadata, "input">>,
fn: (options: { fn: (options: {
effects: Effects effects: Effects
utils: Utils<Store> utils: Utils<Manifest, Store>
input: Type input: Type
}) => Promise<ActionResult>, }) => Promise<ActionResult>,
input: Config<Type, Store> | Config<Type, never>, input: Config<Type, Store> | Config<Type, never>,
@@ -136,7 +136,7 @@ export class StartSdk<Manifest extends SDKManifest, Store> {
>( >(
spec: ConfigType, spec: ConfigType,
write: Save<Store, Type, Manifest>, write: Save<Store, Type, Manifest>,
read: Read<Store, Type>, read: Read<Manifest, Store, Type>,
) => setupConfig<Store, ConfigType, Manifest, Type>(spec, write, read), ) => setupConfig<Store, ConfigType, Manifest, Type>(spec, write, read),
setupConfigRead: < setupConfigRead: <
ConfigSpec extends ConfigSpec extends
@@ -144,7 +144,7 @@ export class StartSdk<Manifest extends SDKManifest, Store> {
| Config<Record<string, never>, never>, | Config<Record<string, never>, never>,
>( >(
_configSpec: ConfigSpec, _configSpec: ConfigSpec,
fn: Read<Store, ConfigSpec>, fn: Read<Manifest, Store, ConfigSpec>,
) => fn, ) => fn,
setupConfigSave: < setupConfigSave: <
ConfigSpec extends ConfigSpec extends
@@ -158,6 +158,7 @@ export class StartSdk<Manifest extends SDKManifest, Store> {
config: Config<Input, Store> | Config<Input, never>, config: Config<Input, Store> | Config<Input, never>,
autoConfigs: { autoConfigs: {
[K in keyof Manifest["dependencies"]]: DependencyConfig< [K in keyof Manifest["dependencies"]]: DependencyConfig<
Manifest,
Store, Store,
Input, Input,
any any
@@ -192,9 +193,9 @@ export class StartSdk<Manifest extends SDKManifest, Store> {
fn: (o: { fn: (o: {
effects: Effects effects: Effects
started(onTerm: () => void): null started(onTerm: () => void): null
utils: Utils<Store, {}> utils: Utils<Manifest, Store, {}>
}) => Promise<Daemons<any>>, }) => Promise<Daemons<any>>,
) => setupMain<Store>(fn), ) => setupMain<Manifest, Store>(fn),
setupMigrations: <Migrations extends Array<Migration<Store, any>>>( setupMigrations: <Migrations extends Array<Migration<Store, any>>>(
...migrations: EnsureUniqueId<Migrations> ...migrations: EnsureUniqueId<Migrations>
) => setupMigrations<Store, Migrations>(this.manifest, ...migrations), ) => setupMigrations<Store, Migrations>(this.manifest, ...migrations),
@@ -238,14 +239,16 @@ export class StartSdk<Manifest extends SDKManifest, Store> {
dependencyConfig: (options: { dependencyConfig: (options: {
effects: Effects effects: Effects
localConfig: LocalConfig localConfig: LocalConfig
utils: Utils<Store> utils: Utils<Manifest, Store>
}) => Promise<void | DeepPartial<RemoteConfig>> }) => Promise<void | DeepPartial<RemoteConfig>>
update?: Update<void | DeepPartial<RemoteConfig>, RemoteConfig> update?: Update<void | DeepPartial<RemoteConfig>, RemoteConfig>
}) { }) {
return new DependencyConfig<Store, LocalConfig, RemoteConfig>( return new DependencyConfig<
dependencyConfig, Manifest,
update, Store,
) LocalConfig,
RemoteConfig
>(dependencyConfig, update)
}, },
}, },
List: { List: {
@@ -320,10 +323,13 @@ export class StartSdk<Manifest extends SDKManifest, Store> {
Migration: { Migration: {
of: <Version extends ManifestVersion>(options: { of: <Version extends ManifestVersion>(options: {
version: Version version: Version
up: (opts: { effects: Effects; utils: Utils<Store> }) => Promise<void> up: (opts: {
effects: Effects
utils: Utils<Manifest, Store>
}) => Promise<void>
down: (opts: { down: (opts: {
effects: Effects effects: Effects
utils: Utils<Store> utils: Utils<Manifest, Store>
}) => Promise<void> }) => Promise<void>
}) => Migration.of<Store, Version>(options), }) => Migration.of<Store, Version>(options),
}, },

View File

@@ -22,7 +22,7 @@ export type Save<
> = (options: { > = (options: {
effects: Effects effects: Effects
input: ExtractConfigType<A> & Record<string, any> input: ExtractConfigType<A> & Record<string, any>
utils: Utils<Store> utils: Utils<Manifest, Store>
dependencies: D.ConfigDependencies<Manifest> dependencies: D.ConfigDependencies<Manifest>
}) => Promise<{ }) => Promise<{
dependenciesReceipt: DependenciesReceipt dependenciesReceipt: DependenciesReceipt
@@ -30,6 +30,7 @@ export type Save<
restart: boolean restart: boolean
}> }>
export type Read< export type Read<
Manifest extends SDKManifest,
Store, Store,
A extends A extends
| Record<string, any> | Record<string, any>
@@ -37,7 +38,7 @@ export type Read<
| Config<Record<string, any>, never>, | Config<Record<string, any>, never>,
> = (options: { > = (options: {
effects: Effects effects: Effects
utils: Utils<Store> utils: Utils<Manifest, Store>
}) => Promise<void | (ExtractConfigType<A> & Record<string, any>)> }) => Promise<void | (ExtractConfigType<A> & Record<string, any>)>
/** /**
* We want to setup a config export with a get and set, this * We want to setup a config export with a get and set, this
@@ -57,7 +58,7 @@ export function setupConfig<
>( >(
spec: Config<Type, Store> | Config<Type, never>, spec: Config<Type, Store> | Config<Type, never>,
write: Save<Store, Type, Manifest>, write: Save<Store, Type, Manifest>,
read: Read<Store, Type>, read: Read<Manifest, Store, Type>,
) { ) {
const validator = spec.validator const validator = spec.validator
return { return {
@@ -79,7 +80,7 @@ export function setupConfig<
} }
}) as ExpectedExports.setConfig, }) as ExpectedExports.setConfig,
getConfig: (async ({ effects }) => { getConfig: (async ({ effects }) => {
const myUtils = utils<Store>(effects) const myUtils = utils<Manifest, Store>(effects)
const configValue = nullIfEmpty( const configValue = nullIfEmpty(
(await read({ effects, utils: myUtils })) || null, (await read({ effects, utils: myUtils })) || null,
) )

View File

@@ -6,6 +6,7 @@ import {
import { Utils, utils } from "../util/utils" import { Utils, utils } from "../util/utils"
import { deepEqual } from "../util/deepEqual" import { deepEqual } from "../util/deepEqual"
import { deepMerge } from "../util/deepMerge" import { deepMerge } from "../util/deepMerge"
import { SDKManifest } from "../manifest/ManifestTypes"
export type Update<QueryResults, RemoteConfig> = (options: { export type Update<QueryResults, RemoteConfig> = (options: {
remoteConfig: RemoteConfig remoteConfig: RemoteConfig
@@ -13,6 +14,7 @@ export type Update<QueryResults, RemoteConfig> = (options: {
}) => Promise<RemoteConfig> }) => Promise<RemoteConfig>
export class DependencyConfig< export class DependencyConfig<
Manifest extends SDKManifest,
Store, Store,
Input extends Record<string, any>, Input extends Record<string, any>,
RemoteConfig extends Record<string, any>, RemoteConfig extends Record<string, any>,
@@ -27,7 +29,7 @@ export class DependencyConfig<
readonly dependencyConfig: (options: { readonly dependencyConfig: (options: {
effects: Effects effects: Effects
localConfig: Input localConfig: Input
utils: Utils<Store> utils: Utils<Manifest, Store>
}) => Promise<void | DeepPartial<RemoteConfig>>, }) => Promise<void | DeepPartial<RemoteConfig>>,
readonly update: Update< readonly update: Update<
void | DeepPartial<RemoteConfig>, void | DeepPartial<RemoteConfig>,
@@ -39,7 +41,7 @@ export class DependencyConfig<
return this.dependencyConfig({ return this.dependencyConfig({
localConfig: options.localConfig as Input, localConfig: options.localConfig as Input,
effects: options.effects, effects: options.effects,
utils: utils<Store>(options.effects), utils: utils<Manifest, Store>(options.effects),
}) })
} }
} }

View File

@@ -11,6 +11,7 @@ export function setupDependencyConfig<
_config: Config<Input, Store> | Config<Input, never>, _config: Config<Input, Store> | Config<Input, never>,
autoConfigs: { autoConfigs: {
[key in keyof Manifest["dependencies"] & string]: DependencyConfig< [key in keyof Manifest["dependencies"] & string]: DependencyConfig<
Manifest,
Store, Store,
Input, Input,
any any

View File

@@ -6,6 +6,7 @@ import "../interfaces/NetworkInterfaceBuilder"
import "../interfaces/Origin" import "../interfaces/Origin"
import "./Daemons" import "./Daemons"
import { SDKManifest } from "../manifest/ManifestTypes"
/** /**
* Used to ensure that the main function is running with the valid proofs. * Used to ensure that the main function is running with the valid proofs.
@@ -17,17 +18,17 @@ import "./Daemons"
* @param fn * @param fn
* @returns * @returns
*/ */
export const setupMain = <Store>( export const setupMain = <Manifest extends SDKManifest, Store>(
fn: (o: { fn: (o: {
effects: Effects effects: Effects
started(onTerm: () => void): null started(onTerm: () => void): null
utils: Utils<Store, {}> utils: Utils<Manifest, Store, {}>
}) => Promise<Daemons<any>>, }) => Promise<Daemons<any>>,
): ExpectedExports.main => { ): ExpectedExports.main => {
return async (options) => { return async (options) => {
const result = await fn({ const result = await fn({
...options, ...options,
utils: createMainUtils<Store>(options.effects), utils: createMainUtils<Manifest, Store>(options.effects),
}) })
await result.build().then((x) => x.wait()) await result.build().then((x) => x.wait())
} }

View File

@@ -7,6 +7,7 @@ type Store = {
someValue: "a" | "b" someValue: "a" | "b"
} }
} }
type Manifest = any
const todo = <A>(): A => { const todo = <A>(): A => {
throw new Error("not implemented") throw new Error("not implemented")
} }
@@ -14,14 +15,17 @@ const noop = () => {}
describe("Store", () => { describe("Store", () => {
test("types", async () => { test("types", async () => {
;async () => { ;async () => {
utils<Store>(todo<Effects>()).store.setOwn("/config", { utils<Manifest, Store>(todo<Effects>()).store.setOwn("/config", {
someValue: "a", someValue: "a",
}) })
utils<Store>(todo<Effects>()).store.setOwn("/config/someValue", "b") utils<Manifest, Store>(todo<Effects>()).store.setOwn(
utils<Store>(todo<Effects>()).store.setOwn("", { "/config/someValue",
"b",
)
utils<Manifest, Store>(todo<Effects>()).store.setOwn("", {
config: { someValue: "b" }, config: { someValue: "b" },
}) })
utils<Store>(todo<Effects>()).store.setOwn( utils<Manifest, Store>(todo<Effects>()).store.setOwn(
"/config/someValue", "/config/someValue",
// @ts-expect-error Type is wrong for the setting value // @ts-expect-error Type is wrong for the setting value
@@ -48,10 +52,10 @@ describe("Store", () => {
path: "/config/some2Value", path: "/config/some2Value",
value: "a", value: "a",
}) })
;(await createMainUtils<Store>(todo<Effects>()) ;(await createMainUtils<Manifest, Store>(todo<Effects>())
.store.getOwn("/config/someValue") .store.getOwn("/config/someValue")
.const()) satisfies string .const()) satisfies string
;(await createMainUtils<Store>(todo<Effects>()) ;(await createMainUtils<Manifest, Store>(todo<Effects>())
.store.getOwn("/config") .store.getOwn("/config")
.const()) satisfies Store["config"] .const()) satisfies Store["config"]
await createMainUtils(todo<Effects>()) await createMainUtils(todo<Effects>())
@@ -60,31 +64,31 @@ describe("Store", () => {
.const() .const()
/// ----------------- ERRORS ----------------- /// ----------------- ERRORS -----------------
utils<Store>(todo<Effects>()).store.setOwn("", { utils<Manifest, Store>(todo<Effects>()).store.setOwn("", {
// @ts-expect-error Type is wrong for the setting value // @ts-expect-error Type is wrong for the setting value
config: { someValue: "notInAOrB" }, config: { someValue: "notInAOrB" },
}) })
utils<Store>(todo<Effects>()).store.setOwn( utils<Manifest, Store>(todo<Effects>()).store.setOwn(
"/config/someValue", "/config/someValue",
// @ts-expect-error Type is wrong for the setting value // @ts-expect-error Type is wrong for the setting value
"notInAOrB", "notInAOrB",
) )
;(await utils<Store>(todo<Effects>()) ;(await utils<Manifest, Store>(todo<Effects>())
.store.getOwn("/config/someValue") .store.getOwn("/config/someValue")
// @ts-expect-error Const should normally not be callable // @ts-expect-error Const should normally not be callable
.const()) satisfies string .const()) satisfies string
;(await utils<Store>(todo<Effects>()) ;(await utils<Manifest, Store>(todo<Effects>())
.store.getOwn("/config") .store.getOwn("/config")
// @ts-expect-error Const should normally not be callable // @ts-expect-error Const should normally not be callable
.const()) satisfies Store["config"] .const()) satisfies Store["config"]
await utils<Store>(todo<Effects>()) await utils<Manifest, Store>(todo<Effects>())
// @ts-expect-error Path is wrong // @ts-expect-error Path is wrong
.store.getOwn("/config/somdsfeValue") .store.getOwn("/config/somdsfeValue")
// @ts-expect-error Const should normally not be callable // @ts-expect-error Const should normally not be callable
.const() .const()
/// ///
;(await utils<Store>(todo<Effects>()) ;(await utils<Manifest, Store>(todo<Effects>())
.store.getOwn("/config/someValue") .store.getOwn("/config/someValue")
// @ts-expect-error satisfies type is wrong // @ts-expect-error satisfies type is wrong
.const()) satisfies number .const()) satisfies number

View File

@@ -2,6 +2,7 @@ export * as configTypes from "./config/configTypes"
import { InputSpec } from "./config/configTypes" import { InputSpec } from "./config/configTypes"
import { DependenciesReceipt } from "./config/setupConfig" import { DependenciesReceipt } from "./config/setupConfig"
import { PortOptions } from "./interfaces/Host" import { PortOptions } from "./interfaces/Host"
import { Overlay } from "./util/Overlay"
import { UrlString } from "./util/getNetworkInterface" import { UrlString } from "./util/getNetworkInterface"
import { NetworkInterfaceType, Signals } from "./util/utils" import { NetworkInterfaceType, Signals } from "./util/utils"
@@ -225,7 +226,7 @@ export type Effects = {
input: Input input: Input
}): Promise<unknown> }): Promise<unknown>
/** The idea is that we want to create a sub image. This would be useful for things like creating a ro mode for sandbox. */ /** A low level api used by makeOverlay */
createOverlayedImage(options: { imageId: string }): Promise<string> createOverlayedImage(options: { imageId: string }): Promise<string>
/** Removes all network bindings */ /** Removes all network bindings */

92
lib/util/Overlay.ts Normal file
View File

@@ -0,0 +1,92 @@
import fs from "fs/promises"
import * as T from "../types"
import cp from "child_process"
import { promisify } from "util"
import { Buffer } from "node:buffer"
export const execFile = promisify(cp.execFile)
export class Overlay {
private constructor(readonly effects: T.Effects, readonly rootfs: string) {}
static async of(effects: T.Effects, imageId: string) {
const rootfs = await effects.createOverlayedImage({ imageId })
for (const dirPart of ["dev", "sys", "proc", "run"] as const) {
const dir = await fs.mkdir(`${rootfs}/${dirPart}`, { recursive: true })
if (!dir) break
await execFile("mount", ["--bind", `/${dirPart}`, dir])
}
return new Overlay(effects, rootfs)
}
async mount(options: MountOptions, path: string): Promise<Overlay> {
path = path.startsWith("/")
? `${this.rootfs}${path}`
: `${this.rootfs}/${path}`
if (options.type === "volume") {
await execFile("mount", [
"--bind",
`/media/startos/volumes/${options.id}`,
path,
])
} else if (options.type === "assets") {
await execFile("mount", [
"--bind",
`/media/startos/assets/${options.id}`,
path,
])
} else if (options.type === "pointer") {
await this.effects.mount({ location: path, target: options })
} else {
throw new Error(`unknown type ${(options as any).type}`)
}
return this
}
async destroy() {
await execFile("umount", ["-R", this.rootfs])
await fs.rm(this.rootfs, { recursive: true, force: true })
}
async exec(
command: string[],
options?: CommandOptions,
): Promise<{ stdout: string | Buffer; stderr: string | Buffer }> {
return await execFile("chroot", [this.rootfs, ...command], options)
}
spawn(
command: string[],
options?: CommandOptions,
): cp.ChildProcessWithoutNullStreams {
return cp.spawn("chroot", [this.rootfs, ...command], options)
}
}
export type CommandOptions = {
env?: { [variable: string]: string }
cwd?: string
}
export type MountOptions =
| MountOptionsVolume
| MountOptionsAssets
| MountOptionsPointer
export type MountOptionsVolume = {
type: "volume"
id: string
}
export type MountOptionsAssets = {
type: "assets"
id: string
}
export type MountOptionsPointer = {
type: "pointer"
packageId: string
volumeId: string
path: string
readonly: boolean
}

View File

@@ -5,8 +5,10 @@ import "./fileHelper"
import "../store/getStore" import "../store/getStore"
import "./deepEqual" import "./deepEqual"
import "./deepMerge" import "./deepMerge"
import "./Overlay"
import "./once" import "./once"
import { utils } from "./utils" import { utils } from "./utils"
import { SDKManifest } from "../manifest/ManifestTypes"
// prettier-ignore // prettier-ignore
export type FlattenIntersection<T> = export type FlattenIntersection<T> =
@@ -22,8 +24,9 @@ export const isKnownError = (e: unknown): e is T.KnownError =>
declare const affine: unique symbol declare const affine: unique symbol
export const createUtils = utils export const createUtils = utils
export const createMainUtils = <Store>(effects: T.Effects) => export const createMainUtils = <Manifest extends SDKManifest, Store>(
createUtils<Store, {}>(effects) effects: T.Effects,
) => createUtils<Manifest, Store, {}>(effects)
type NeverPossible = { [affine]: string } type NeverPossible = { [affine]: string }
export type NoAny<A> = NeverPossible extends A export type NoAny<A> = NeverPossible extends A

View File

@@ -37,6 +37,8 @@ import {
import * as CP from "node:child_process" import * as CP from "node:child_process"
import { promisify } from "node:util" import { promisify } from "node:util"
import { splitCommand } from "./splitCommand" import { splitCommand } from "./splitCommand"
import { SDKManifest } from "../manifest/ManifestTypes"
import { MountOptions, Overlay, CommandOptions } from "./Overlay"
export type Signals = NodeJS.Signals export type Signals = NodeJS.Signals
export const SIGTERM: Signals = "SIGTERM" export const SIGTERM: Signals = "SIGTERM"
@@ -50,7 +52,11 @@ const childProcess = {
export type NetworkInterfaceType = "ui" | "p2p" | "api" | "other" export type NetworkInterfaceType = "ui" | "p2p" | "api" | "other"
export type Utils<Store, WrapperOverWrite = { const: never }> = { export type Utils<
Manifest extends SDKManifest,
Store,
WrapperOverWrite = { const: never },
> = {
checkPortListening( checkPortListening(
port: number, port: number,
options: { options: {
@@ -107,9 +113,19 @@ export type Utils<Store, WrapperOverWrite = { const: never }> = {
}) => GetNetworkInterfaces & WrapperOverWrite }) => GetNetworkInterfaces & WrapperOverWrite
} }
nullIfEmpty: typeof nullIfEmpty nullIfEmpty: typeof nullIfEmpty
runDaemon: <A extends string>( runCommand: <A extends string>(
imageId: Manifest["images"][number],
command: ValidIfNoStupidEscape<A> | [string, ...string[]], command: ValidIfNoStupidEscape<A> | [string, ...string[]],
options: { env?: Record<string, string> }, options: CommandOptions & {
mounts?: { path: string; options: MountOptions }[]
},
) => Promise<{ stdout: string | Buffer; stderr: string | Buffer }>
runDaemon: <A extends string>(
imageId: Manifest["images"][number],
command: ValidIfNoStupidEscape<A> | [string, ...string[]],
options: CommandOptions & {
mounts?: { path: string; options: MountOptions }[]
},
) => Promise<DaemonReturned> ) => Promise<DaemonReturned>
store: { store: {
get: <Path extends string>( get: <Path extends string>(
@@ -125,9 +141,13 @@ export type Utils<Store, WrapperOverWrite = { const: never }> = {
) => Promise<void> ) => Promise<void>
} }
} }
export const utils = <Store = never, WrapperOverWrite = { const: never }>( export const utils = <
Manifest extends SDKManifest,
Store = never,
WrapperOverWrite = { const: never },
>(
effects: Effects, effects: Effects,
): Utils<Store, WrapperOverWrite> => { ): Utils<Manifest, Store, WrapperOverWrite> => {
return { return {
createInterface: (options: { createInterface: (options: {
name: string name: string
@@ -181,12 +201,39 @@ export const utils = <Store = never, WrapperOverWrite = { const: never }>(
) => effects.store.set<Store, Path>({ value, path: path as any }), ) => effects.store.set<Store, Path>({ value, path: path as any }),
}, },
runDaemon: async <A extends string>( runCommand: async <A extends string>(
imageId: Manifest["images"][number],
command: ValidIfNoStupidEscape<A> | [string, ...string[]], command: ValidIfNoStupidEscape<A> | [string, ...string[]],
options: { env?: Record<string, string> }, options: CommandOptions & {
mounts?: { path: string; options: MountOptions }[]
},
): Promise<{ stdout: string | Buffer; stderr: string | Buffer }> => {
const commands = splitCommand(command)
const overlay = await Overlay.of(effects, imageId)
try {
for (let mount of options.mounts || []) {
await overlay.mount(mount.options, mount.path)
}
return await overlay.exec(commands)
} finally {
await overlay.destroy()
}
},
runDaemon: async <A extends string>(
imageId: Manifest["images"][number],
command: ValidIfNoStupidEscape<A> | [string, ...string[]],
options: CommandOptions & {
mounts?: { path: string; options: MountOptions }[]
},
): Promise<DaemonReturned> => { ): Promise<DaemonReturned> => {
const commands = splitCommand(command) const commands = splitCommand(command)
const childProcess = CP.spawn(commands[0], commands.slice(1), options) const overlay = await Overlay.of(effects, imageId)
for (let mount of options.mounts || []) {
await overlay.mount(mount.options, mount.path)
}
const childProcess = overlay.spawn(commands, {
env: options.env,
})
const answer = new Promise<string>((resolve, reject) => { const answer = new Promise<string>((resolve, reject) => {
const output: string[] = [] const output: string[] = []
childProcess.stdout.on("data", (data) => { childProcess.stdout.on("data", (data) => {
@@ -210,18 +257,22 @@ export const utils = <Store = never, WrapperOverWrite = { const: never }>(
return answer return answer
}, },
async term({ signal = SIGTERM, timeout = NO_TIMEOUT } = {}) { async term({ signal = SIGTERM, timeout = NO_TIMEOUT } = {}) {
childProcess.kill(signal) try {
childProcess.kill(signal)
if (timeout <= NO_TIMEOUT) { if (timeout <= NO_TIMEOUT) {
const didTimeout = await Promise.race([ const didTimeout = await Promise.race([
new Promise((resolve) => setTimeout(resolve, timeout)).then( new Promise((resolve) => setTimeout(resolve, timeout)).then(
() => true, () => true,
), ),
answer.then(() => false), answer.then(() => false),
]) ])
if (didTimeout) childProcess.kill(SIGKILL) if (didTimeout) childProcess.kill(SIGKILL)
}
await answer
} finally {
await overlay.destroy()
} }
await answer
}, },
} }
}, },