Skip to content

Commit f6caf13

Browse files
authored
Merge pull request #7340 from mook-as/path-management/diagnostics
Pick Diagnostics: path management onto release-1.15
2 parents 78743f9 + c6c3467 commit f6caf13

10 files changed

Lines changed: 250 additions & 50 deletions

File tree

pkg/rancher-desktop/assets/specs/command-api.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ paths:
101101
summary: >-
102102
Return a list of the check IDs for the Diagnostics category,
103103
or 404 if there is no such `category`.
104-
Specifying an exiting category with no checks
104+
Specifying an existing category with no checks
105105
will return status code 200 and an empty array.
106106
parameters:
107107
- in: query

pkg/rancher-desktop/integrations/manageLinesInFile.ts

Lines changed: 55 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,60 @@ export const START_LINE = '### MANAGED BY RANCHER DESKTOP START (DO NOT EDIT)';
66
export const END_LINE = '### MANAGED BY RANCHER DESKTOP END (DO NOT EDIT)';
77
const DEFAULT_FILE_MODE = 0o644;
88

9+
/**
10+
* `newErrorWithPath` returns a dynamically constructed subclass of `Error` that
11+
* has a constructor that constructs a message using the `messageTemplate`
12+
* function, and also sets any inputs to the function as properties on the
13+
* resulting object.
14+
* @param messageTemplate A function used to generate an error message, based on
15+
* any arguments passed in as properties of an object.
16+
* @returns A subclass of Error.
17+
*/
18+
function newErrorWithPath<T extends Record<string, any>>(messageTemplate: (input: T) => string) {
19+
const result = class extends Error {
20+
constructor(input: T, options?: ErrorOptions) {
21+
super(messageTemplate(input), options);
22+
Object.assign(this, input);
23+
}
24+
};
25+
26+
return result as unknown as new(...args: ConstructorParameters<typeof result>) => (InstanceType<typeof result> & T);
27+
}
28+
29+
/**
30+
* `ErrorDeterminingExtendedAttributes` signifies that we failed to determine if
31+
* the given path contains extended attributes; to be safe, we are not managing
32+
* this file.
33+
*/
34+
export const ErrorDeterminingExtendedAttributes =
35+
newErrorWithPath(({ path }: {path: string}) => `Failed to determine if \`${ path }\` contains extended attributes`);
36+
/**
37+
* `ErrorHasExtendedAttributes` signifies that we were unable to process a file
38+
* because it has extended attributes that would not have been preserved had we
39+
* tried to edit it.
40+
*/
41+
export const ErrorHasExtendedAttributes =
42+
newErrorWithPath(({ path }: {path: string}) => `Refusing to manage \`${ path }\` which has extended attributes`);
43+
/**
44+
* `ErrorNotRegularFile` signifies that we were unable to process a file because
45+
* it is not a regular file (e.g. a named pipe or a device).
46+
*/
47+
export const ErrorNotRegularFile =
48+
newErrorWithPath(({ path }: {path: string}) => `Refusing to manage \`${ path }\` which is neither a regular file nor a symbolic link`);
49+
/**
50+
* `ErrorWritingFile` signifies that we attempted to process a file but writing
51+
* to it resulted in unexpected contents.
52+
*/
53+
export const ErrorWritingFile =
54+
newErrorWithPath(({ path, backupPath }: {path: string, backupPath: string}) => `Error writing to \`${ path }\`: written contents are unexpected; see backup in \`${ backupPath }\``);
55+
956
/**
1057
* Inserts/removes fenced lines into/from a file. Idempotent.
1158
* @param path The path to the file to work on.
1259
* @param desiredManagedLines The lines to insert into the file.
1360
* @param desiredPresent Whether the lines should be present.
61+
* @throws If the file could not be managed; for example, if it has extended
62+
* attributes, is not a regular file, or a backup exists.
1463
*/
1564
export default async function manageLinesInFile(path: string, desiredManagedLines: string[], desiredPresent: boolean): Promise<void> {
1665
const desired = getDesiredLines(desiredManagedLines, desiredPresent);
@@ -35,7 +84,7 @@ export default async function manageLinesInFile(path: string, desiredManagedLine
3584

3685
if (fileStats.isFile()) {
3786
if (await fileHasExtendedAttributes(path)) {
38-
throw new Error(`Refusing to manage ${ path } which has extended attributes`);
87+
throw new ErrorHasExtendedAttributes({ path });
3988
}
4089

4190
const tempName = `${ path }.rd-temp`;
@@ -88,13 +137,13 @@ export default async function manageLinesInFile(path: string, desiredManagedLine
88137
const actualContents = await fs.promises.readFile(path, 'utf-8');
89138

90139
if (!isEqual(targetContents, actualContents)) {
91-
throw new Error(`Error writing to ${ path }: written contents are unexpected; see backup in ${ backupPath }`);
140+
throw new ErrorWritingFile({ path, backupPath });
92141
}
93142
await fs.promises.unlink(backupPath);
94143
} else {
95144
// Target exists, and is neither a normal file nor a symbolic link.
96145
// Return with an error.
97-
throw new Error(`Refusing to manage ${ path } which is neither a regular file nor a symbolic link`);
146+
throw new ErrorNotRegularFile({ path });
98147
}
99148
}
100149

@@ -112,15 +161,15 @@ async function fileHasExtendedAttributes(filePath: string): Promise<boolean> {
112161
const { list } = await import('fs-xattr');
113162

114163
return (await list(filePath)).length > 0;
115-
} catch {
164+
} catch (cause) {
116165
if (process.env.NODE_ENV === 'test' && process.env.RD_TEST !== 'e2e') {
117166
// When running unit tests, assume they do not have extended attributes.
118167
return false;
119168
}
120169

121-
console.error(`Failed to import fs-xattr, cannot check for extended attributes on ${ filePath }; assuming it exists.`);
170+
console.error(`Failed to import fs-xattr, cannot check for extended attributes on ${ filePath }`);
122171

123-
return true;
172+
throw new ErrorDeterminingExtendedAttributes({ path: filePath }, { cause });
124173
}
125174
}
126175

pkg/rancher-desktop/integrations/pathManager.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,15 @@
77
export interface PathManager {
88
/** The PathManagementStrategy that corresponds to the implementation. */
99
readonly strategy: PathManagementStrategy
10-
/** Makes real any changes to the system. Should be idempotent. */
10+
/**
11+
* Applies changes to the system. Should be idempotent, and should not throw
12+
* any exceptions.
13+
*/
1114
enforce(): Promise<void>
12-
/** Removes any changes that the PathManager may have made. Should be idempotent. */
15+
/**
16+
* Removes any changes that the PathManager may have made. Should be
17+
* idempotent, and should not throw any exceptions.
18+
*/
1319
remove(): Promise<void>
1420
}
1521

pkg/rancher-desktop/integrations/pathManagerImpl.ts

Lines changed: 50 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,11 @@ import { Mutex } from 'async-mutex';
77
import manageLinesInFile from '@pkg/integrations/manageLinesInFile';
88
import { ManualPathManager, PathManagementStrategy, PathManager } from '@pkg/integrations/pathManager';
99
import mainEvents from '@pkg/main/mainEvents';
10+
import Logging from '@pkg/utils/logging';
1011
import paths from '@pkg/utils/paths';
1112

13+
const console = Logging['path-management'];
14+
1215
/**
1316
* RcFilePathManager is for when the user wants Rancher Desktop to
1417
* make changes to their PATH by putting lines that change it in their
@@ -32,15 +35,36 @@ export class RcFilePathManager implements PathManager {
3235
}
3336

3437
async enforce(): Promise<void> {
35-
await this.managePosix(true);
36-
await this.manageCsh(true);
37-
await this.manageFish(true);
38+
try {
39+
await this.managePosix(true);
40+
await this.manageCsh(true);
41+
await this.manageFish(true);
42+
} catch (error) {
43+
console.error(error);
44+
}
3845
}
3946

4047
async remove(): Promise<void> {
41-
await this.managePosix(false);
42-
await this.manageCsh(false);
43-
await this.manageFish(false);
48+
try {
49+
await this.managePosix(false);
50+
await this.manageCsh(false);
51+
await this.manageFish(false);
52+
} catch (error) {
53+
console.error(error);
54+
}
55+
}
56+
57+
/**
58+
* Call manageFilesInLine, wrapped in calls to trigger diagnostics updates.
59+
*/
60+
protected async manageLinesInFile(fileName: string, filePath: string, lines: string[], desiredPresent: boolean) {
61+
try {
62+
await manageLinesInFile(filePath, lines, desiredPresent);
63+
mainEvents.emit('diagnostics-event', 'path-management', { fileName, error: undefined });
64+
} catch (error: any) {
65+
mainEvents.emit('diagnostics-event', 'path-management', { fileName, error });
66+
throw error;
67+
}
4468
}
4569

4670
/**
@@ -51,7 +75,8 @@ export class RcFilePathManager implements PathManager {
5175
protected async managePosix(desiredPresent: boolean): Promise<void> {
5276
await this.posixMutex.runExclusive(async() => {
5377
const pathLine = `export PATH="${ paths.integration }:$PATH"`;
54-
// Note: order is important here. Only the first one that is present is modified.
78+
// Note: order is important here. Only the first one has the PATH added;
79+
// all others have it removed.
5580
const bashLoginShellFiles = [
5681
'.bash_profile',
5782
'.bash_login',
@@ -70,35 +95,38 @@ export class RcFilePathManager implements PathManager {
7095
await fs.promises.stat(filePath);
7196
} catch (error: any) {
7297
if (error.code === 'ENOENT') {
98+
// If the file does not exist, it is not an error.
99+
mainEvents.emit('diagnostics-event', 'path-management', { fileName, error: undefined });
73100
continue;
74101
}
102+
mainEvents.emit('diagnostics-event', 'path-management', { fileName, error });
75103
throw error;
76104
}
77-
await manageLinesInFile(filePath, [pathLine], desiredPresent);
105+
await this.manageLinesInFile(fileName, filePath, [pathLine], !linesAdded);
78106
linesAdded = true;
79-
break;
80107
}
81108

82109
// If none of the files exist, write .bash_profile
83110
if (!linesAdded) {
84-
const filePath = path.join(os.homedir(), bashLoginShellFiles[0]);
111+
const fileName = bashLoginShellFiles[0];
112+
const filePath = path.join(os.homedir(), fileName);
85113

86-
await manageLinesInFile(filePath, [pathLine], desiredPresent);
114+
await this.manageLinesInFile(fileName, filePath, [pathLine], true);
87115
}
88116
} else {
89117
// Ensure lines are not present in any of the files
90-
await Promise.all(bashLoginShellFiles.map((fileName) => {
118+
await Promise.all(bashLoginShellFiles.map(async(fileName) => {
91119
const filePath = path.join(os.homedir(), fileName);
92120

93-
return manageLinesInFile(filePath, [], desiredPresent);
121+
await this.manageLinesInFile(fileName, filePath, [], false);
94122
}));
95123
}
96124

97125
// Handle other shells' rc files and .bashrc
98-
await Promise.all(['.bashrc', '.zshrc'].map((rcName) => {
99-
const rcPath = path.join(os.homedir(), rcName);
126+
await Promise.all(['.bashrc', '.zshrc'].map((fileName) => {
127+
const rcPath = path.join(os.homedir(), fileName);
100128

101-
return manageLinesInFile(rcPath, [pathLine], desiredPresent);
129+
return this.manageLinesInFile(fileName, rcPath, [pathLine], desiredPresent);
102130
}));
103131

104132
mainEvents.invoke('diagnostics-trigger', 'RD_BIN_IN_BASH_PATH');
@@ -110,10 +138,10 @@ export class RcFilePathManager implements PathManager {
110138
await this.cshMutex.runExclusive(async() => {
111139
const pathLine = `setenv PATH "${ paths.integration }"\\:"$PATH"`;
112140

113-
await Promise.all(['.cshrc', '.tcshrc'].map((rcName) => {
114-
const rcPath = path.join(os.homedir(), rcName);
141+
await Promise.all(['.cshrc', '.tcshrc'].map((fileName) => {
142+
const rcPath = path.join(os.homedir(), fileName);
115143

116-
return manageLinesInFile(rcPath, [pathLine], desiredPresent);
144+
return this.manageLinesInFile(fileName, rcPath, [pathLine], desiredPresent);
117145
}));
118146
});
119147
}
@@ -122,11 +150,12 @@ export class RcFilePathManager implements PathManager {
122150
await this.fishMutex.runExclusive(async() => {
123151
const pathLine = `set --export --prepend PATH "${ paths.integration }"`;
124152
const configHome = process.env['XDG_CONFIG_HOME'] || path.join(os.homedir(), '.config');
153+
const fileName = 'config.fish';
125154
const fishConfigDir = path.join(configHome, 'fish');
126-
const fishConfigPath = path.join(fishConfigDir, 'config.fish');
155+
const fishConfigPath = path.join(fishConfigDir, fileName);
127156

128157
await fs.promises.mkdir(fishConfigDir, { recursive: true, mode: 0o700 });
129-
await manageLinesInFile(fishConfigPath, [pathLine], desiredPresent);
158+
await this.manageLinesInFile(fileName, fishConfigPath, [pathLine], desiredPresent);
130159
});
131160
}
132161
}

pkg/rancher-desktop/main/diagnostics/diagnostics.ts

Lines changed: 34 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { DiagnosticsCategory, DiagnosticsChecker, DiagnosticsCheckerResult } from './types';
1+
import { DiagnosticsCategory, DiagnosticsChecker, DiagnosticsCheckerResult, DiagnosticsCheckerSingleResult } from './types';
22

33
import mainEvents from '@pkg/main/mainEvents';
44
import Logging from '@pkg/utils/logging';
@@ -38,7 +38,7 @@ export class DiagnosticsManager {
3838
lastUpdate = new Date(0);
3939

4040
/** Last known check results, indexed by the checker id. */
41-
results: Record<DiagnosticsChecker['id'], DiagnosticsCheckerResult> = {};
41+
results: Record<DiagnosticsChecker['id'], DiagnosticsCheckerResult|DiagnosticsCheckerSingleResult[]> = {};
4242

4343
/** Mapping of category name to diagnostic ids */
4444
readonly checkerIdByCategory: Partial<Record<DiagnosticsCategory, string[]>> = {};
@@ -52,6 +52,7 @@ export class DiagnosticsManager {
5252
import('./kubeContext'),
5353
import('./limaDarwin'),
5454
import('./mockForScreenshots'),
55+
import('./pathManagement'),
5556
import('./rdBinInShell'),
5657
import('./testCheckers'),
5758
import('./wslFromStore'),
@@ -92,9 +93,10 @@ export class DiagnosticsManager {
9293
}
9394

9495
protected async applicableCheckers(categoryName: string | null, id: string | null): Promise<DiagnosticsChecker[]> {
96+
const checkerId = id?.split(':', 1)[0];
9597
const checkers = (await this.checkers)
9698
.filter(checker => categoryName ? checker.category === categoryName : true)
97-
.filter(checker => id ? checker.id === id : true);
99+
.filter(checker => checkerId ? checker.id === checkerId : true);
98100

99101
return (await Promise.all(checkers.map(async(checker) => {
100102
try {
@@ -124,12 +126,25 @@ export class DiagnosticsManager {
124126
return {
125127
last_update: this.lastUpdate.toISOString(),
126128
checks: checkers
127-
.map(checker => ({
128-
...this.results[checker.id],
129-
id: checker.id,
130-
category: checker.category,
131-
mute: false,
132-
})),
129+
.flatMap((checker) => {
130+
const result = this.results[checker.id];
131+
132+
if (Array.isArray(result)) {
133+
return result.map(result => ({
134+
...result,
135+
id: `${ checker.id }:${ result.id }`,
136+
category: checker.category,
137+
mute: false,
138+
}));
139+
} else {
140+
return {
141+
...result,
142+
id: checker.id,
143+
category: checker.category,
144+
mute: false,
145+
};
146+
}
147+
}),
133148
};
134149
}
135150

@@ -139,8 +154,16 @@ export class DiagnosticsManager {
139154
protected async runChecker(checker: DiagnosticsChecker) {
140155
console.debug(`Running check ${ checker.id }`);
141156
try {
142-
this.results[checker.id] = await checker.check();
143-
console.debug(`Check ${ checker.id } result: ${ JSON.stringify(this.results[checker.id]) }`);
157+
const result = await checker.check();
158+
159+
this.results[checker.id] = result;
160+
if (Array.isArray(result)) {
161+
for (const singleResult of result) {
162+
console.debug(`Check ${ checker.id }:${ singleResult.id } result: ${ JSON.stringify(singleResult) }`);
163+
}
164+
} else {
165+
console.debug(`Check ${ checker.id } result: ${ JSON.stringify(result) }`);
166+
}
144167
} catch (e) {
145168
console.error(`ERROR checking ${ checker.id }`, { e });
146169
}

pkg/rancher-desktop/main/diagnostics/kubeConfigSymlink.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ const CheckKubeConfigSymlink: DiagnosticsChecker = {
2727
id: 'VERIFY_WSL_INTEGRATION_KUBECONFIG',
2828
category: DiagnosticsCategory.Kubernetes,
2929
applicable() {
30-
return Promise.resolve(true);
30+
return Promise.resolve(process.platform === 'win32');
3131
},
3232
async check() {
3333
return Promise.resolve({

0 commit comments

Comments
 (0)