Skip to content

Commit 9982ac9

Browse files
bjaspanBarry Jaspanpokey
authored
Create Io interface and refactor fs-specific code into NativeIo (#19)
* Define RuntimeAdapter, implement NativeAdapter, convert to using it. * Add comments; cleanup. * rename activate.nativeAdapter * cleanup * remove incorrectly returning empty promise * tweaks * more tweaks --------- Co-authored-by: Barry Jaspan <[email protected]> Co-authored-by: Pokey Rule <[email protected]>
1 parent 43b6986 commit 9982ac9

File tree

7 files changed

+164
-123
lines changed

7 files changed

+164
-123
lines changed

src/commandRunner.ts

Lines changed: 11 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,16 @@
1-
import { open } from "fs/promises";
21
import { Minimatch } from "minimatch";
32
import * as vscode from "vscode";
43

5-
import { readRequest, writeResponse } from "./io";
6-
import { getResponsePath } from "./paths";
74
import { any } from "./regex";
85
import { Request } from "./types";
6+
import { Io } from "./io";
97

108
export default class CommandRunner {
11-
allowRegex!: RegExp;
12-
denyRegex!: RegExp | null;
13-
backgroundWindowProtection!: boolean;
9+
private allowRegex!: RegExp;
10+
private denyRegex!: RegExp | null;
11+
private backgroundWindowProtection!: boolean;
1412

15-
constructor() {
13+
constructor(private io: Io) {
1614
this.reloadConfiguration = this.reloadConfiguration.bind(this);
1715
this.runCommand = this.runCommand.bind(this);
1816

@@ -51,14 +49,14 @@ export default class CommandRunner {
5149
* types.
5250
*/
5351
async runCommand() {
54-
const responseFile = await open(getResponsePath(), "wx");
52+
await this.io.prepareResponse();
5553

5654
let request: Request;
5755

5856
try {
59-
request = await readRequest();
57+
request = await this.io.readRequest();
6058
} catch (err) {
61-
await responseFile.close();
59+
await this.io.closeResponse();
6260
throw err;
6361
}
6462

@@ -94,20 +92,20 @@ export default class CommandRunner {
9492
await commandPromise;
9593
}
9694

97-
await writeResponse(responseFile, {
95+
await this.io.writeResponse({
9896
error: null,
9997
uuid,
10098
returnValue: commandReturnValue,
10199
warnings,
102100
});
103101
} catch (err) {
104-
await writeResponse(responseFile, {
102+
await this.io.writeResponse({
105103
error: (err as Error).message,
106104
uuid,
107105
warnings,
108106
});
109107
}
110108

111-
await responseFile.close();
109+
await this.io.closeResponse();
112110
}
113111
}

src/extension.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
import * as vscode from "vscode";
22

3+
import { NativeIo } from "./nativeIo";
34
import CommandRunner from "./commandRunner";
4-
import { initializeCommunicationDir } from "./initializeCommunicationDir";
5-
import { getInboundSignal } from "./signal";
65
import { FocusedElementType } from "./types";
76

8-
export function activate(context: vscode.ExtensionContext) {
9-
initializeCommunicationDir();
7+
export async function activate(context: vscode.ExtensionContext) {
8+
const io = new NativeIo();
9+
await io.initialize();
1010

11-
const commandRunner = new CommandRunner();
11+
const commandRunner = new CommandRunner(io);
1212
let focusedElementType: FocusedElementType | undefined;
1313

1414
context.subscriptions.push(
@@ -40,7 +40,7 @@ export function activate(context: vscode.ExtensionContext) {
4040
* This signal is emitted by the voice engine to indicate that a phrase has
4141
* just begun execution.
4242
*/
43-
prePhrase: getInboundSignal("prePhrase"),
43+
prePhrase: io.getInboundSignal("prePhrase"),
4444
},
4545
};
4646
}

src/fileUtils.ts

Lines changed: 0 additions & 11 deletions
This file was deleted.

src/initializeCommunicationDir.ts

Lines changed: 0 additions & 29 deletions
This file was deleted.

src/io.ts

Lines changed: 24 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,29 @@
1-
import { FileHandle, readFile, stat } from "fs/promises";
2-
import { VSCODE_COMMAND_TIMEOUT_MS } from "./constants";
3-
import { getRequestPath } from "./paths";
41
import { Request, Response } from "./types";
5-
import { writeJSON } from "./fileUtils";
62

7-
/**
8-
* Reads the JSON-encoded request from the request file, unlinking the file
9-
* after reading.
10-
* @returns A promise that resolves to a Response object
11-
*/
12-
export async function readRequest(): Promise<Request> {
13-
const requestPath = getRequestPath();
14-
15-
const stats = await stat(requestPath);
16-
const request = JSON.parse(await readFile(requestPath, "utf-8"));
17-
18-
if (
19-
Math.abs(stats.mtimeMs - new Date().getTime()) > VSCODE_COMMAND_TIMEOUT_MS
20-
) {
21-
throw new Error(
22-
"Request file is older than timeout; refusing to execute command"
23-
);
24-
}
25-
26-
return request;
3+
export interface SignalReader {
4+
/**
5+
* Gets the current version of the signal. This version string changes every
6+
* time the signal is emitted, and can be used to detect whether signal has
7+
* been emitted between two timepoints.
8+
* @returns The current signal version or null if the signal file could not be
9+
* found
10+
*/
11+
getVersion: () => Promise<string | null>;
2712
}
2813

29-
/**
30-
* Writes the response to the response file as JSON.
31-
* @param file The file to write to
32-
* @param response The response object to JSON-encode and write to disk
33-
*/
34-
export async function writeResponse(file: FileHandle, response: Response) {
35-
await writeJSON(file, response);
14+
export interface Io {
15+
initialize: () => Promise<void>;
16+
// Prepares to send a response to readRequest, preventing any other process
17+
// from doing so until closeResponse is called. Throws an error if called
18+
// twice before closeResponse.
19+
prepareResponse: () => Promise<void>;
20+
// Closes a prepared response, allowing other processes to respond to
21+
// readRequest. Throws an error if the prepareResponse has not been called.
22+
closeResponse: () => Promise<void>;
23+
// Returns a request from Talon command client.
24+
readRequest: () => Promise<Request>;
25+
// Writes a response. Throws an error if prepareResponse has not been called.
26+
writeResponse: (response: Response) => Promise<void>;
27+
// Returns a SignalReader.
28+
getInboundSignal: (name: string) => SignalReader;
3629
}

src/nativeIo.ts

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
import { mkdirSync, lstatSync } from "fs";
2+
import { join } from "path";
3+
import { S_IWOTH } from "constants";
4+
import {
5+
getCommunicationDirPath,
6+
getRequestPath,
7+
getResponsePath,
8+
getSignalDirPath,
9+
} from "./paths";
10+
import { userInfo } from "os";
11+
import { Io } from "./io";
12+
import { FileHandle, open, readFile, stat } from "fs/promises";
13+
import { VSCODE_COMMAND_TIMEOUT_MS } from "./constants";
14+
import { Request, Response } from "./types";
15+
16+
class InboundSignal {
17+
constructor(private path: string) {}
18+
19+
/**
20+
* Gets the current version of the signal. This version string changes every
21+
* time the signal is emitted, and can be used to detect whether signal has
22+
* been emitted between two timepoints.
23+
* @returns The current signal version or null if the signal file could not be
24+
* found
25+
*/
26+
async getVersion() {
27+
try {
28+
return (await stat(this.path)).mtimeMs.toString();
29+
} catch (err) {
30+
if ((err as NodeJS.ErrnoException).code !== "ENOENT") {
31+
throw err;
32+
}
33+
34+
return null;
35+
}
36+
}
37+
}
38+
39+
export class NativeIo implements Io {
40+
private responseFile: FileHandle | null;
41+
42+
constructor() {
43+
this.responseFile = null;
44+
}
45+
46+
async initialize(): Promise<void> {
47+
const communicationDirPath = getCommunicationDirPath();
48+
49+
console.debug(`Creating communication dir ${communicationDirPath}`);
50+
mkdirSync(communicationDirPath, { recursive: true, mode: 0o770 });
51+
52+
const stats = lstatSync(communicationDirPath);
53+
54+
const info = userInfo();
55+
56+
if (
57+
!stats.isDirectory() ||
58+
stats.isSymbolicLink() ||
59+
stats.mode & S_IWOTH ||
60+
// On Windows, uid < 0, so we don't worry about it for simplicity
61+
(info.uid >= 0 && stats.uid !== info.uid)
62+
) {
63+
throw new Error(
64+
`Refusing to proceed because of invalid communication dir ${communicationDirPath}`
65+
);
66+
}
67+
}
68+
69+
async prepareResponse(): Promise<void> {
70+
if (this.responseFile) {
71+
throw new Error("response is already locked");
72+
}
73+
this.responseFile = await open(getResponsePath(), "wx");
74+
}
75+
76+
async closeResponse(): Promise<void> {
77+
if (!this.responseFile) {
78+
throw new Error("response is not locked");
79+
}
80+
await this.responseFile.close();
81+
this.responseFile = null;
82+
}
83+
84+
/**
85+
* Reads the JSON-encoded request from the request file, unlinking the file
86+
* after reading.
87+
* @returns A promise that resolves to a Response object
88+
*/
89+
async readRequest(): Promise<Request> {
90+
const requestPath = getRequestPath();
91+
92+
const stats = await stat(requestPath);
93+
const request = JSON.parse(await readFile(requestPath, "utf-8"));
94+
95+
if (
96+
Math.abs(stats.mtimeMs - new Date().getTime()) > VSCODE_COMMAND_TIMEOUT_MS
97+
) {
98+
throw new Error(
99+
"Request file is older than timeout; refusing to execute command"
100+
);
101+
}
102+
103+
return request;
104+
}
105+
106+
/**
107+
* Writes the response to the response file as JSON.
108+
* @param file The file to write to
109+
* @param response The response object to JSON-encode and write to disk
110+
*/
111+
async writeResponse(response: Response) {
112+
if (!this.responseFile) {
113+
throw new Error("response is not locked");
114+
}
115+
await this.responseFile.write(`${JSON.stringify(response)}\n`);
116+
}
117+
118+
getInboundSignal(name: string) {
119+
const signalDir = getSignalDirPath();
120+
const path = join(signalDir, name);
121+
return new InboundSignal(path);
122+
}
123+
}

src/signal.ts

Lines changed: 0 additions & 33 deletions
This file was deleted.

0 commit comments

Comments
 (0)