Skip to content

Commit b9fc9a9

Browse files
authored
Merge pull request #320 from MatrixAI/feature-undefined-errors
Properly display non-Polykey errors instead of printing `undefined`
2 parents 9ec49bf + 8685257 commit b9fc9a9

14 files changed

+336
-136
lines changed

npmDepsHash

+1-1
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
sha256-/JZlQA4VOEGvT5iFaN2KN4LpKI0RqlFhmFQJlesKRQU=
1+
sha256-WkSKOsHFtWeCLyYr5WICTXG+A4+P6/hdxvlutNudQTg=

package-lock.json

+8-8
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,7 @@
153153
"nexpect": "^0.6.0",
154154
"node-gyp-build": "^4.4.0",
155155
"nodemon": "^3.0.1",
156-
"polykey": "^1.15.1",
156+
"polykey": "^1.16.0",
157157
"prettier": "^3.0.0",
158158
"shelljs": "^0.8.5",
159159
"shx": "^0.3.4",

src/errors.ts

+16
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,15 @@ class ErrorPolykeyCLIAsynchronousDeadlock<T> extends ErrorPolykeyCLI<T> {
7575
exitCode = sysexits.SOFTWARE;
7676
}
7777

78+
/**
79+
* Unexpected error is a runtime error.
80+
* If these exceptions occur, there is a bug. Most likely in Polykey.
81+
*/
82+
class ErrorPolykeyCLIUnexpectedError<T> extends ErrorPolykeyCLI<T> {
83+
static description = 'An unexpected error occured';
84+
exitCode = sysexits.SOFTWARE;
85+
}
86+
7887
class ErrorPolykeyCLINodePath<T> extends ErrorPolykeyCLI<T> {
7988
static description = 'Cannot derive default node path from unknown platform';
8089
exitCode = sysexits.USAGE;
@@ -172,10 +181,16 @@ class ErrorPolykeyCLICatSecret<T> extends ErrorPolykeyCLI<T> {
172181
exitCode = 1;
173182
}
174183

184+
class ErrorPolykeyCLIEditSecret<T> extends ErrorPolykeyCLI<T> {
185+
static description = 'Failed to edit a secret';
186+
exitCode = 1;
187+
}
188+
175189
export {
176190
ErrorPolykeyCLI,
177191
ErrorPolykeyCLIUncaughtException,
178192
ErrorPolykeyCLIUnhandledRejection,
193+
ErrorPolykeyCLIUnexpectedError,
179194
ErrorPolykeyCLIAsynchronousDeadlock,
180195
ErrorPolykeyCLINodePath,
181196
ErrorPolykeyCLIClientOptions,
@@ -196,4 +211,5 @@ export {
196211
ErrorPolykeyCLIRenameSecret,
197212
ErrorPolykeyCLIRemoveSecret,
198213
ErrorPolykeyCLICatSecret,
214+
ErrorPolykeyCLIEditSecret,
199215
};

src/polykey.ts

+10-2
Original file line numberDiff line numberDiff line change
@@ -75,10 +75,14 @@ async function polykeyAgentMain(): Promise<number> {
7575
process.exitCode = e.exitCode;
7676
} else {
7777
// Unknown error, this should not happen
78+
const wrappedError = new binErrors.ErrorPolykeyCLIUnexpectedError(
79+
binUtils.composeErrorMessage(e),
80+
{ cause: e },
81+
);
7882
process.stderr.write(
7983
binUtils.outputFormatter({
8084
type: errFormat,
81-
data: e,
85+
data: wrappedError,
8286
}),
8387
);
8488
process.exitCode = 255;
@@ -217,10 +221,14 @@ async function polykeyMain(argv: Array<string>): Promise<number> {
217221
process.exitCode = e.exitCode;
218222
} else {
219223
// Unknown error, this should not happen
224+
const wrappedError = new binErrors.ErrorPolykeyCLIUnexpectedError(
225+
binUtils.composeErrorMessage(e),
226+
{ cause: e },
227+
);
220228
process.stderr.write(
221229
binUtils.outputFormatter({
222230
type: errFormat,
223-
data: e,
231+
data: wrappedError,
224232
}),
225233
);
226234
process.exitCode = 255;

src/secrets/CommandCat.ts

+26-8
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import type PolykeyClient from 'polykey/dist/PolykeyClient';
2+
import type { ContentOrErrorMessage } from 'polykey/dist/client/types';
3+
import type { ReadableStream } from 'stream/web';
24
import CommandPolykey from '../CommandPolykey';
35
import * as binUtils from '../utils';
46
import * as binOptions from '../utils/options';
@@ -80,28 +82,44 @@ class CommandGet extends CommandPolykey {
8082
}
8183
const hasErrored = await binUtils.retryAuthentication(async (auth) => {
8284
// Write secret paths to input stream
83-
const response = await pkClient.rpcClient.methods.vaultsSecretsGet();
85+
const response = await pkClient.rpcClient.methods.vaultsSecretsCat();
8486
const writer = response.writable.getWriter();
8587
let first = true;
8688
for (const [vaultName, secretPath] of secretPaths) {
8789
await writer.write({
8890
nameOrId: vaultName,
8991
secretName: secretPath ?? '/',
90-
metadata: first
91-
? { ...auth, options: { continueOnError: true } }
92-
: undefined,
92+
metadata: first ? auth : undefined,
9393
});
9494
first = false;
9595
}
9696
await writer.close();
9797
// Print out incoming data to standard out
9898
let hasErrored = false;
99-
for await (const chunk of response.readable) {
100-
if (chunk.error) {
99+
// TypeScript cannot properly perform type narrowing on this type, so
100+
// the `as` keyword is used to help it out.
101+
for await (const result of response.readable as ReadableStream<ContentOrErrorMessage>) {
102+
if (result.type === 'error') {
101103
hasErrored = true;
102-
process.stderr.write(chunk.error);
104+
switch (result.code) {
105+
case 'ENOENT':
106+
// Attempt to cat a non-existent file
107+
process.stderr.write(
108+
`cat: ${result.reason}: No such file or directory\n`,
109+
);
110+
break;
111+
case 'EISDIR':
112+
// Attempt to cat a directory
113+
process.stderr.write(
114+
`cat: ${result.reason}: Is a directory\n`,
115+
);
116+
break;
117+
default:
118+
// No other code should be thrown
119+
throw result;
120+
}
103121
} else {
104-
process.stdout.write(chunk.secretContent);
122+
process.stdout.write(result.secretContent);
105123
}
106124
}
107125
return hasErrored;

src/secrets/CommandEdit.ts

+40-7
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ class CommandEdit extends CommandPolykey {
2424
this.addOption(binOptions.clientPort);
2525
this.action(async (secretPath, options) => {
2626
const os = await import('os');
27-
const { execSync } = await import('child_process');
27+
const { spawn } = await import('child_process');
2828
const vaultsErrors = await import('polykey/dist/vaults/errors');
2929
const { default: PolykeyClient } = await import(
3030
'polykey/dist/PolykeyClient'
@@ -64,17 +64,14 @@ class CommandEdit extends CommandPolykey {
6464
const secretExists = await binUtils.retryAuthentication(
6565
async (auth) => {
6666
let exists = true;
67-
const res = await pkClient.rpcClient.methods.vaultsSecretsGet();
68-
const writer = res.writable.getWriter();
69-
await writer.write({
67+
const response = await pkClient.rpcClient.methods.vaultsSecretsGet({
7068
nameOrId: secretPath[0],
7169
secretName: secretPath[1] ?? '/',
7270
metadata: auth,
7371
});
74-
await writer.close();
7572
try {
7673
let rawSecretContent: string = '';
77-
for await (const chunk of res.readable) {
74+
for await (const chunk of response) {
7875
rawSecretContent += chunk.secretContent;
7976
}
8077
const secretContent = Buffer.from(rawSecretContent, 'binary');
@@ -83,6 +80,20 @@ class CommandEdit extends CommandPolykey {
8380
const [cause, _] = binUtils.remoteErrorCause(e);
8481
if (cause instanceof vaultsErrors.ErrorSecretsSecretUndefined) {
8582
exists = false;
83+
} else if (
84+
cause instanceof vaultsErrors.ErrorSecretsIsDirectory
85+
) {
86+
// First, write the inline error to standard error like other
87+
// secrets commands do.
88+
process.stderr.write(
89+
`edit: ${secretPath[1] ?? '/'}: No such file or directory\n`,
90+
);
91+
// Then, throw an error to get the non-zero exit code. As this
92+
// command is Polykey-specific, the code doesn't really matter
93+
// that much.
94+
throw new errors.ErrorPolykeyCLIEditSecret(
95+
'Failed to edit secret',
96+
);
8697
} else {
8798
throw e;
8899
}
@@ -94,7 +105,29 @@ class CommandEdit extends CommandPolykey {
94105
// If the editor exited with a code other than zero, then execSync
95106
// will throw an error. So, in the case of saving the file but the
96107
// editor crashing, the program won't save the updated secret.
97-
execSync(`${process.env.EDITOR} \"${tmpFile}\"`, { stdio: 'inherit' });
108+
await new Promise<void>((resolve, reject) => {
109+
// If $EDITOR is unset, default to nano. If that doesn't exist, then
110+
// the command will raise an error.
111+
const editorProc = spawn(process.env.EDITOR ?? 'nano', [tmpFile], {
112+
stdio: 'inherit',
113+
});
114+
editorProc.on('error', (e) => {
115+
const error = new errors.ErrorPolykeyCLIEditSecret(
116+
`Failed to run command ${process.env.EDITOR}`,
117+
{ cause: e },
118+
);
119+
reject(error);
120+
});
121+
editorProc.on('close', (code) => {
122+
if (code !== 0) {
123+
const error = new errors.ErrorPolykeyCLIEditSecret(
124+
`Editor exited with code ${code}`,
125+
);
126+
reject(error);
127+
}
128+
resolve();
129+
});
130+
});
98131
let content: string;
99132
try {
100133
const buffer = await this.fs.promises.readFile(tmpFile);

src/secrets/CommandMkdir.ts

+17-20
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,12 @@
11
import type PolykeyClient from 'polykey/dist/PolykeyClient';
2-
import type { ErrorMessage } from 'polykey/dist/client/types';
2+
import type { SuccessOrErrorMessage } from 'polykey/dist/client/types';
3+
import type { ReadableStream } from 'stream/web';
34
import CommandPolykey from '../CommandPolykey';
45
import * as binUtils from '../utils';
56
import * as binOptions from '../utils/options';
67
import * as binParsers from '../utils/parsers';
78
import * as binProcessors from '../utils/processors';
8-
import {
9-
ErrorPolykeyCLIMakeDirectory,
10-
ErrorPolykeyCLIUncaughtException,
11-
} from '../errors';
9+
import { ErrorPolykeyCLIMakeDirectory } from '../errors';
1210

1311
class CommandMkdir extends CommandPolykey {
1412
constructor(...args: ConstructorParameters<typeof CommandPolykey>) {
@@ -79,29 +77,28 @@ class CommandMkdir extends CommandPolykey {
7977
// Print out incoming data to standard out, or incoming errors to
8078
// standard error.
8179
let hasErrored = false;
82-
for await (const result of response.readable) {
80+
// TypeScript cannot properly perform type narrowing on this type, so
81+
// the `as` keyword is used to help it out.
82+
for await (const result of response.readable as ReadableStream<SuccessOrErrorMessage>) {
8383
if (result.type === 'error') {
84-
// TS cannot properly evaluate a type this deeply nested, so we use
85-
// the as keyword to help it. Inside this block, the type of data
86-
// is ensured to be 'error'.
87-
const error = result as ErrorMessage;
8884
hasErrored = true;
89-
let message: string = '';
90-
switch (error.code) {
85+
switch (result.code) {
9186
case 'ENOENT':
92-
message = 'No such secret or directory';
87+
// Attempt to create a directory without existing parents
88+
process.stderr.write(
89+
`mkdir: cannot create directory ${result.reason}: No such file or directory\n`,
90+
);
9391
break;
9492
case 'EEXIST':
95-
message = 'Secret or directory exists';
93+
// Attempt to create a directory where something already exists
94+
process.stderr.write(
95+
`mkdir: cannot create directory ${result.reason}: File or directory exists\n`,
96+
);
9697
break;
9798
default:
98-
throw new ErrorPolykeyCLIUncaughtException(
99-
`Unexpected error code: ${error.code}`,
100-
);
99+
// No other code should be thrown
100+
throw result;
101101
}
102-
process.stderr.write(
103-
`${error.code}: cannot create directory ${error.reason}: ${message}\n`,
104-
);
105102
}
106103
}
107104
return hasErrored;

0 commit comments

Comments
 (0)