|
3 | 3 | * The .NET Foundation licenses this file to you under the MIT license. |
4 | 4 | *--------------------------------------------------------------------------------------------*/ |
5 | 5 | import * as fs from 'fs'; |
6 | | -import * as path from 'path'; |
7 | 6 | import * as os from 'os'; |
8 | | -import { |
| 7 | +import * as path from 'path'; |
| 8 | +import |
| 9 | +{ |
9 | 10 | DotnetInstallationValidated, |
10 | 11 | DotnetInstallationValidationError, |
11 | | - EventBasedError, |
12 | | - DotnetInstallationValidationMissed |
| 12 | + DotnetInstallationValidationMissed, |
| 13 | + EventBasedError |
13 | 14 | } from '../EventStream/EventStreamEvents'; |
14 | | -import { IInstallationValidator } from './IInstallationValidator'; |
15 | 15 | import { DotnetInstall } from './DotnetInstall'; |
| 16 | +import { IInstallationValidator } from './IInstallationValidator'; |
| 17 | + |
| 18 | +/** |
| 19 | + * Context object containing information needed for validation operations. |
| 20 | + */ |
| 21 | +interface ValidationContext |
| 22 | +{ |
| 23 | + install: DotnetInstall; |
| 24 | + dotnetPath: string; |
| 25 | + baseErrorMessage: string; |
| 26 | +} |
| 27 | + |
| 28 | +/** |
| 29 | + * Validates .NET installations by checking for the existence and validity of .NET executables or directories. |
| 30 | + * Provides options to either throw errors or return false on validation failure. |
| 31 | + */ |
| 32 | +export class InstallationValidator extends IInstallationValidator |
| 33 | +{ |
| 34 | + /** |
| 35 | + * Validates a .NET installation by checking either an executable file or installation directory. |
| 36 | + * |
| 37 | + * @param install - The DotnetInstall object containing installation details |
| 38 | + * @param dotnetPath - Path to the .NET executable or installation directory to validate |
| 39 | + * @param validateDirectory - If true, validates the path as a directory; if false, validates as an executable file |
| 40 | + * @param failOnErr - If true, throws an error on validation failure; if false, returns false and posts a validation missed event |
| 41 | + * @returns true if validation passes, false if validation fails. Throws if failOnErr is true and validation fails. |
| 42 | + * @remarks Validation is not completely exhaustive. Runtime files besides the executable may be missing. |
| 43 | + * @throws EventBasedError if validation fails and failOnErr is true |
| 44 | + */ |
| 45 | + public validateDotnetInstall(install: DotnetInstall, dotnetPath: string, validateDirectory = false, failOnErr = true): boolean |
| 46 | + { |
| 47 | + const validationContext = { |
| 48 | + install, |
| 49 | + dotnetPath, |
| 50 | + baseErrorMessage: `Validation of .dotnet installation for version ${JSON.stringify(install)} failed:` |
| 51 | + }; |
16 | 52 |
|
17 | | -export class InstallationValidator extends IInstallationValidator { |
18 | | - public validateDotnetInstall(install: DotnetInstall, dotnetPath: string, isDotnetFolder = false, failOnErr = true): void { |
19 | | - const dotnetValidationFailed = `Validation of .dotnet installation for version ${JSON.stringify(install)} failed:`; |
20 | | - const folder = path.dirname(dotnetPath); |
| 53 | + const isValid = validateDirectory |
| 54 | + ? this.validateDotnetDirectory(validationContext, failOnErr) |
| 55 | + : this.validateDotnetExecutable(validationContext, failOnErr); |
21 | 56 |
|
22 | | - if(!isDotnetFolder) |
| 57 | + if (isValid) |
23 | 58 | { |
24 | | - this.assertOrThrowError(failOnErr, fs.existsSync(folder), |
25 | | - `${dotnetValidationFailed} Expected installation folder ${folder} does not exist.`, install, dotnetPath); |
| 59 | + this.eventStream.post(new DotnetInstallationValidated(install)); |
| 60 | + } |
| 61 | + |
| 62 | + return isValid; |
| 63 | + } |
26 | 64 |
|
27 | | - this.assertOrThrowError(failOnErr, fs.existsSync(dotnetPath), |
28 | | - `${dotnetValidationFailed} Expected executable does not exist at "${dotnetPath}"`, install, dotnetPath); |
| 65 | + /** |
| 66 | + * Validates a .NET executable file by checking: |
| 67 | + * 1. Parent directory exists |
| 68 | + * 2. Executable file exists |
| 69 | + * 3. Path points to a file (not a directory) |
| 70 | + * |
| 71 | + * @param context - Validation context containing install details and paths |
| 72 | + * @param failOnErr - Whether to throw on validation failure or return false |
| 73 | + * @returns true if all validations pass, false otherwise |
| 74 | + */ |
| 75 | + private validateDotnetExecutable(context: ValidationContext, failOnErr: boolean): boolean |
| 76 | + { |
| 77 | + const { dotnetPath, baseErrorMessage } = context; |
| 78 | + const parentDirectory = path.dirname(dotnetPath); |
29 | 79 |
|
30 | | - this.assertOrThrowError(failOnErr, fs.lstatSync(dotnetPath).isFile(), |
31 | | - `${dotnetValidationFailed} Expected executable file exists but is not a file: "${dotnetPath}"`, install, dotnetPath); |
| 80 | + // Check if parent directory exists |
| 81 | + if (!this.validateCondition( |
| 82 | + fs.existsSync(parentDirectory), |
| 83 | + `${baseErrorMessage} Expected installation folder ${parentDirectory} does not exist.`, |
| 84 | + context, |
| 85 | + failOnErr |
| 86 | + )) |
| 87 | + { |
| 88 | + return false; |
32 | 89 | } |
33 | | - else |
| 90 | + |
| 91 | + // Check if executable exists |
| 92 | + if (!this.validateCondition( |
| 93 | + fs.existsSync(dotnetPath), |
| 94 | + `${baseErrorMessage} Expected executable does not exist at "${dotnetPath}"`, |
| 95 | + context, |
| 96 | + failOnErr |
| 97 | + )) |
34 | 98 | { |
35 | | - this.assertOrThrowError(failOnErr, fs.existsSync(folder), |
36 | | - `${dotnetValidationFailed} Expected dotnet folder ${dotnetPath} does not exist.`, install, dotnetPath); |
| 99 | + return false; |
| 100 | + } |
37 | 101 |
|
38 | | - try |
| 102 | + // Check if path points to a file (not a directory) |
| 103 | + try |
| 104 | + { |
| 105 | + if (!this.validateCondition( |
| 106 | + fs.lstatSync(dotnetPath).isFile(), |
| 107 | + `${baseErrorMessage} Expected executable file exists but is not a file: "${dotnetPath}"`, |
| 108 | + context, |
| 109 | + failOnErr |
| 110 | + )) |
39 | 111 | { |
40 | | - this.assertOrThrowError(failOnErr, fs.readdirSync(folder).length !== 0, |
41 | | - `${dotnetValidationFailed} The dotnet folder is empty "${dotnetPath}"`, install, dotnetPath); |
| 112 | + return false; |
42 | 113 | } |
43 | | - catch(error : any) // fs.readdirsync throws ENOENT so we need to recall the function |
| 114 | + } catch (error) |
| 115 | + { |
| 116 | + return this.handleValidationError( |
| 117 | + `${baseErrorMessage} Unable to verify that "${dotnetPath}" is a file: ${JSON.stringify(error ?? '')}`, |
| 118 | + context, |
| 119 | + failOnErr |
| 120 | + ); |
| 121 | + } |
| 122 | + |
| 123 | + return true; |
| 124 | + } |
| 125 | + |
| 126 | + private validateDotnetDirectory(context: ValidationContext, failOnErr: boolean): boolean |
| 127 | + { |
| 128 | + const { dotnetPath, baseErrorMessage } = context; |
| 129 | + |
| 130 | + // Check if directory exists |
| 131 | + if (!this.validateCondition( |
| 132 | + fs.existsSync(dotnetPath), |
| 133 | + `${baseErrorMessage} Expected dotnet folder ${dotnetPath} does not exist.`, |
| 134 | + context, |
| 135 | + failOnErr |
| 136 | + )) |
| 137 | + { |
| 138 | + return false; |
| 139 | + } |
| 140 | + |
| 141 | + // Check if directory is not empty |
| 142 | + try |
| 143 | + { |
| 144 | + const directoryContents = fs.readdirSync(dotnetPath); |
| 145 | + if (!this.validateCondition( |
| 146 | + directoryContents.length > 0, |
| 147 | + `${baseErrorMessage} The dotnet folder is empty "${dotnetPath}"`, |
| 148 | + context, |
| 149 | + failOnErr |
| 150 | + )) |
44 | 151 | { |
45 | | - this.assertOrThrowError(failOnErr, false, |
46 | | - `${dotnetValidationFailed} The dotnet file dne "${dotnetPath}"`, install, dotnetPath); |
| 152 | + return false; |
47 | 153 | } |
48 | 154 | } |
| 155 | + catch (error) |
| 156 | + { |
| 157 | + return this.handleValidationError( |
| 158 | + `${baseErrorMessage} Unable to read dotnet directory "${dotnetPath}": ${JSON.stringify(error ?? '')}`, |
| 159 | + context, |
| 160 | + failOnErr |
| 161 | + ); |
| 162 | + } |
49 | 163 |
|
50 | | - this.eventStream.post(new DotnetInstallationValidated(install)); |
| 164 | + return true; |
51 | 165 | } |
52 | 166 |
|
53 | | - private assertOrThrowError(failOnErr : boolean, passedValidation: boolean, message: string, install: DotnetInstall, dotnetPath: string) { |
54 | | - if (!passedValidation && failOnErr) |
| 167 | + private validateCondition( |
| 168 | + condition: boolean, |
| 169 | + errorMessage: string, |
| 170 | + context: ValidationContext, |
| 171 | + failOnErr: boolean |
| 172 | + ): boolean |
| 173 | + { |
| 174 | + if (!condition) |
55 | 175 | { |
56 | | - this.eventStream.post(new DotnetInstallationValidationError(new Error(message), install, dotnetPath)); |
57 | | - throw new EventBasedError('DotnetInstallationValidationError', message); |
| 176 | + return this.handleValidationError(errorMessage, context, failOnErr); |
58 | 177 | } |
| 178 | + return true; |
| 179 | + } |
59 | 180 |
|
60 | | - if(os.platform() === 'darwin') |
| 181 | + private handleValidationError( |
| 182 | + message: string, |
| 183 | + context: ValidationContext, |
| 184 | + failOnErr: boolean |
| 185 | + ): boolean |
| 186 | + { |
| 187 | + const { install, dotnetPath } = context; |
| 188 | + |
| 189 | + if (failOnErr) |
61 | 190 | { |
62 | | - message = `Did you close the .NET Installer, cancel the installation, or refuse the password prompt? If you want to install the .NET SDK, please try again. If you are facing an error, please report it at https://github.com/dotnet/vscode-dotnet-runtime/issues. |
63 | | -${message}`; |
| 191 | + this.eventStream.post(new DotnetInstallationValidationError(new Error(message), install, dotnetPath)); |
| 192 | + throw new EventBasedError('DotnetInstallationValidationError', this.enhanceErrorMessageForMacOS(message)); |
64 | 193 | } |
| 194 | + else |
| 195 | + { |
| 196 | + this.eventStream?.post(new DotnetInstallationValidationMissed(new Error(message), message)); |
| 197 | + } |
| 198 | + |
| 199 | + return false; |
| 200 | + } |
65 | 201 |
|
66 | | - if(!passedValidation && !failOnErr) |
| 202 | + private enhanceErrorMessageForMacOS(message: string): string |
| 203 | + { |
| 204 | + if (os.platform() === 'darwin') |
67 | 205 | { |
68 | | - this.eventStream?.post(new DotnetInstallationValidationMissed(new Error(message), message)) |
| 206 | + return `Did you close the .NET Installer, cancel the installation, or refuse the password prompt? If you want to install the .NET SDK, please try again. If you are facing an error, please report it at https://github.com/dotnet/vscode-dotnet-runtime/issues. |
| 207 | +${message}`; |
69 | 208 | } |
| 209 | + return message; |
70 | 210 | } |
71 | 211 | } |
0 commit comments