Skip to content

Commit 9c5596e

Browse files
authored
Improved server robustness (#4)
* Improved server robustness * Stop unlinking request / response json * Add command-server.backgroundWindowProtection * readme
1 parent fb4977f commit 9c5596e

File tree

10 files changed

+89
-75
lines changed

10 files changed

+89
-75
lines changed

.eslintrc.json

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,14 @@
1212
"@typescript-eslint/naming-convention": "warn",
1313
"@typescript-eslint/semi": "warn",
1414
"curly": "warn",
15-
"eqeqeq": "warn",
15+
"eqeqeq": [
16+
"warn",
17+
"always",
18+
{
19+
"null": "never"
20+
}
21+
],
1622
"no-throw-literal": "warn",
1723
"semi": "off"
1824
}
19-
}
25+
}

.vscode/settings.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,8 @@
77
"out": true // set this to false to include "out" folder in search results
88
},
99
// Turn off tsc task auto detection since we have the necessary tasks as npm scripts
10-
"typescript.tsc.autoDetect": "off"
10+
"typescript.tsc.autoDetect": "off",
11+
"cSpell.words": [
12+
"eqeqeq"
13+
]
1114
}

README.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,17 @@ Contributes the following commands:
5555
## Configuration
5656
Contributes the following settings:
5757

58+
### `command-server.backgroundWindowProtection`
59+
Turn this off if you're frequently seeing an error saying "This editor is not active".
60+
61+
```json
62+
{
63+
"command-server.backgroundWindowProtection": false
64+
}
65+
```
66+
67+
Defaults to `true` (protection enabled).
68+
5869
### `command-server.allowList`
5970
Allows user to specify the allowed commands using glob syntax, eg:
6071

@@ -82,12 +93,22 @@ Defaults to `[]` (doesn't deny anything).
8293

8394
## Known issues
8495

96+
- If you see errors saying "This editor is not active", disable
97+
`command-server.backgroundWindowProtection`, as described above. VSCode
98+
seems to be a bit inconsistent with determining which window has
99+
focus. There is code in the command server that tries to prevent a
100+
background window from inadvertently executing a command, but when the
101+
focused window detection fails, it will end up preventing correct commands
102+
from running.
85103
- The server won't respond until the extension is loaded. This may be obvious,
86104
but just be aware that if you have other extensions that take a while to
87105
load, the server might not respond for a little while after you load an
88106
editor window until everything is fully loaded.
89107

90108
## Release Notes
91109

110+
### 0.5.0
111+
- Improve robustness, and add `command-server.backgroundWindowProtection` setting
112+
92113
### 0.4.0
93114
- Switch to file-based RPC

package.json

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
"type": "git",
99
"url": "https://github.com/pokey/command-server"
1010
},
11-
"version": "0.4.0",
11+
"version": "0.5.0",
1212
"engines": {
1313
"vscode": "^1.53.0"
1414
},
@@ -50,6 +50,11 @@
5050
"type": "array",
5151
"default": [],
5252
"description": "Commands to deny. Supports simple glob syntax"
53+
},
54+
"command-server.backgroundWindowProtection": {
55+
"type": "boolean",
56+
"default": true,
57+
"description": "Whether to enable protection against background windows executing a command"
5358
}
5459
}
5560
}

src/commandRunner.ts

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,16 @@
1+
import { open } from "fs/promises";
12
import { Minimatch } from "minimatch";
23
import * as vscode from "vscode";
34

45
import { readRequest, writeResponse } from "./io";
6+
import { getResponsePath } from "./paths";
57
import { any } from "./regex";
8+
import { Request, Response } from "./types";
69

