Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Pet Daemon #1413

Closed
wants to merge 23 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
c43f578
fix(daemon): Log to state, not cache
kriskowal Dec 15, 2022
4171cb9
refactor(daemon): Debugging comments
kriskowal Dec 20, 2022
0eca5b1
refactor(daemon): Endo daemon bootstrap is the private facet
kriskowal Dec 9, 2022
3c5d25b
refactor(daemon): Tautological restart condition
kriskowal Dec 20, 2022
3947ec9
feat(daemon): Thread ephemeral state path
kriskowal Jan 3, 2023
74da700
feat(daemon): Clean up socket on halt
kriskowal Dec 20, 2022
d0d01c7
feat(daemon): Reset restarts if currently running
kriskowal Dec 20, 2022
158e4b6
chore(daemon): Ignore test output artifacts
kriskowal Dec 20, 2022
d9db807
feat(cli): Thread ephemeral state path
kriskowal Jan 3, 2023
2a9445a
feat(cli): Log follow option
kriskowal Dec 20, 2022
3c7e702
feat(daemon): RefReader and ReaderRef
kriskowal Dec 20, 2022
f7b5995
feat(daemon): Pet names, spawn, eval, store, durable ephemera, worker…
kriskowal Dec 20, 2022
2ecf21a
test(daemon): Test store
kriskowal Jan 8, 2023
da57e6e
test(daemon): Closure state lost on restart using eval
kriskowal Jan 8, 2023
35d956b
feat(cli): Store archive as pet name
kriskowal Dec 20, 2022
1506968
feat(cli): Store readable blob
kriskowal Dec 20, 2022
74045b6
feat(cli): Spawn worker
kriskowal Dec 20, 2022
5b4b1cc
feat(cli): Show pet name
kriskowal Dec 20, 2022
8af7082
feat(cli): Cat command
kriskowal Dec 20, 2022
4c55ff5
feat(cli): Eval in worker
kriskowal Dec 20, 2022
017cee3
feat(cli): Start daemon on demand
kriskowal Dec 28, 2022
e9ecc9b
feat(cli): Log follow watches for reset
kriskowal Dec 23, 2022
ed3c23e
feat(cli): Follow command
kriskowal Dec 28, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
"@endo/far": "^0.2.18",
"@endo/lockdown": "^0.1.28",
"@endo/promise-kit": "^0.2.56",
"@endo/stream-node": "^0.2.26",
"@endo/where": "^0.3.1",
"commander": "^5.0.0",
"ses": "^0.18.4"
Expand Down
286 changes: 262 additions & 24 deletions packages/cli/src/endo.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
/* global process */
/* global process, setTimeout, clearTimeout */
/* eslint-disable no-await-in-loop */

// Establish a perimeter:
import 'ses';
Expand All @@ -21,18 +22,28 @@ import {
clean,
reset,
makeEndoClient,
makeReaderRef,
makeRefIterator,
makeRefReader,
} from '@endo/daemon';
import { whereEndoState, whereEndoSock, whereEndoCache } from '@endo/where';
import {
whereEndoState,
whereEndoEphemeralState,
whereEndoSock,
whereEndoCache,
} from '@endo/where';
import {
mapLocation,
hashLocation,
loadArchive,
makeArchive,
writeArchive,
} from '@endo/compartment-mapper';
import {
makeReadPowers,
makeWritePowers,
} from '@endo/compartment-mapper/node-powers.js';
import { makeNodeReader } from '@endo/stream-node';
import { E } from '@endo/far';

const readPowers = makeReadPowers({ fs, url, crypto });
Expand All @@ -52,10 +63,43 @@ const info = {
};

const statePath = whereEndoState(process.platform, process.env, info);
const ephemeralStatePath = whereEndoEphemeralState(
process.platform,
process.env,
info,
);
const sockPath = whereEndoSock(process.platform, process.env, info);
const cachePath = whereEndoCache(process.platform, process.env, info);
const logPath = path.join(statePath, 'endo.log');

