-
Notifications
You must be signed in to change notification settings - Fork 871
Expand file tree
/
Copy pathlaunchProfiles.ts
More file actions
243 lines (210 loc) · 9.64 KB
/
launchProfiles.ts
File metadata and controls
243 lines (210 loc) · 9.64 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
import * as path from 'path';
import * as fs from 'fs';
import { ExecutableLaunchConfiguration, EnvVar, ProjectLaunchConfiguration } from '../dcp/types';
import { extensionLogOutputChannel } from '../utils/logging';
import { isSingleFileApp } from './languages/dotnet';
import { stripComments } from 'jsonc-parser';
/*
* Represents a launchSettings.json profile.
* Only a property that is available both in the C# vscode debugger (https://code.visualstudio.com/docs/csharp/debugger-settings)
* *and* in the launchSettings.json is available here.
*/
export interface LaunchProfile {
commandName: string;
executablePath?: string;
workingDirectory?: string;
// args in debug configuration
commandLineArgs?: string;
// Both these properties must be set to launch the browser. See
// https://code.visualstudio.com/docs/csharp/debugger-settings#_starting-a-web-browser
launchBrowser?: boolean;
applicationUrl?: string;
// env in debug configuration
environmentVariables?: { [key: string]: string };
// checkForDevCert in debug configuration
useSSL?: boolean;
}
export interface LaunchSettings {
profiles: { [key: string]: LaunchProfile };
}
export interface LaunchProfileResult {
profile: LaunchProfile | null;
profileName: string | null;
}
/**
* Reads and parses the launchSettings.json file for a given project
*/
export async function readLaunchSettings(projectPath: string): Promise<LaunchSettings | null> {
try {
let launchSettingsPath: string;
if (isSingleFileApp(projectPath)) {
const fileNameWithoutExt = path.basename(projectPath, path.extname(projectPath));
launchSettingsPath = path.join(path.dirname(projectPath), `${fileNameWithoutExt}.run.json`);
} else {
const projectDir = path.dirname(projectPath);
launchSettingsPath = path.join(projectDir, 'Properties', 'launchSettings.json');
}
const launchSettingsExists = fs.existsSync(launchSettingsPath);
extensionLogOutputChannel.debug('[launchSettings] Resolved launchSettings path', {
projectPath,
resolvedPath: launchSettingsPath,
exists: launchSettingsExists,
});
if (!launchSettingsExists) {
extensionLogOutputChannel.debug(`Launch settings file not found at: ${launchSettingsPath}`);
return null;
}
let content = fs.readFileSync(launchSettingsPath, 'utf8');
// We need to strip comments from the JSON file before parsing
content = stripComments(content);
const launchSettings = JSON.parse(content) as LaunchSettings;
const profileNames = launchSettings?.profiles ? Object.keys(launchSettings.profiles) : [];
extensionLogOutputChannel.debug(`[launchSettings] parsed ${profileNames.length} profiles: ${profileNames.join(', ')}`);
extensionLogOutputChannel.debug(`Successfully read launch settings from: ${launchSettingsPath}`);
return launchSettings;
} catch (error) {
extensionLogOutputChannel.error(`Failed to read launch settings for project ${projectPath}: ${error}`);
return null;
}
}
/**
* Determines the base launch profile according to the Aspire launch profile rules
*/
export function determineBaseLaunchProfile(
launchConfig: ProjectLaunchConfiguration,
launchSettings: LaunchSettings | null
): LaunchProfileResult {
const debugMessage =
`[launchProfile] determineBaseLaunchProfile:
disable_launch_profile=${!!launchConfig.disable_launch_profile}
launch_profile='${launchConfig.launch_profile ?? ''}'
hasLaunchSettings=${!!launchSettings}
profileCount=${launchSettings?.profiles ? Object.keys(launchSettings.profiles).length : 0}`;
extensionLogOutputChannel.debug(debugMessage);
// If disable_launch_profile property is set to true in project launch configuration, there is no base profile, regardless of the value of launch_profile property.
if (launchConfig.disable_launch_profile === true) {
extensionLogOutputChannel.debug('Launch profile disabled via disable_launch_profile=true');
return { profile: null, profileName: null };
}
if (!launchSettings || !launchSettings.profiles) {
extensionLogOutputChannel.debug('No launch settings or profiles available');
return { profile: null, profileName: null };
}
// If launch_profile property is set, check if that profile exists
if (launchConfig.launch_profile) {
const profileName = launchConfig.launch_profile;
const profile = launchSettings.profiles[profileName];
if (profile) {
extensionLogOutputChannel.debug(`Using explicit launch profile: ${profileName}`);
return { profile, profileName };
} else {
extensionLogOutputChannel.debug(`Explicit launch profile '${profileName}' not found in launch settings`);
return { profile: null, profileName: null };
}
}
// If launch_profile is absent, choose the first one with commandName='Project'
for (const [name, profile] of Object.entries(launchSettings.profiles)) {
if (profile.commandName === 'Project') {
extensionLogOutputChannel.debug(`Using default launch profile: ${name}`);
return { profile, profileName: name };
}
}
// TODO: If launch_profile is absent, check for a ServiceDefaults project in the workspace
// and look for a launch profile with that ServiceDefaults project name in the current project's launch settings
extensionLogOutputChannel.debug('No base launch profile determined');
return { profile: null, profileName: null };
}
/**
* Merges environment variables from launch profile with run session environment variables
* Run session variables take precedence over launch profile variables
*/
export function mergeEnvironmentVariables(
baseProfileEnv: { [key: string]: string } | undefined,
runSessionEnv: EnvVar[],
runApiEnv?: { [key: string]: string }
): [string, string][] {
const merged: { [key: string]: string } = {};
// Start with base profile environment variables
if (baseProfileEnv) {
Object.assign(merged, baseProfileEnv);
}
// Override with run API environment variables
if (runApiEnv) {
Object.assign(merged, runApiEnv);
}
// Override with run session environment variables (these take precedence)
for (const envVar of runSessionEnv) {
merged[envVar.name] = envVar.value;
}
return Object.entries(merged);
}
/**
* Determines the final arguments array according to launch profile rules
* If run session args are present (including empty array), they completely replace launch profile args
* If run session args are absent/null, launch profile args are used if available
*/
export function determineArguments(
baseProfileArgs: string | undefined,
runSessionArgs: string[] | undefined | null
): string | undefined {
// If run session args are explicitly provided (including empty array), use them
if (runSessionArgs !== undefined && runSessionArgs !== null) {
extensionLogOutputChannel.debug(`Using run session arguments: ${JSON.stringify(runSessionArgs)}`);
return runSessionArgs.join(' ');
}
// If run session args are absent/null, use launch profile args if available
if (baseProfileArgs) {
extensionLogOutputChannel.debug(`Using launch profile arguments: ${baseProfileArgs}`);
return baseProfileArgs;
}
extensionLogOutputChannel.debug('No arguments determined');
return undefined;
}
/**
* Determines the working directory for project execution
* Uses launch profile WorkingDirectory if specified, otherwise uses project directory
*/
export function determineWorkingDirectory(
projectPath: string,
baseProfile: LaunchProfile | null
): string {
if (baseProfile?.workingDirectory) {
// If working directory is relative, resolve it relative to project directory
if (path.isAbsolute(baseProfile.workingDirectory)) {
extensionLogOutputChannel.debug(`Using absolute working directory from launch profile: ${baseProfile.workingDirectory}`);
return baseProfile.workingDirectory;
} else {
const projectDir = path.dirname(projectPath);
const workingDir = path.resolve(projectDir, baseProfile.workingDirectory);
extensionLogOutputChannel.debug(`Using relative working directory from launch profile: ${workingDir}`);
return workingDir;
}
}
// Default to project directory
const projectDir = path.dirname(projectPath);
extensionLogOutputChannel.debug(`Using default working directory (project directory): ${projectDir}`);
return projectDir;
}
export interface ServerReadyAction {
action: "openExternally";
pattern: string;
uriFormat: string;
}
export function determineServerReadyAction(launchBrowser?: boolean, applicationUrl?: string, debugConfigurationServerReadyAction?: ServerReadyAction): ServerReadyAction | undefined {
if (launchBrowser === false) {
return undefined;
}
// A serverReadyAction may already have been defined in the aspire launch configuration. In that case, it applies to all resources unless launchBrowser is false (resource explicitly has opted out of browser launch).
if (debugConfigurationServerReadyAction) {
return debugConfigurationServerReadyAction;
}
if (launchBrowser === undefined || !applicationUrl) {
return undefined;
}
let uriFormat = applicationUrl.includes(';') ? applicationUrl.split(';')[0] : applicationUrl;
return {
action: "openExternally",
pattern: "\\bNow listening on:\\s+(https?://\\S+)",
uriFormat: uriFormat
};
}