710
export default class CommandRunner {
811
allowRegex!: RegExp;
912
denyRegex!: RegExp | null;
13+
backgroundWindowProtection!: boolean;
1014

1115
constructor() {
1216
this.reloadConfiguration = this.reloadConfiguration.bind(this);
@@ -33,6 +37,10 @@ export default class CommandRunner {
3337
denyList.length === 0
3438
? null
3539
: any(...denyList.map((glob) => new Minimatch(glob).makeRe()));
40+
41+
this.backgroundWindowProtection = vscode.workspace
42+
.getConfiguration("command-server")
43+
.get<boolean>("backgroundWindowProtection")!;
3644
}
3745

3846
/**
@@ -43,12 +51,29 @@ export default class CommandRunner {
4351
* types.
4452
*/
4553
async runCommand() {
54+
const responseFile = await open(getResponsePath(), "wx");
55+
56+
var request: Request;
57+
58+
try {
59+
request = await readRequest();
60+
} catch (err) {
61+
await responseFile.close();
62+
throw err;
63+
}
64+
4665
const { commandId, args, uuid, returnCommandOutput, waitForFinish } =
47-
await readRequest();
66+
request;
67+
68+
const warnings = [];
4869

4970
try {
5071
if (!vscode.window.state.focused) {
51-
throw new Error("This editor is not active");
72+
if (this.backgroundWindowProtection) {
73+
throw new Error("This editor is not active");
74+
} else {
75+
warnings.push("This editor is not active");
76+
}
5277
}
5378

5479
if (!commandId.match(this.allowRegex)) {
@@ -69,16 +94,20 @@ export default class CommandRunner {
6994
await commandPromise;
7095
}
7196

72-
await writeResponse({
97+
await writeResponse(responseFile, {
7398
error: null,
7499
uuid,
75100
returnValue: commandReturnValue,
101+
warnings,
76102
});
77103
} catch (err) {
78-
await writeResponse({
104+
await writeResponse(responseFile, {
79105
error: err.message,
80106
uuid,
107+
warnings,
81108
});
82109
}
110+
111+
await responseFile.close();
83112
}
84113
}

src/fileUtils.ts

Lines changed: 4 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,11 @@
1-
import { unlinkSync } from "fs";
2-
import { writeFile } from "fs/promises";
1+
import { FileHandle } from "fs/promises";
32

43
/**
5-
* Opens a file exclusively, failing if it exists, and writes stringified JSON.
4+
* Writes stringified JSON.
65
* Appends newline so that other side knows when it is done
76
* @param path Output path
87
* @param body Body to stringify and write
98
*/
10-
export async function writeJSONExclusive(path: string, body: any) {
11-
await writeFile(path, `${JSON.stringify(body)}\n`, { flag: "wx" });
12-
}
13-
14-
/**
15-
* Unlink the given file if it exists, otherwise do nothing
16-
* @param path The path to unlink
17-
*/
18-
export function unlinkIfExistsSync(path: string) {
19-
try {
20-
unlinkSync(path);
21-
} catch (err) {
22-
if (err.code !== "ENOENT") {
23-
throw err;
24-
}
25-
}
9+
export async function writeJSON(file: FileHandle, body: any) {
10+
await file.write(`${JSON.stringify(body)}\n`);
2611
}

src/initializeCommunicationDir.ts

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,8 @@
11
import { mkdirSync, lstatSync } from "fs";
22
import { S_IWOTH } from "constants";
33
import {
4-
getRequestPath,
5-
getResponsePath,
64
getCommunicationDirPath,
75
} from "./paths";
8-
import { unlinkIfExistsSync } from "./fileUtils";
96
import { userInfo } from "os";
107

118
export function initializeCommunicationDir() {
@@ -29,7 +26,4 @@ export function initializeCommunicationDir() {
2926
`Refusing to proceed because of invalid communication dir ${communicationDirPath}`
3027
);
3128
}
32-
33-
unlinkIfExistsSync(getRequestPath());
34-
unlinkIfExistsSync(getResponsePath());
3529
}

src/io.ts

Lines changed: 6 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
import { readFile, stat, unlink } from "fs/promises";
1+
import { FileHandle, readFile, stat, unlink } from "fs/promises";
22
import { STALE_TIMEOUT_MS, VSCODE_COMMAND_TIMEOUT_MS } from "./constants";
33
import { getRequestPath, getResponsePath } from "./paths";
44
import { Request, Response } from "./types";
5-
import { writeJSONExclusive } from "./fileUtils";
5+
import { writeJSON } from "./fileUtils";
66

77
/**
88
* Reads the JSON-encoded request from the request file, unlinking the file
@@ -15,8 +15,6 @@ export async function readRequest(): Promise<Request> {
1515
const stats = await stat(requestPath);
1616
const request = JSON.parse(await readFile(requestPath, "utf-8"));
1717

18-
await unlink(requestPath);
19-
2018
if (
2119
Math.abs(stats.mtimeMs - new Date().getTime()) > VSCODE_COMMAND_TIMEOUT_MS
2220
) {
@@ -29,38 +27,10 @@ export async function readRequest(): Promise<Request> {
2927
}
3028

3129
/**
32-
* Writes the response to the response file as JSON. If a stale response file
33-
* exists, it will be removed. If a non-stale response file exists, assumes
34-
* there is another VSCode instance mysteriously trying to handle the request
35-
* as well and fails.
30+
* Writes the response to the response file as JSON.
31+
* @param file The file to write to
3632
* @param response The response object to JSON-encode and write to disk
3733
*/
38-
export async function writeResponse(response: Response) {
39-
const responsePath = getResponsePath();
40-
41-
try {
42-
await writeJSONExclusive(responsePath, response);
43-
} catch (err) {
44-
if (err.code !== "EEXIST") {
45-
throw err;
46-
}
47-
48-
try {
49-
const stats = await stat(responsePath);
50-
51-
if (Math.abs(stats.mtimeMs - new Date().getTime()) < STALE_TIMEOUT_MS) {
52-
throw new Error("Another process has an active response file");
53-
}
54-
55-
console.log("Removing stale response file");
56-
await unlink(responsePath);
57-
} catch (err) {
58-
// If the file was removed for whatever reason in the interim we just continue
59-
if (err.code !== "ENOENT") {
60-
throw err;
61-
}
62-
}
63-
64-
await writeJSONExclusive(responsePath, response);
65-
}
34+
export async function writeResponse(file: FileHandle, response: Response) {
35+
await writeJSON(file, response);
6636
}

src/paths.ts

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,9 @@ export function getCommunicationDirPath() {
1212
}
1313

1414
export function getRequestPath() {
15-
const communicationDirPath = getCommunicationDirPath();
16-
17-
return join(communicationDirPath, "request.json");
15+
return join(getCommunicationDirPath(), "request.json");
1816
}
1917

2018
export function getResponsePath() {
21-
const communicationDirPath = getCommunicationDirPath();
22-
23-
return join(communicationDirPath, "response.json");
19+
return join(getCommunicationDirPath(), "response.json");
2420
}

src/types.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,4 +45,9 @@ export interface Response {
4545
* Any error encountered or null if successful
4646
*/
4747
error: string | null;
48+
49+
/**
50+
* A list of warnings issued when running the command
51+
*/
52+
warnings: string[];
4853
}

0 commit comments

Comments
 (0)