const delay = async (ms, cancelled) => {
// Do not attempt to set up a timer if already cancelled.
await Promise.race([cancelled, undefined]);
return new Promise((resolve, reject) => {
const handle = setTimeout(resolve, ms);
cancelled.catch(error => {
reject(error);
clearTimeout(handle);
});
});
};

const provideEndoClient = async (...args) => {
try {
// It is okay to fail to connect because the daemon is not running.
return await makeEndoClient(...args);
} catch {
console.error('Starting Endo daemon...');
// It is also okay to fail the race to start.
await start().catch(() => {});
// But not okay to fail to connect after starting.
// We are not going to contemplate reliably in the face of a worker getting
// stopped the moment after it was started.
// That is a bridge too far.
return makeEndoClient(...args);
}
};

export const main = async rawArgs => {
const { promise: cancelled, reject: cancel } = makePromiseKit();
cancelled.catch(() => {});
Expand All @@ -77,6 +121,10 @@ export const main = async rawArgs => {
process.stdout.write(`${statePath}\n`);
});

where.command('run').action(async _cmd => {
process.stdout.write(`${ephemeralStatePath}\n`);
});

where.command('sock').action(async _cmd => {
process.stdout.write(`${sockPath}\n`);
});
Expand Down Expand Up @@ -109,19 +157,51 @@ export const main = async rawArgs => {
await reset();
});

const log = program.command('log').action(async cmd => {
await new Promise((resolve, reject) => {
const args = cmd.opts().follow ? ['-f'] : [];
const child = spawn('tail', [...args, logPath], {
stdio: ['inherit', 'inherit', 'inherit'],
});
child.on('error', reject);
child.on('exit', resolve);
cancelled.catch(() => child.kill());
});
});
program
.command('log')
.option('-f, --follow', 'follow the tail of the log')
.option('-p,--ping <interval>', 'milliseconds between daemon reset checks')
.action(async cmd => {
const follow = cmd.opts().follow;
const ping = cmd.opts().ping;
const logCheckIntervalMs = ping !== undefined ? Number(ping) : 5_000;

do {
// Scope cancellation and propagate.
const { promise: followCancelled, reject: cancelFollower } =
makePromiseKit();
cancelled.catch(cancelFollower);

log.option('-f, --follow', 'follow the tail of the log');
(async () => {
const { getBootstrap } = await makeEndoClient(
'log-follower-probe',
sockPath,
followCancelled,
);
const bootstrap = await getBootstrap();
for (;;) {
await delay(logCheckIntervalMs, followCancelled);
await E(bootstrap).ping();
}
})().catch(cancelFollower);

await new Promise((resolve, reject) => {
const args = follow ? ['-f'] : [];
const child = spawn('tail', [...args, logPath], {
stdio: ['inherit', 'inherit', 'inherit'],
});
child.on('error', reject);
child.on('exit', resolve);
followCancelled.catch(() => {
child.kill();
});
});

if (follow) {
await delay(logCheckIntervalMs, cancelled);
}
} while (follow);
});

program.command('ping').action(async _cmd => {
const { getBootstrap } = await makeEndoClient(
Expand All @@ -130,8 +210,7 @@ export const main = async rawArgs => {
cancelled,
);
const bootstrap = getBootstrap();
const publicFacet = E.get(bootstrap).publicFacet;
await E(publicFacet).ping();
await E(bootstrap).ping();
process.stderr.write('ok\n');
});

Expand All @@ -157,16 +236,175 @@ export const main = async rawArgs => {
});

