diff --git a/package.json b/package.json index 46c65076..e28fd225 100644 --- a/package.json +++ b/package.json @@ -205,6 +205,10 @@ "command": "vscode-objectscript.explorer.project.refresh", "when": "vscode-objectscript.connectActive" }, + { + "command": "vscode-objectscript.ccs.activateNamespaceConnections", + "when": "workspaceFolderCount != 0" + }, { "command": "vscode-objectscript.serverCommands.sourceControl", "when": "(vscode-objectscript.connectActive && resourceScheme == isfs) || (vscode-objectscript.connectActive && !editorIsOpen && virtualWorkspace =~ /^isfs(-readonly)?$/)" @@ -384,6 +388,11 @@ "when": "view == ObjectScriptExplorer", "group": "navigation" }, + { + "command": "vscode-objectscript.ccs.activateNamespaceConnections", + "when": "view == ObjectScriptExplorer", + "group": "navigation" + }, { "command": "vscode-objectscript.explorer.project.refresh", "when": "view == ObjectScriptProjectsExplorer", @@ -1082,6 +1091,12 @@ "category": "ObjectScript", "icon": "$(refresh)" }, + { + "command": "vscode-objectscript.ccs.activateNamespaceConnections", + "title": "Reativar Conexões de Namespaces", + "category": "ObjectScript", + "icon": "$(plug)" + }, { "category": "ObjectScript", "command": "vscode-objectscript.jumpToTagAndOffset", diff --git a/src/ccs/connectionActivator.ts b/src/ccs/connectionActivator.ts new file mode 100644 index 00000000..b169116c --- /dev/null +++ b/src/ccs/connectionActivator.ts @@ -0,0 +1,170 @@ +import axios from "axios"; +import * as httpsModule from "https"; +import * as vscode from "vscode"; + +// Inline dos schemas ISFS para evitar dependência circular com utils/extension +const ISFS_SCHEMES = ["isfs", "isfs-readonly"]; + +function isClientSideFolder(uri: vscode.Uri): boolean { + return !ISFS_SCHEMES.includes(uri.scheme); +} + +function getFolderConn(folderUri: vscode.Uri): vscode.WorkspaceConfiguration { + return vscode.workspace.getConfiguration("objectscript", folderUri); +} + +function getConnValue(folderConfig: vscode.WorkspaceConfiguration): Record | undefined { + const inspect = folderConfig.inspect("conn"); + return (inspect?.workspaceFolderValue ?? inspect?.workspaceValue) as Record | undefined; +} + +function getConnTarget(folderConfig: vscode.WorkspaceConfiguration): vscode.ConfigurationTarget { + return folderConfig.inspect("conn")?.workspaceFolderValue + ? vscode.ConfigurationTarget.WorkspaceFolder + : vscode.ConfigurationTarget.Workspace; +} + +/** + * Testa se o servidor responde fazendo uma requisição HTTP direta. + * Qualquer resposta HTTP (incluindo 401) indica que o servidor está ativo. + */ +async function isServerReachable(host: string, port: number, secure: boolean, pathPrefix: string): Promise { + if (!host || !port) return false; + + const proto = secure ? "https" : "http"; + let prefix = (pathPrefix ?? "").trim(); + if (prefix.length && !prefix.startsWith("/")) prefix = "/" + prefix; + const url = `${proto}://${host}:${port}${prefix}/api/atelier`; + + try { + const strictSSL = vscode.workspace.getConfiguration("http").get("proxyStrictSSL") ?? true; + const httpsAgent = new httpsModule.Agent({ rejectUnauthorized: strictSSL }); + await axios.get(url, { + httpsAgent, + timeout: 5000, + validateStatus: (status) => status < 500, + }); + return true; + } catch { + return false; + } +} + +/** + * Ativa os workspace folders inativos que usam o mesmo servidor (host:port) + * do folder que acabou de conectar com sucesso. Chamado automaticamente após + * uma conexão bem-sucedida para restaurar os namespaces relacionados. + * Retorna a quantidade de folders ativados. + */ +export async function activateSiblingFolders(host: string, port: number): Promise { + const folders = vscode.workspace.workspaceFolders ?? []; + let count = 0; + + for (const folder of folders) { + if (!isClientSideFolder(folder.uri)) continue; + + const folderConfig = getFolderConn(folder.uri); + const connValue = getConnValue(folderConfig); + if (!connValue) continue; + + // Apenas folders no mesmo servidor que estão inativos + if (connValue.active !== false) continue; + if (connValue.host !== host || connValue.port !== port) continue; + + await folderConfig.update("conn", { ...connValue, active: true }, getConnTarget(folderConfig)); + count++; + } + + return count; +} + +export interface ReactivationResult { + success: boolean; + activatedCount: number; + activatedFolderUris: vscode.Uri[]; + errorMessage?: string; +} + +/** + * Chamado pelo botão manual no Explorer. Valida conectividade com o servidor + * antes de reativar todos os workspace folders com conexão direta inativa. + */ +export async function reactivateNamespaceConnections(): Promise { + const allFolders = (vscode.workspace.workspaceFolders ?? []).filter((f) => isClientSideFolder(f.uri)); + + if (!allFolders.length) { + return { + success: false, + activatedCount: 0, + activatedFolderUris: [], + errorMessage: "Nenhum workspace folder local encontrado.", + }; + } + + // Mapeia servidores únicos e lista os folders inativos + const serverMap = new Map(); + const inactiveFolders: Array<{ folder: vscode.WorkspaceFolder; connValue: Record }> = []; + + for (const folder of allFolders) { + const folderConfig = getFolderConn(folder.uri); + const connValue = getConnValue(folderConfig); + if (!connValue?.host || !connValue?.port) continue; + + const key = `${connValue.host}:${connValue.port}`; + if (!serverMap.has(key)) { + serverMap.set(key, { + host: connValue.host, + port: connValue.port, + secure: connValue.https ?? false, + pathPrefix: connValue.pathPrefix ?? "", + }); + } + + if (connValue.active === false) { + inactiveFolders.push({ folder, connValue }); + } + } + + if (!serverMap.size) { + return { + success: false, + activatedCount: 0, + activatedFolderUris: [], + errorMessage: "Nenhuma configuração de servidor encontrada nos workspace folders.", + }; + } + + if (!inactiveFolders.length) { + return { success: true, activatedCount: 0, activatedFolderUris: [] }; + } + + // Valida quais servidores estão acessíveis + const reachableKeys = new Set(); + for (const [key, info] of serverMap) { + if (await isServerReachable(info.host, info.port, info.secure, info.pathPrefix)) { + reachableKeys.add(key); + } + } + + if (!reachableKeys.size) { + return { + success: false, + activatedCount: 0, + activatedFolderUris: [], + errorMessage: "Não foi possível conectar ao servidor. Verifique se o servidor está acessível e tente novamente.", + }; + } + + // Ativa os folders inativos dos servidores alcançáveis + const activatedFolderUris: vscode.Uri[] = []; + for (const { folder, connValue } of inactiveFolders) { + const key = `${connValue.host}:${connValue.port}`; + if (!reachableKeys.has(key)) continue; + + const folderConfig = getFolderConn(folder.uri); + await folderConfig.update("conn", { ...connValue, active: true }, getConnTarget(folderConfig)); + activatedFolderUris.push(folder.uri); + } + + return { success: true, activatedCount: activatedFolderUris.length, activatedFolderUris }; +} diff --git a/src/ccs/index.ts b/src/ccs/index.ts index 8a2ae60d..d4c4b3bc 100644 --- a/src/ccs/index.ts +++ b/src/ccs/index.ts @@ -33,3 +33,5 @@ export { createItem } from "./commands/createItem"; export { convertCurrentItem, convertCurrentItemCustom, convertCurrentItemOnSave } from "./commands/converterItem"; export { analizarVersaoItem } from "./commands/analizarVersaoItem"; export { atualizarConfiguracoes } from "./commands/atualizarConfiguracoes"; +export { activateSiblingFolders, reactivateNamespaceConnections } from "./connectionActivator"; +export type { ReactivationResult } from "./connectionActivator"; diff --git a/src/extension.ts b/src/extension.ts index aa74bc95..3ede4b42 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -172,6 +172,8 @@ import { convertCurrentItemOnSave, analizarVersaoItem, atualizarConfiguracoes, + activateSiblingFolders, + reactivateNamespaceConnections, } from "./ccs"; const packageJson = vscode.extensions.getExtension(extensionId).packageJSON; @@ -476,7 +478,10 @@ export async function checkConnection( panel.tooltip = new vscode.MarkdownString(`Connected as \`${username}\``); } inactiveServerIds.delete(api.serverId); - if (!api.externalServer) await setConnectionState(configName, true); + if (!api.externalServer) { + await setConnectionState(configName, true); + await activateSiblingFolders(host, port); + } return; }; @@ -1474,6 +1479,75 @@ export async function activate(context: vscode.ExtensionContext): Promise { sendCommandTelemetryEvent("explorer.project.refresh"); projectsExplorerProvider.refresh(); }), + vscode.commands.registerCommand("vscode-objectscript.ccs.activateNamespaceConnections", async () => { + sendCommandTelemetryEvent("ccs.activateNamespaceConnections"); + await vscode.window.withProgress( + { + location: vscode.ProgressLocation.Notification, + title: "Reativando conexões de namespaces...", + cancellable: false, + }, + async () => { + const result = await reactivateNamespaceConnections(); + if (!result.success) { + vscode.window.showErrorMessage(result.errorMessage ?? "Falha ao reativar as conexões de namespaces."); + return; + } + if (result.activatedFolderUris.length === 0) { + vscode.window.showInformationMessage("Todas as conexões de namespace já estão ativas."); + return; + } + + // Valida cada folder reativado com AtelierAPI antes de confirmar o sucesso + let successCount = 0; + const failureMessages: string[] = []; + + for (const folderUri of result.activatedFolderUris) { + const { apiTarget, configName } = connectionTarget(folderUri); + const api = new AtelierAPI(apiTarget, false); + try { + await api.serverInfo(true, 5000); + successCount++; + } catch (error: any) { + // Reverte: a conexão não está realmente funcional + await setConnectionState(configName, false); + + // Monta mensagem descritiva do problema + const label = configName || folderUri.fsPath; + let reason: string; + if (!error || (typeof error === "object" && Object.keys(error).length === 0)) { + reason = "configurações inválidas — verifique host, porta e namespace"; + } else if (error?.code === "WrongNamespace") { + reason = `namespace '${api.config.ns}' não encontrado no servidor`; + } else if (error?.statusCode === 401 || error?.statusCode === 403) { + reason = "credenciais inválidas — verifique usuário e senha"; + } else if (["ECONNREFUSED", "ENOTFOUND", "EADDRNOTAVAIL"].includes(error?.code)) { + reason = "servidor inacessível — verifique host e porta"; + } else if (["ECONNABORTED", "ERR_CANCELED", "ETIMEDOUT"].includes(error?.code)) { + reason = "tempo limite excedido — servidor não respondeu"; + } else { + reason = error?.message ?? "erro desconhecido"; + } + failureMessages.push(`• ${label}: ${reason}`); + } + } + + if (successCount === 0) { + vscode.window.showErrorMessage( + `Não foi possível reativar as conexões de namespace. Verifique as configurações:\n${failureMessages.join("\n")}` + ); + } else if (failureMessages.length > 0) { + vscode.window.showWarningMessage( + `${successCount} namespace${successCount !== 1 ? "s" : ""} reativado${successCount !== 1 ? "s" : ""} com sucesso. ${failureMessages.length} com erro:\n${failureMessages.join("\n")}` + ); + } else { + vscode.window.showInformationMessage( + `${successCount} namespace${successCount !== 1 ? "s" : ""} reativado${successCount !== 1 ? "s" : ""} com sucesso.` + ); + } + } + ); + }), // Register the vscode-objectscript.explorer.open command elsewhere registerExplorerOpen(), vscode.commands.registerCommand("vscode-objectscript.explorer.export", (item, items) => {