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

refactor(agoric-cli): clarify flow of inputs to outputs in start.js (WIP) #6698

Draft
wants to merge 13 commits into
base: master
Choose a base branch
from
Draft
93 changes: 68 additions & 25 deletions packages/access-token/src/access-token.js
Original file line number Diff line number Diff line change
@@ -1,17 +1,25 @@
// @ts-check
import fs from 'fs';
import crypto from 'crypto';
import os from 'os';
import path from 'path';

import { openJSONStore } from './json-store.js';

const { freeze: harden } = Object; // IOU

// Adapted from https://stackoverflow.com/a/43866992/14073862
export function generateAccessToken({
stringBase = 'base64url',
byteLength = 48,
} = {}) {
export function generateAccessToken(
{
stringBase = /** @type {const} */ ('base64url'),
byteLength = 48,
randomBytes,
} = {
randomBytes: crypto.randomBytes,
},
) {
return new Promise((resolve, reject) =>
crypto.randomBytes(byteLength, (err, buffer) => {
randomBytes(byteLength, (err, buffer) => {
if (err) {
reject(err);
} else if (stringBase === 'base64url') {
Expand All @@ -26,26 +34,61 @@ export function generateAccessToken({
);
}

export async function getAccessToken(port) {
if (typeof port === 'string') {
const match = port.match(/^(.*:)?(\d+)$/);
if (match) {
port = match[2];
}
}
/**
* @param {string=} sharedStateDir
* @param {object} io
* @param {typeof fs} io.fs
*/
export const makeMyAgoricDir = (
sharedStateDir = path.join(os.homedir(), '.agoric'),
{ fs: fsSync } = { fs },
) => {
const {
promises: { mkdir },
} = fsSync;
return harden({
/**
* @param {string | number} port
* @param {object} io
* @param {typeof import('crypto').randomBytes} io.randomBytes
*/
getAccessToken: async (
port,
{ randomBytes } = { randomBytes: crypto.randomBytes },
) => {
if (typeof port === 'string') {
const match = port.match(/^(.*:)?(\d+)$/);
if (match) {
port = match[2];
}
}

// Ensure we're protected with a unique accessToken for this basedir.
const sharedStateDir = path.join(os.homedir(), '.agoric');
await fs.promises.mkdir(sharedStateDir, { mode: 0o700, recursive: true });
// Ensure we're protected with a unique accessToken for this basedir.
await mkdir(sharedStateDir, { mode: 0o700, recursive: true });

// Ensure an access token exists.
const { storage, commit, close } = openJSONStore(sharedStateDir);
const accessTokenKey = `accessToken/${port}`;
if (!storage.has(accessTokenKey)) {
storage.set(accessTokenKey, await generateAccessToken());
await commit();
}
const accessToken = storage.get(accessTokenKey);
await close();
return accessToken;
// TODO: pass fsSync IO access explicitly to makeJSONStore
// Ensure an access token exists.
const { storage, commit, close } = openJSONStore(sharedStateDir);
const accessTokenKey = `accessToken/${port}`;
const stored = storage.has(accessTokenKey);
const accessToken = await (stored
? storage.get(accessTokenKey)
: generateAccessToken({ randomBytes }));
await (stored ||
(async () => {
storage.set(accessTokenKey, accessToken);
await commit();
})());
await close();
return accessToken;
},
});
};

/**
* @param {number|string} port
* @deprecated use makeMyAgoricDir for OCap Discipline
*/
export async function getAccessToken(port) {
return makeMyAgoricDir().getAccessToken(port);
}
2 changes: 2 additions & 0 deletions packages/agoric-cli/src/entrypoint.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ main(progname, rawArgs, {
stdout,
makeWebSocket,
fs,
fsSync: rawFs,
path,
now: Date.now,
os,
process,
Expand Down
75 changes: 75 additions & 0 deletions packages/agoric-cli/src/follow.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,91 @@
import process from 'process';
import { Far, getInterfaceOf } from '@endo/marshal';
import { decodeToJustin } from '@endo/marshal/src/marshal-justin.js';
import { assert, details as X } from '@agoric/assert';

import {
DEFAULT_KEEP_POLLING_SECONDS,
DEFAULT_JITTER_SECONDS,
iterateLatest,
makeCastingSpec,
iterateEach,
makeFollower,
makeLeader,
} from '@agoric/casting';
import { Command } from 'commander';
import { makeLeaderOptions } from './lib/casting.js';

export const createFollowCommand = () =>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1 to using Command. though with it, this file should go in src/commands. You might want do the Commander refactorings in a separate PR.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hm... using src/commands seems to be the exception, not the rule. main.js looks like:

import cosmosMain from './cosmos.js';
import deployMain from './deploy.js';
import publishMain from './main-publish.js';
import initMain, { makeInitCommand } from './init.js';
import installMain from './install.js';
import setDefaultsMain, { makeSetDefaultsCommand } from './set-defaults.js';
import startMain from './start.js';
import followMain, { createFollowCommand } from './follow.js';
import { makeOpenCommand, makeTUI } from './open.js';
import { makeWalletCommand } from './commands/wallet.js';

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Depends on how you look at it. What I am requesting is that files that import the Command class (and export an instance of it) go in src/commands. There are 4 examples of that in master. The one exception in src is src/main.js which is making the program. That's cool.

These others are not command instances:

import cosmosMain from './cosmos.js';
import deployMain from './deploy.js';
import publishMain from './main-publish.js';
import installMain from './install.js';
import startMain from './start.js';

And these in your snippet actually are Commands, but ones you're making in this PR:

// makeInitCommand
// makeSetDefaultsCommand
// createFollowCommand
// makeOpenCommand

So I'm requesting that they go with the other Command objects in src/commands.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't plan to export an instance of Command; I plan to export a maker function that takes the necessary authority as args and creates a Command.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry for my impression: exporting a maker function that returns an instance of Command. That's what createFollowCommand is, right? That's also what these are:

export const makePsmCommand = async logger => {

export const makePerfCommand = async logger => {

export const makeOracleCommand = async logger => {

export const makeWalletCommand = async () => {

When you wrote "seems to be the exception, not the rule" I took it that you were appealing to precedent. The for functions like you describe (returning a Command instance) is that all four of them are in src/commands. It seems important to you that you don't put those files in src/commands. I don't understand why but I'll wait for R4R before engaging on the topic again in this PR.

new Command('follow')
.description('follow an Agoric Casting leader')
.arguments('<path-spec...>')
.option(
'--proof <strict | optimistic | none>',
'set proof mode',
value => {
assert(
['strict', 'optimistic', 'none'].includes(value),
X`--proof must be one of 'strict', 'optimistic', or 'none'`,
TypeError,
);
return value;
},
'optimistic',
)
.option(
'--sleep <seconds>',
'sleep <seconds> between polling (may be fractional)',
value => {
const num = Number(value);
assert.equal(`${num}`, value, X`--sleep must be a number`, TypeError);
return num;
},
DEFAULT_KEEP_POLLING_SECONDS,
)
.option(
'--jitter <max-seconds>',
'jitter up to <max-seconds> (may be fractional)',
value => {
const num = Number(value);
assert.equal(`${num}`, value, X`--jitter must be a number`, TypeError);
return num;
},
DEFAULT_JITTER_SECONDS,
)
.option(
'-o, --output <format>',
'value output format',
value => {
assert(
[
'hex',
'justin',
'justinlines',
'json',
'jsonlines',
'text',
].includes(value),
X`--output must be one of 'hex', 'justin', 'justinlines', 'json', 'jsonlines', or 'text'`,
TypeError,
);
return value;
},
'justin',
)
.option(
'-l, --lossy',
'show only the most recent value for each sample interval',
)
.option(
'-b, --block-height',
'show first block height when each value was stored',
)
.option(
'-c, --current-block-height',
'show current block height when each value is reported',
)
.option('-B, --bootstrap <config>', 'network bootstrap configuration');

export default async function followerMain(progname, rawArgs, powers, opts) {
const { anylogger } = powers;
const console = anylogger('agoric:follower');
Expand Down
49 changes: 49 additions & 0 deletions packages/agoric-cli/src/helpers.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
/* global process */
// @ts-check

const { freeze } = Object;

/** @typedef {import('child_process').ChildProcess} ChildProcess */

export const getSDKBinaries = ({
Expand Down Expand Up @@ -87,3 +89,50 @@ export const makePspawn = ({
});
return Object.assign(pr, { childProcess: cp });
};

/**
* @param {string} fileName
* @param {object} io
* @param {typeof import('fs')} io.fs
* @param {typeof import('path')} io.path
*
* @typedef {ReturnType<typeof makeFileReader>} FileReader
*/
export const makeFileReader = (fileName, { fs, path }) => {
/** @param {string} there */
const make = there => makeFileReader(there, { fs, path });
const self = harden({
toString: () => fileName,
readText: () => fs.promises.readFile(fileName, 'utf-8'),
/** @param {string} ref */
neighbor: ref => make(path.resolve(fileName, ref)),
stat: () => fs.promises.stat(fileName),
absolute: () => path.normalize(fileName),
/** @param {string} there */
relative: there => path.relative(fileName, there),
existsSync: () => fs.existsSync(fileName),
readOnly: () => self,
});
return self;
};

/**
* @param {string} fileName
* @param {object} io
* @param {typeof import('fs')} io.fs
* @param {typeof import('path')} io.path
*
* @typedef {ReturnType<typeof makeFileWriter>} FileWriter
*/
export const makeFileWriter = (fileName, { fs, path }) => {
const make = there => makeFileWriter(there, { fs, path });
return harden({
toString: () => fileName,
/** @param {string} txt */
writeText: txt => fs.promises.writeFile(fileName, txt),
readOnly: () => makeFileReader(fileName, { fs, path }),
/** @param {string} ref */
neighbor: ref => make(path.resolve(fileName, ref)),
mkdir: opts => fs.promises.mkdir(fileName, opts),
});
};
27 changes: 27 additions & 0 deletions packages/agoric-cli/src/init.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,13 @@ import { makePspawn } from './helpers.js';
import '@endo/captp/src/types.js';
import '@agoric/swingset-vat/exported.js';
import '@agoric/swingset-vat/src/vats/network/types.js';
import commander from 'commander';

const { Command } = commander;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ditto, src/commands


const DEFAULT_DAPP_TEMPLATE = 'dapp-fungible-faucet';
const DEFAULT_DAPP_URL_BASE = 'https://github.com/Agoric/';
const DEFAULT_DAPP_BRANCH = undefined;

// Use either an absolute template URL, or find it relative to DAPP_URL_BASE.
const gitURL = (relativeOrAbsoluteURL, base) => {
Expand All @@ -17,6 +24,26 @@ const gitURL = (relativeOrAbsoluteURL, base) => {
return url.href;
};

export const makeInitCommand = () =>
new Command('init')
.description('create a new Dapp directory named <project>')
.arguments('<project>')
.option(
'--dapp-template <name>',
'use the template specified by <name>',
DEFAULT_DAPP_TEMPLATE,
)
.option(
'--dapp-base <base-url>',
'find the template relative to <base-url>',
DEFAULT_DAPP_URL_BASE,
)
.option(
'--dapp-branch <branch>',
'use this branch instead of the repository HEAD',
DEFAULT_DAPP_BRANCH,
);

export default async function initMain(_progname, rawArgs, priv, opts) {
const { anylogger, spawn, fs } = priv;
const log = anylogger('agoric:init');
Expand Down
Loading