program
.command('archive <archive-path> <application-path>')
.action(async (_cmd, [archivePath, applicationPath]) => {
const archiveLocation = url.pathToFileURL(archivePath);
.command('archive <application-path>')
.option('-n,--name <name>', 'Store the archive into Endo')
.option('-f,--file <archive-path>', 'Store the archive into a file')
.action(async (applicationPath, cmd) => {
const archiveName = cmd.opts().name;
const archivePath = cmd.opts().file;
const applicationLocation = url.pathToFileURL(applicationPath);
await writeArchive(
write,
readPowers,
archiveLocation,
applicationLocation,
if (archiveName !== undefined) {
const archiveBytes = await makeArchive(readPowers, applicationLocation);
const readerRef = makeReaderRef([archiveBytes]);
const { getBootstrap } = await provideEndoClient(
'cli',
sockPath,
cancelled,
);
try {
const bootstrap = getBootstrap();
await E(bootstrap).store(readerRef, archiveName);
} catch (error) {
console.error(error);
cancel(error);
}
} else if (archivePath !== undefined) {
const archiveLocation = url.pathToFileURL(archivePath);
await writeArchive(
write,
readPowers,
archiveLocation,
applicationLocation,
);
} else {
throw new TypeError('Archive command requires either a name or a path');
}
});

program
.command('store <path>')
.option(
'-n,--name <name>',
'Assigns a pet name to the result for future reference',
)
.action(async (storablePath, cmd) => {
const { name } = cmd.opts();
const nodeReadStream = fs.createReadStream(storablePath);
const reader = makeNodeReader(nodeReadStream);
const readerRef = makeReaderRef(reader);

const { getBootstrap } = await provideEndoClient(
'cli',
sockPath,
cancelled,
);
try {
const bootstrap = getBootstrap();
await E(bootstrap).store(readerRef, name);
} catch (error) {
console.error(error);
cancel(error);
}
});

program.command('spawn <name>').action(async name => {
const { getBootstrap } = await provideEndoClient(
'cli',
sockPath,
cancelled,
);
try {
const bootstrap = getBootstrap();
await E(bootstrap).makeWorker(name);
} catch (error) {
console.error(error);
cancel(error);
}
});

program.command('show <name>').action(async name => {
const { getBootstrap } = await provideEndoClient(
'cli',
sockPath,
cancelled,
);
try {
const bootstrap = getBootstrap();
const pet = await E(bootstrap).provide(name);
console.log(pet);
} catch (error) {
console.error(error);
cancel(error);
}
});

program.command('follow <name>').action(async name => {
const { getBootstrap } = await provideEndoClient(
'cli',
sockPath,
cancelled,
);
try {
const bootstrap = getBootstrap();
const iterable = await E(bootstrap).provide(name);
const iterator = await E(iterable)[Symbol.asyncIterator]();
for await (const iterand of makeRefIterator(iterator)) {
console.log(iterand);
}
} catch (error) {
console.error(error);
cancel(error);
}
});

program.command('cat <name>').action(async name => {
const { getBootstrap } = await provideEndoClient(
'cli',
sockPath,
cancelled,
);
try {
const bootstrap = getBootstrap();
const readable = await E(bootstrap).provide(name);
const readerRef = E(readable).stream();
const reader = makeRefReader(readerRef);
for await (const chunk of reader) {
process.stdout.write(chunk);
}
} catch (error) {
console.error(error);
cancel(error);
}
});

program
.command('eval <worker> <source> [names...]')
.option(
'-n,--name <name>',
'Assigns a name to the result for future reference, persisted between restarts',
)
.action(async (worker, source, names, cmd) => {
const { name: resultPetName } = cmd.opts();
const { getBootstrap } = await provideEndoClient(
'cli',
sockPath,
cancelled,
);
try {
const bootstrap = getBootstrap();
const workerRef = E(bootstrap).provide(worker);

const pairs = names.map(name => {
const pair = name.split(':');
if (pair.length === 1) {
return [name, name];
}
return pair;
});
const codeNames = pairs.map(pair => pair[0]);
const petNames = pairs.map(pair => pair[1]);

const result = await E(workerRef).evaluate(
source,
codeNames,
petNames,
resultPetName,
);
console.log(result);
} catch (error) {
console.error(error);
cancel(error);
}
});

// Throw an error instead of exiting directly.
Expand Down
1 change: 1 addition & 0 deletions packages/daemon/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/tmp
6 changes: 2 additions & 4 deletions packages/daemon/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,5 @@ with the user, and manages per-user storage and compute access.

Over that channel, the daemon communicates in CapTP over netstring message
envelopes.
The bootstrap object has public and private facets.
All access over the public facet are mediate on the private facet.
So, for example, a request for a handle would be posed on the public facet, and
the user would be obliged to answer on the private facet.
The bootstrap provides the user agent API from which one can derive facets for
other agents.
Loading