More auto-detection improvements (#486)

* Introduce constants

* Properly handle text editor events

* Use waitForSubscriptions to avoid race condition

* Calculate workspaceFolder ourselves on lang change

* Only load schema for active text editor

* Only request schema on session change if logged in

* Failure to load Git extension shouldn't be fatal

* Simplify onDidOpenTextDocument handling

* Add a bunch of logs
This commit is contained in:
Winston Liu 2022-08-11 22:09:40 -07:00 коммит произвёл GitHub
Родитель 282970bd18
Коммит d4d82fcf06
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
2 изменённых файлов: 86 добавлений и 31 удалений

Просмотреть файл

@ -13,8 +13,21 @@ import { schemaContributor, CUSTOM_SCHEMA_REQUEST, CUSTOM_CONTENT_REQUEST } from
import { telemetryHelper } from './helpers/telemetryHelper';
import { getAzureAccountExtensionApi } from './extensionApis';
/**
* The unique string that identifies the Azure Pipelines languge.
*/
const LANGUAGE_IDENTIFIER = 'azure-pipelines';
/**
* The document selector to use when deciding whether to activate Azure Pipelines-specific features.
*/
const DOCUMENT_SELECTOR = [
{ language: LANGUAGE_IDENTIFIER, scheme: 'file' },
{ language: LANGUAGE_IDENTIFIER, scheme: 'untitled' }
]
export async function activate(context: vscode.ExtensionContext) {
const configurePipelineEnabled = vscode.workspace.getConfiguration('azure-pipelines').get<boolean>('configure', true);
const configurePipelineEnabled = vscode.workspace.getConfiguration(LANGUAGE_IDENTIFIER).get<boolean>('configure', true);
telemetryHelper.initialize('azurePipelines.activate', {
isActivationEvent: 'true',
configurePipelineEnabled: `${configurePipelineEnabled}`,
@ -34,7 +47,7 @@ export async function activate(context: vscode.ExtensionContext) {
async function activateYmlContributor(context: vscode.ExtensionContext) {
const serverOptions: languageclient.ServerOptions = getServerOptions(context);
const clientOptions: languageclient.LanguageClientOptions = getClientOptions();
const client = new languageclient.LanguageClient('azure-pipelines', 'Azure Pipelines Language', serverOptions, clientOptions);
const client = new languageclient.LanguageClient(LANGUAGE_IDENTIFIER, 'Azure Pipelines Language', serverOptions, clientOptions);
const disposable = client.start();
context.subscriptions.push(disposable);
@ -56,7 +69,7 @@ async function activateYmlContributor(context: vscode.ExtensionContext) {
});
// TODO: Can we get rid of this since it's set in package.json?
vscode.languages.setLanguageConfiguration('azure-pipelines', { wordPattern: /("(?:[^\\\"]*(?:\\.)?)*"?)|[^\s{}\[\],:]+/ });
vscode.languages.setLanguageConfiguration(LANGUAGE_IDENTIFIER, { wordPattern: /("(?:[^\\\"]*(?:\\.)?)*"?)|[^\s{}\[\],:]+/ });
// Let the server know of any schema changes.
context.subscriptions.push(vscode.workspace.onDidChangeConfiguration(async event => {
@ -66,27 +79,33 @@ async function activateYmlContributor(context: vscode.ExtensionContext) {
}));
// Load the schema if we were activated because an Azure Pipelines file.
if (vscode.window.activeTextEditor?.document.languageId === 'azure-pipelines') {
if (vscode.window.activeTextEditor?.document.languageId === LANGUAGE_IDENTIFIER) {
await loadSchema(context, client);
}
// And subscribe to future open events, as well.
context.subscriptions.push(vscode.window.onDidChangeActiveTextEditor(async () => {
await loadSchema(context, client);
}));
// Or if the active editor's language changes.
context.subscriptions.push(vscode.workspace.onDidOpenTextDocument(async textDocument => {
// NOTE: We need to explicitly compute the workspace folder here rather than
// relying on the logic in loadSchema, because somehow preview editors
// don't count as "active".
if (textDocument?.languageId !== 'azure-pipelines') {
// Ensure this event is due to a language change.
// Since onDidOpenTextDocument is fired *before* activeTextEditor changes,
// if the URIs are the same we know that the new text document must be
// due to a language change.
if (textDocument.uri !== vscode.window.activeTextEditor?.document.uri) {
return;
}
const workspaceFolder = vscode.workspace.getWorkspaceFolder(textDocument.uri);
await loadSchema(context, client, workspaceFolder);
await loadSchema(context, client);
}));
// Re-request the schema on Azure login since auto-detection is dependent on login.
// Re-request the schema when sessions change since auto-detection is dependent on
// being able to query ADO organizations using session credentials.
const azureAccountApi = await getAzureAccountExtensionApi();
context.subscriptions.push(azureAccountApi.onStatusChanged(async status => {
if (status === 'LoggedIn') {
context.subscriptions.push(azureAccountApi.onSessionsChanged(async () => {
if (azureAccountApi.status === 'LoggedIn') {
await loadSchema(context, client);
}
}));
@ -105,7 +124,7 @@ async function loadSchema(
workspaceFolder?: vscode.WorkspaceFolder): Promise<void> {
if (workspaceFolder === undefined) {
const textDocument = vscode.window.activeTextEditor?.document;
if (textDocument?.languageId !== 'azure-pipelines') {
if (textDocument?.languageId !== LANGUAGE_IDENTIFIER) {
return;
}
@ -133,10 +152,7 @@ function getServerOptions(context: vscode.ExtensionContext): languageclient.Serv
function getClientOptions(): languageclient.LanguageClientOptions {
return {
// Register the server for Azure Pipelines documents
documentSelector: [
{ language: 'azure-pipelines', scheme: 'file' },
{ language: 'azure-pipelines', scheme: 'untitled' }
],
documentSelector: DOCUMENT_SELECTOR,
synchronize: {
// TODO: Switch to handling the workspace/configuration request
configurationSection: ['yaml', 'http.proxy', 'http.proxyStrictSSL'],

Просмотреть файл

@ -33,14 +33,20 @@ export async function locateSchemaFile(
// TODO: Support auto-detection for Azure Pipelines files outside of the workspace.
if (workspaceFolder !== undefined) {
try {
logger.log(`Detecting schema for workspace folder ${workspaceFolder.name}`, 'SchemaDetection');
schemaUri = await autoDetectSchema(context, workspaceFolder);
if (schemaUri) {
logger.log(
`Detected schema for workspace folder ${workspaceFolder.name}: ${schemaUri.path}`,
'SchemaDetection');
return schemaUri.path;
}
} catch (error) {
// Well, we tried our best. Fall back to the predetermined schema paths.
// TODO: Re-throw error once we're more confident in the schema detection.
logger.log(`Error auto-detecting schema: ${error}`, 'SchemaAutoDetectError');
logger.log(
`Error auto-detecting schema for workspace folder ${workspaceFolder.name}: ${error}`,
'SchemaDetection');
}
}
@ -60,6 +66,10 @@ export async function locateSchemaFile(
schemaUri = vscode.Uri.file(path.join(context.extensionPath, 'service-schema.json'));
}
logger.log(
`Using hardcoded schema for workspace folder ${workspaceFolder.name}: ${schemaUri.path}`,
'SchemaDetection');
// TODO: We should update getSchemaAssociations so we don't need to constantly
// notify the server of a "new" schema when in reality we're simply updating
// associations -- which is exactly what getSchemaAssociations is there for!
@ -83,7 +93,16 @@ async function autoDetectSchema(
context: vscode.ExtensionContext,
workspaceFolder: vscode.WorkspaceFolder): Promise<vscode.Uri | undefined> {
const azureAccountApi = await getAzureAccountExtensionApi();
if (!(await azureAccountApi.waitForLogin())) {
// We could care less about the subscriptions; all we need are the sessions.
// However, there's no waitForSessions API, and waitForLogin returns before
// the underlying account information is guaranteed to finish loading.
// The next-best option is then waitForSubscriptions which, by definition,
// can't return until the sessions are also available.
// This only returns false if there is no login.
if (!(await azureAccountApi.waitForSubscriptions())) {
logger.log(`Waiting for login`, 'SchemaDetection');
// Don't await this message so that we can return the fallback schema instead of blocking.
// We'll detect the login in extension.ts and then re-request the schema.
vscode.window.showInformationMessage(Messages.signInForEnhancedIntelliSense, Messages.signInLabel)
@ -103,23 +122,32 @@ async function autoDetectSchema(
// Get the remote URL if we're in a Git repo.
let remoteUrl: string | undefined;
const gitExtension = await getGitExtensionApi();
// Use openRepository because it's possible the Git extension hasn't
// finished opening all the repositories yet, and thus getRepository
// may return null if an Azure Pipelines file is open on startup.
const repo = await gitExtension.openRepository(workspaceFolder.uri);
if (repo !== null) {
await repo.status();
if (repo.state.HEAD?.upstream !== undefined) {
const remoteName = repo.state.HEAD.upstream.remote;
remoteUrl = repo.state.remotes.find(remote => remote.name === remoteName)?.fetchUrl;
try {
const gitExtension = await getGitExtensionApi();
// Use openRepository because it's possible the Git extension hasn't
// finished opening all the repositories yet, and thus getRepository
// may return null if an Azure Pipelines file is open on startup.
const repo = await gitExtension.openRepository(workspaceFolder.uri);
if (repo !== null) {
await repo.status();
if (repo.state.HEAD?.upstream !== undefined) {
const remoteName = repo.state.HEAD.upstream.remote;
remoteUrl = repo.state.remotes.find(remote => remote.name === remoteName)?.fetchUrl;
logger.log(`Found remote URL for ${workspaceFolder.name}: ${remoteUrl}`, 'SchemaDetection');
}
}
} catch (error) {
// Log and that's it - perhaps they're not in a Git repo, and so don't have the Git extension enabled.
logger.log(`${workspaceFolder.name} has no remote URLs: ${error}`, 'SchemaDetection');
}
let organizationName: string;
let session: AzureSession | undefined;
if (remoteUrl !== undefined && AzureDevOpsHelper.isAzureReposUrl(remoteUrl)) {
logger.log(`${workspaceFolder.name} is an Azure repo`, 'SchemaDetection');
// If we're in an Azure repo, we can silently determine the organization name and session.
organizationName = AzureDevOpsHelper.getRepositoryDetailsFromRemoteUrl(remoteUrl).organizationName;
for (const azureSession of azureAccountApi.sessions) {
@ -131,6 +159,8 @@ async function autoDetectSchema(
}
}
} else {
logger.log(`${workspaceFolder.name} has no remote URL or is not an Azure repo`, 'SchemaDetection');
const azurePipelinesDetails = context.workspaceState.get<{
[folder: string]: { organization: string; tenant: string; }
}>('azurePipelinesDetails');
@ -139,7 +169,13 @@ async function autoDetectSchema(
const details = azurePipelinesDetails[workspaceFolder.name];
organizationName = details.organization;
session = azureAccountApi.sessions.find(session => session.tenantId === details.tenant);
logger.log(
`Using cached information for ${workspaceFolder.name}: ${organizationName}, ${session.tenantId}`,
'SchemaDetection');
} else {
logger.log(`Prompting for organization for ${workspaceFolder.name}`, 'SchemaDetection');
// Otherwise, we need to manually prompt.
// We do this by asking them to select an organization via an information message,
// then displaying the quick pick of all the organizations they have access to.
@ -157,7 +193,6 @@ async function autoDetectSchema(
>(async resolve => {
const organizationAndSessions: QuickPickItemWithData<AzureSession>[] = [];
// FIXME: azureAccountApi.sessions changes under us. Why?
for (const azureSession of azureAccountApi.sessions) {
const organizationsClient = new OrganizationsClient(azureSession.credentials2);
const organizations = await organizationsClient.listOrganizations();
@ -200,6 +235,7 @@ async function autoDetectSchema(
// Not logged into an account that has access.
if (session === undefined) {
logger.log(`No organization found for ${workspaceFolder.name}`, 'SchemaDetection');
vscode.window.showErrorMessage(format(Messages.unableToAccessOrganization, organizationName));
return undefined;
}
@ -216,9 +252,12 @@ async function autoDetectSchema(
// hit the network to request an updated schema for an organization once per session.
const schemaUri = Utils.joinPath(context.globalStorageUri, `${organizationName}-schema.json`);
if (seenOrganizations.has(organizationName)) {
logger.log(`Returning cached schema for ${workspaceFolder.name}`, 'SchemaDetection');
return schemaUri;
}
logger.log(`Retrieving schema for ${workspaceFolder.name}`, 'SchemaDetection');
const token = await session.credentials2.getToken();
const authHandler = azdev.getBearerHandler(token.accessToken);
const azureDevOpsClient = new azdev.WebApi(`https://dev.azure.com/${organizationName}`, authHandler);