Skip to content
Open
6 changes: 3 additions & 3 deletions vscode-dotnet-runtime-extension/src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -266,7 +266,7 @@ export function activate(vsCodeContext: vscode.ExtensionContext, extensionContex
commandContext.forceUpdate = true;
}

const isOffline = !(await WebRequestWorkerSingleton.getInstance().isOnline(timeoutValue ?? defaultTimeoutValue, globalEventStream));
const isOffline = !(await WebRequestWorkerSingleton.getInstance().isOnline(timeoutValue ?? defaultTimeoutValue, globalEventStream, workerContext.proxyUrl));
if (!commandContext.forceUpdate || isOffline)
{
// 3.0 Breaking Change: Don't always return latest .NET runtime by default
Expand Down Expand Up @@ -971,7 +971,7 @@ ${JSON.stringify(commandContext)}`));

async function getExistingInstallIfOffline(worker: DotnetCoreAcquisitionWorker, workerContext: IAcquisitionWorkerContext): Promise<IDotnetAcquireResult | null>
{
const isOffline = !(await WebRequestWorkerSingleton.getInstance().isOnline(timeoutValue ?? defaultTimeoutValue, globalEventStream));
const isOffline = !(await WebRequestWorkerSingleton.getInstance().isOnline(timeoutValue ?? defaultTimeoutValue, globalEventStream, workerContext.proxyUrl));
if (isOffline)
{
return getExistingInstallOffline(worker, workerContext);
Expand All @@ -989,7 +989,7 @@ ${JSON.stringify(commandContext)}`));
}
else
{
if (!(await WebRequestWorkerSingleton.getInstance().isOnline(timeoutValue ?? defaultTimeoutValue, globalEventStream)))
if (!(await WebRequestWorkerSingleton.getInstance().isOnline(timeoutValue ?? defaultTimeoutValue, globalEventStream, workerContext.proxyUrl)))
{
globalEventStream.post(new DotnetOfflineWarning(`It looks like you may be offline (can you connect to www.microsoft.com?) and have no compatible installations of .NET ${workerContext.acquisitionContext.version} for ${workerContext.acquisitionContext.requestingExtensionId ?? 'user'}.
Installation will timeout in ${timeoutValue} seconds.`))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ If you cannot change this flag, try setting a custom existingDotnetPath via the
}
if (error)
{
if (!(await WebRequestWorkerSingleton.getInstance().isOnline(this.workerContext.timeoutSeconds, this.eventStream)))
if (!(await WebRequestWorkerSingleton.getInstance().isOnline(this.workerContext.timeoutSeconds, this.eventStream, this.workerContext.proxyUrl)))
{
const offlineError = new EventBasedError('DotnetOfflineFailure', 'No internet connection detected: Cannot install .NET');
this.eventStream.post(new DotnetOfflineFailure(offlineError, install));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ export class WebRequestWorkerSingleton
{
timeoutCancelTokenHook.abort();
ctx.eventStream.post(new WebRequestTime(`Timer for request:`, String(this.timeoutMsFromCtx(ctx)), 'false', url, '777')); // 777 for custom abort status. arbitrary
if (!(await this.isOnline(ctx.timeoutSeconds, ctx.eventStream)))
if (!(await this.isOnline(ctx.timeoutSeconds, ctx.eventStream, ctx.proxyUrl)))
{
const offlineError = new EventBasedError('DotnetOfflineFailure', 'No internet connection detected: Cannot install .NET');
ctx.eventStream.post(new DotnetOfflineFailure(offlineError, null));
Expand Down Expand Up @@ -269,7 +269,7 @@ export class WebRequestWorkerSingleton
}
}

public async isOnline(timeoutSec: number, eventStream: IEventStream): Promise<boolean>
public async isOnline(timeoutSec: number, eventStream: IEventStream, proxyUrl?: string): Promise<boolean>
{
if (process.env.DOTNET_INSTALL_TOOL_OFFLINE === '1')
{
Expand All @@ -281,7 +281,7 @@ export class WebRequestWorkerSingleton
// ... 100 ms is there as a default to prevent the dns resolver from throwing a runtime error if the user sets timeoutSeconds to 0.

const dnsResolver = new dns.promises.Resolver({ timeout: expectedDNSResolutionTimeMs });
const couldConnect = await dnsResolver.resolve(microsoftServerHostName).then(() =>
const dnsOnline = await dnsResolver.resolve(microsoftServerHostName).then(() =>
{
return true;
}).catch((error: any) =>
Expand All @@ -290,7 +290,47 @@ export class WebRequestWorkerSingleton
return false;
});

return couldConnect;
if (dnsOnline)
{
return true;
}

// DNS failed — but in proxy environments, the proxy handles DNS resolution, so direct DNS lookups may fail
// even though HTTP connectivity works fine. Fall back to a lightweight HEAD request through the proxy-configured client.
if (this.client)
{
const httpFallbackTimeoutMs = Math.max(timeoutSec * 1000, 2000);
const proxyAgent = await this.getProxyAgent(proxyUrl, eventStream);
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should validate this doesn't throw and break the offline flow


const headOptions: object = {
timeout: httpFallbackTimeoutMs,
cache: false,
validateStatus: () => true, // Any HTTP response means we're online, even 4xx/5xx
...(proxyAgent !== null && { proxy: false }),
...(proxyAgent !== null && { httpsAgent: proxyAgent }),
};

const headOnline = await this.client.head(`https://${microsoftServerHostName}`, headOptions)
.then(() =>
{
return true; // Any response at all means we have connectivity
})
.catch(() =>
{
return false;
});

if (headOnline)
{
eventStream.post(new OfflineDetectionLogicTriggered(new EventCancellationError('DnsFailedButHttpSucceeded',
`DNS resolution failed but HTTP HEAD request succeeded. This may indicate a proxy is handling DNS.`),
`DNS failed but HTTP connectivity confirmed via HEAD request to ${microsoftServerHostName}.`));
}

return headOnline;
}

return false;
}
/**
*
Expand Down Expand Up @@ -321,11 +361,22 @@ export class WebRequestWorkerSingleton
}

private async GetProxyAgentIfNeeded(ctx: IAcquisitionWorkerContext): Promise<HttpsProxyAgent<string> | null>
{
return this.getProxyAgent(ctx.proxyUrl, ctx.eventStream);
}

/**
* Resolves a proxy agent from the manual proxy URL or auto-detected system proxy settings.
* Decoupled from IAcquisitionWorkerContext so it can be used by isOnline and other callers that don't have a full context.
*/
private async getProxyAgent(manualProxyUrl?: string, eventStream?: IEventStream): Promise<HttpsProxyAgent<string> | null>
{
try
{
const hasManualProxy = this.proxySettingConfiguredManually(manualProxyUrl);

let discoveredProxy = '';
if (!this.proxySettingConfiguredManually(ctx))
if (!hasManualProxy)
{
const autoDetectProxies = await getProxySettings();
if (autoDetectProxies?.https)
Expand All @@ -338,17 +389,17 @@ export class WebRequestWorkerSingleton
}
}

if (this.proxySettingConfiguredManually(ctx) || discoveredProxy)
if (hasManualProxy || discoveredProxy)
{
const finalProxy = ctx?.proxyUrl && ctx?.proxyUrl !== '""' && ctx?.proxyUrl !== '' ? ctx.proxyUrl : discoveredProxy;
ctx.eventStream.post(new ProxyUsed(`Utilizing the Proxy : Manual ? ${ctx?.proxyUrl}, Automatic: ${discoveredProxy}, Decision : ${finalProxy}`))
const finalProxy = hasManualProxy ? manualProxyUrl! : discoveredProxy;
eventStream?.post(new ProxyUsed(`Utilizing the Proxy : Manual ? ${manualProxyUrl}, Automatic: ${discoveredProxy}, Decision : ${finalProxy}`))
const proxyAgent = new HttpsProxyAgent(finalProxy);
return proxyAgent;
}
}
catch (error: any)
{
ctx.eventStream.post(new SuppressedAcquisitionError(error, `The proxy lookup failed, most likely due to limited registry access. Skipping automatic proxy lookup.`));
eventStream?.post(new SuppressedAcquisitionError(error, `The proxy lookup failed, most likely due to limited registry access. Skipping automatic proxy lookup.`));
}

return null;
Expand Down Expand Up @@ -485,9 +536,9 @@ If you're on a proxy and disable registry access, you must set the proxy in our
}
}

private proxySettingConfiguredManually(ctx: IAcquisitionWorkerContext): boolean
private proxySettingConfiguredManually(proxyUrl?: string): boolean
{
return ctx?.proxyUrl ? ctx?.proxyUrl !== '""' : false;
return proxyUrl ? proxyUrl !== '""' && proxyUrl !== '' : false;
}

private timeoutMsFromCtx(ctx: IAcquisitionWorkerContext): number
Expand Down
Loading