diff --git a/deadlock-plugins/deadlock-extension/src/config.ts b/deadlock-plugins/deadlock-extension/src/config.ts index 21960df6fea4d8e530bc89caaf217a4219f82ab8..fcbe08addba35d99f5fd64632720c89235514ed1 100644 --- a/deadlock-plugins/deadlock-extension/src/config.ts +++ b/deadlock-plugins/deadlock-extension/src/config.ts @@ -4,7 +4,6 @@ export const KEYCLOAK_TOKEN_CREATE_URL = 'https://auth.dev.deadlock.io/auth/realms/Deadlock/protocol/openid-connect/token'; export const KEYCLOAK_USER_INFO_URL = 'https://auth.dev.deadlock.io/auth/realms/Deadlock/protocol/openid-connect/userinfo'; -export const REGISTRY_MISSION_URL = 'registry.takima.io/deadlock/deadlock-challenges'; export const REJECT_UNAUTHORIZED = false; export const ENABLE_AUTOMATIC_SAVE = true; export const ENABLE_RECORDER_HTTP_SERVER = false; diff --git a/deadlock-plugins/deadlock-extension/src/core/callApi.service.ts b/deadlock-plugins/deadlock-extension/src/core/callApi.service.ts index da086311594036b8ad2bc2bc9b27dd064ae4449b..e82c9df4d800e5495c415114400881ad83d1c021 100644 --- a/deadlock-plugins/deadlock-extension/src/core/callApi.service.ts +++ b/deadlock-plugins/deadlock-extension/src/core/callApi.service.ts @@ -1,43 +1,44 @@ -import axios, { AxiosInstance } from 'axios'; -import { API_QUERY_REFERER, API_URL } from '../config'; -import { GiteaPublicProperties } from '../model/giteaPublicProperties.model'; -import { SshKeyPair } from '../model/sshKeyPair.model'; -import { User } from '../model/user.model'; +import axios, {AxiosInstance} from 'axios'; +import {API_QUERY_REFERER, API_URL} from '../config'; +import {GiteaPublicProperties} from '../model/giteaPublicProperties.model'; +import {SshKeyPair} from '../model/sshKeyPair.model'; +import {User} from '../model/user.model'; import Controller from './controller'; import ExtensionStore from './extensionStore'; import KeycloakOAuth2DeviceFlowConnection from './keycloakOAuth2DeviceFlowConnection'; export default class CallApiService { - private callApi: AxiosInstance; - constructor( - private keycloackConnection: KeycloakOAuth2DeviceFlowConnection, - private extensionStore: ExtensionStore, - private controller: Controller, - ) { - this.callApi = axios.create({ - baseURL: API_URL, - headers: { - 'Content-Type': 'application/json', - referer: API_QUERY_REFERER, - }, - }); + private axiosInstance: AxiosInstance; - this.initApiInterceptor(); - } + constructor( + private keycloackConnection: KeycloakOAuth2DeviceFlowConnection, + private extensionStore: ExtensionStore, + private controller: Controller, + ) { + this.axiosInstance = axios.create({ + baseURL: API_URL, + headers: { + 'Content-Type': 'application/json', + referer: API_QUERY_REFERER, + }, + }); - initApiInterceptor() { - this.callApi.interceptors.request.use( - async (config) => { - let accessToken = await this.extensionStore.getAccessToken(); - if (accessToken && config.headers) { - config.headers['authorization'] = `BEARER ${accessToken}`; - } - return config; - }, - (error) => { - return Promise.reject(error); - }, - ); + this.initApiInterceptor(); + } + + initApiInterceptor() { + this.axiosInstance.interceptors.request.use( + async (config) => { + let accessToken = await this.extensionStore.getAccessToken(); + if (accessToken && config.headers) { + config.headers['authorization'] = `BEARER ${accessToken}`; + } + return config; + }, + (error) => { + return Promise.reject(error); + }, + ); this.callApi.interceptors.response.use( (res) => { @@ -88,15 +89,15 @@ export default class CallApiService { ); } - getGiteaPublicProperties(): Promise<GiteaPublicProperties> { - return this.callApi.get<GiteaPublicProperties>(`gitea`).then((res) => res.data); - } + getGiteaPublicProperties(): Promise<GiteaPublicProperties> { + return this.axiosInstance.get<GiteaPublicProperties>(`gitea`).then((res) => res.data); + } - getUserSshKey(): Promise<SshKeyPair> { - return this.callApi.put<SshKeyPair>(`users/gitea/keypair`).then((res) => res.data); - } + getUserSshKey(): Promise<SshKeyPair> { + return this.axiosInstance.put<SshKeyPair>(`users/gitea/keypair`).then((res) => res.data); + } - getUser(): Promise<User> { - return this.callApi.get<User>(`auth`).then((res) => res.data); - } + getUser(): Promise<User> { + return this.axiosInstance.get<User>(`auth`).then((res) => res.data); + } } diff --git a/deadlock-plugins/deadlock-extension/src/core/controller.ts b/deadlock-plugins/deadlock-extension/src/core/controller.ts index e2435100647345293570d8be520c03acbc59b442..a628c1ca01b275a1062ccb16165fba59357a0833 100644 --- a/deadlock-plugins/deadlock-extension/src/core/controller.ts +++ b/deadlock-plugins/deadlock-extension/src/core/controller.ts @@ -1,119 +1,128 @@ import * as vscode from 'vscode'; import { - KEYCLOAK_DEVICE_AUTH_URL, - KEYCLOAK_TOKEN_CREATE_URL, - KEYCLOAK_USER_INFO_URL, - REGISTRY_MISSION_URL, + KEYCLOAK_DEVICE_AUTH_URL, + KEYCLOAK_TOKEN_CREATE_URL, + KEYCLOAK_USER_INFO_URL, + REGISTRY_MISSION_URL, } from '../config'; -import { log } from '../recorder/utils'; -import { OPEN_QUICK_SETUP_COMMAND } from '../theia/command'; +import {log} from '../recorder/utils'; +import {OPEN_QUICK_SETUP_COMMAND} from '../theia/command'; import BriefingView from '../view/briefingView'; import QuickSetupView from '../view/quickSetupView'; -import { CHOOSE_MISSION_WORKDIR_COMMAND, CommandHandler, OPEN_URL_IN_BROWSER_COMMAND } from './commandHandler'; +import {CHOOSE_MISSION_WORKDIR_COMMAND, CommandHandler, OPEN_URL_IN_BROWSER_COMMAND} from './commandHandler'; import ExtensionStore from './extensionStore'; import KeycloakOAuth2DeviceFlowConnection from './keycloakOAuth2DeviceFlowConnection'; -import Mission from './mission'; +import Mission from './mission/mission'; import CallApiService from './callApi.service'; import KeycloakOAuth2DeviceFlowConnectionVSCodeImpl from './keycloakOAuth2DeviceFlowConnectionVSCodeImpl'; import SshKeyManager from './sshKeyManager'; -import { GiteaPublicProperties } from '../model/giteaPublicProperties.model'; -import { User } from '../model/user.model'; +import {GiteaPublicProperties} from '../model/giteaPublicProperties.model'; +import {User} from '../model/user.model'; export default class Controller { - public connection: KeycloakOAuth2DeviceFlowConnection; - private commandHandler: CommandHandler; - private briefingView: BriefingView; - private quickSetupView: QuickSetupView; - private extensionStore: ExtensionStore; - private callApiService: CallApiService; - private sshKeyManager: SshKeyManager; - - constructor(private context: vscode.ExtensionContext) { - this.extensionStore = ExtensionStore.getInstance(context); - this.briefingView = new BriefingView(); - this.quickSetupView = new QuickSetupView(context.extensionUri); - this.commandHandler = new CommandHandler(this); - this.connection = new KeycloakOAuth2DeviceFlowConnectionVSCodeImpl( - KEYCLOAK_DEVICE_AUTH_URL, - KEYCLOAK_TOKEN_CREATE_URL, - KEYCLOAK_USER_INFO_URL, - ); - - this.callApiService = new CallApiService(this.connection, this.extensionStore, this); - this.sshKeyManager = new SshKeyManager(); - - this.init(); - } - private async init() { - const that = this; - vscode.window.registerUriHandler({ - handleUri(uri: vscode.Uri) { - const queryParams: URLSearchParams = new URLSearchParams(uri.query); - const action: string | null = queryParams.get('action'); - log('Opening link', uri); - - switch (action) { - case 'open-challenge': - that.launchMission(queryParams.get('missionId'), queryParams.get('missionVersion')); - break; - - default: - vscode.window.showInformationMessage('Aucune action trouvée!'); + public connection: KeycloakOAuth2DeviceFlowConnection; + private commandHandler: CommandHandler; + private briefingView: BriefingView; + private quickSetupView: QuickSetupView; + private extensionStore: ExtensionStore; + private callApiService: CallApiService; + private sshKeyManager: SshKeyManager; + + constructor(private context: vscode.ExtensionContext) { + this.extensionStore = ExtensionStore.getInstance(context); + this.briefingView = new BriefingView(); + this.quickSetupView = new QuickSetupView(context.extensionUri); + this.commandHandler = new CommandHandler(this); + this.connection = new KeycloakOAuth2DeviceFlowConnectionVSCodeImpl( + KEYCLOAK_DEVICE_AUTH_URL, + KEYCLOAK_TOKEN_CREATE_URL, + KEYCLOAK_USER_INFO_URL, + ); + + this.callApiService = new CallApiService(this.connection, this.extensionStore, this); + this.sshKeyManager = new SshKeyManager(); + + this.init(); + } + + private async init() { + const that = this; + vscode.window.registerUriHandler({ + handleUri(uri: vscode.Uri) { + const queryParams = new URLSearchParams(uri.query); + const action = queryParams.get('action'); + log('Opening link', uri); + + switch (action) { + case 'open-challenge': + const missionId = queryParams.get('missionId'); + const missionVersion = queryParams.get('missionVersion'); + if (!missionId || !missionVersion) { + //display smth + } + that.launchMission(queryParams.get('missionId'), queryParams.get('missionVersion')); + break; + + default: + vscode.window.showInformationMessage('Aucune action trouvée!'); + } + }, + }); + + const exensionStorage = ExtensionStore.getInstance(); + this.quickSetupView.isAlreadyConnected = !!(await exensionStorage.getAccessToken()); + } + + async chooseMissionWorkdir() { + const actualMissionWorkDir = this.extensionStore.getMissionWorkdir(); + + const folderUri = await vscode.window.showOpenDialog({ + defaultUri: actualMissionWorkDir ? vscode.Uri.file(actualMissionWorkDir) : undefined, + canSelectFolders: true, + canSelectFiles: false, + title: 'Choisis le dossier qui contiendra tes missions', + }); + + if (!folderUri) { + if (this.extensionStore.getMissionWorkdir()) { + return; + } + this.chooseMissionWorkdir(); + } else { + this.extensionStore.setMissionWorkdir(folderUri[0].path); } - }, - }); - - const exensionStorage = ExtensionStore.getInstance(); - this.quickSetupView.isAlreadyConnected = !!(await exensionStorage.getAccessToken()); - } - async chooseMissionWorkdir() { - const actualMissionWorkDir = this.extensionStore.getMissionWorkdir(); - - const folderUri = await vscode.window.showOpenDialog({ - defaultUri: actualMissionWorkDir ? vscode.Uri.file(actualMissionWorkDir) : undefined, - canSelectFolders: true, - canSelectFiles: false, - title: 'Choisis le dossier qui contiendra tes missions', - }); - - if (!folderUri) { - if (this.extensionStore.getMissionWorkdir()) { - return; - } - this.chooseMissionWorkdir(); - } else { - this.extensionStore.setMissionWorkdir(folderUri[0].path); } - } - - public async clear() { - const exensionStorage = ExtensionStore.getInstance(); - await exensionStorage.clear(); - this.quickSetupView.isAlreadyConnected = false; - } - - public async createSshKeyPairIfNotExist() { - if (this.sshKeyManager.isSshKeyPairExist()) return; - const { publicKey, privateKey } = await this.callApiService.getUserSshKey(); - this.sshKeyManager.createSshKeyFiles(publicKey, privateKey); - } - - public async authenticate() { - // WARN generate a new device code every time student clicks on log in button. Should I keep it ?\ - // The answer might be 'yes' because when the student is already authenticated, the log in button should be disabled. - await this.connection.registerDevice(); - const tokens = await this.connection.getToken({ openLink: Controller.openBrowserWithUrl }); - await this.extensionStore.setAccessToken(tokens.accessToken); - await this.extensionStore.setRefreshToken(tokens.refreshToken); - await this.createSshKeyPairIfNotExist(); - this.quickSetupView.isAlreadyConnected = true; - } - public static openBrowserWithUrl(url: string) { - vscode.commands.executeCommand(OPEN_URL_IN_BROWSER_COMMAND.cmd, vscode.Uri.parse(url)); - } - - public async launchMission(missionId: string | null, missionVersion: string | null) { + + public async clear() { + const exensionStorage = ExtensionStore.getInstance(); + await exensionStorage.clear(); + this.quickSetupView.isAlreadyConnected = false; + } + + public async createSshKeyPairIfNotExist() { + if (this.sshKeyManager.isSshKeyPairExist()) return; + + const {publicKey, privateKey} = await this.callApiService.getUserSshKey(); + await this.sshKeyManager.createSshKeyFiles(publicKey, privateKey); + } + + public async authenticate() { + // WARN generate a new device code every time student clicks on log in button. Should I keep it ?\ + // The answer might be 'yes' because when the student is already authenticated, the log in button should be disabled. + await this.connection.registerDevice(); + const tokens = await this.connection.getToken({openLink: Controller.openBrowserWithUrl}); + await this.extensionStore.setAccessToken(tokens.accessToken); + await this.extensionStore.setRefreshToken(tokens.refreshToken); + await this.createSshKeyPairIfNotExist(); + this.quickSetupView.isAlreadyConnected = true; + } + + public static openBrowserWithUrl(url: string) { + vscode.commands.executeCommand(OPEN_URL_IN_BROWSER_COMMAND.cmd, vscode.Uri.parse(url)); + } + + public async launchMission(missionId: string, missionVersion: string) { console.log('LAUCH MISSION : ' + missionId); if (missionId && missionVersion) { vscode.window.showInformationMessage(`vous lancez la mission ${missionId}`); @@ -122,37 +131,34 @@ export default class Controller { await vscode.commands.executeCommand(CHOOSE_MISSION_WORKDIR_COMMAND.cmd); } - const hadBeenConnected = (await this.extensionStore.getAccessToken()) !== undefined; + const hadBeenConnected = (await this.extensionStore.getAccessToken()) !== undefined; - if (!hadBeenConnected) { - await this.authenticate(); - vscode.window.showInformationMessage('Nouvelle connexion validée'); - } else { - vscode.window.showInformationMessage('Déjà connecté: session récupérée'); - } + if (!hadBeenConnected) { + await this.authenticate(); + vscode.window.showInformationMessage('Connexion validée'); + } - const user: User = await this.callApiService.getUser(); - const giteaPublicProperties: GiteaPublicProperties = await this.callApiService.getGiteaPublicProperties(); - // TODO Should I fetch GET api/missions/{missionId} one day instead of passing necessary parameters in vscode xdg-open link ? - const mission = new Mission( - { - registryBaseURL: REGISTRY_MISSION_URL, - missionId: missionId, - missionVersion: missionVersion, - }, - user, - giteaPublicProperties, - ); - vscode.window.showInformationMessage( - 'opening inside folder ' + this.extensionStore.getMissionWorkdir()! + '/' + missionId, - ); - console.log('BEFORE SETUP'); - await mission.setup({}); - console.log('BEFORE open editor'); - - await mission.openEditorInFolder(); - - vscode.commands.executeCommand(OPEN_QUICK_SETUP_COMMAND.cmd); + const user: User = await this.callApiService.getUser(); + const giteaPublicProperties: GiteaPublicProperties = await this.callApiService.getGiteaPublicProperties(); + // TODO Should I fetch GET api/missions/{missionId} one day instead of passing necessary parameters in vscode xdg-open link ? + const mission = new Mission( + { + registryBaseURL: REGISTRY_MISSION_URL, + missionId: missionId, + missionVersion: missionVersion, + }, + user, + giteaPublicProperties, + ); + vscode.window.showInformationMessage( + 'opening inside folder ' + this.extensionStore.getMissionWorkdir()! + '/' + missionId, + ); + console.log('BEFORE SETUP'); + await mission.setup(); + console.log('BEFORE open editor'); + + await mission.openEditorInFolder(); + + vscode.commands.executeCommand(OPEN_QUICK_SETUP_COMMAND.cmd); } - } } diff --git a/deadlock-plugins/deadlock-extension/src/core/mission/DevContainer.ts b/deadlock-plugins/deadlock-extension/src/core/mission/DevContainer.ts new file mode 100644 index 0000000000000000000000000000000000000000..c1e4f8aad32a6b00cf6609a4a3e30e6d5ac4c034 --- /dev/null +++ b/deadlock-plugins/deadlock-extension/src/core/mission/DevContainer.ts @@ -0,0 +1,44 @@ +export interface DockerfileSpecific { + image?; + dockerFile?; + context?; + 'build.args'?; + 'build.target'?; + 'build.cacheFrom'?; + containerEnv?; + containerUser?; + mounts?; + workspaceMount?; + workspaceFolder?; + runArgs?; +} + +export interface Base { + name?; + forwardPorts?; + portsAttributes?; + otherPortsAttributes?; + remoteEnv?; + remoteUser?; + updateRemoteUserUID?; + userEnvProbe?; + overrideCommand?; + features?; + shutdownAction?; +} + +export interface VSCodespecific { + extensions?; + settings?; + devPort?; +} + +export interface LifecycleScripts { + initializeCommand?; + onCreateCommand?; + updateContentCommand?; + postCreateCommand?; + postStartCommand?; + postAttachCommand?; + waitFor?; +} diff --git a/deadlock-plugins/deadlock-extension/src/core/mission/MissionDevContainer.ts b/deadlock-plugins/deadlock-extension/src/core/mission/MissionDevContainer.ts new file mode 100644 index 0000000000000000000000000000000000000000..bcf4acfdfa455d08eaab939993b208d2cbe98a84 --- /dev/null +++ b/deadlock-plugins/deadlock-extension/src/core/mission/MissionDevContainer.ts @@ -0,0 +1,48 @@ +import {MissionResource} from "./Mission"; +import {userSshKeyFolderPath} from "../config"; +import ExtensionStore from "../extensionStore"; +import {DockerfileSpecific, LifecycleScripts, VSCodespecific} from "./DevContainer"; + +const DOCKER_IMAGE_URL = 'registry.takima.io/deadlock/deadlock-challenges'; + + +export function createDevContainerFile(mission: MissionResource) { + + const hostBaseWorkDir = ExtensionStore.getInstance().getMissionWorkdir() ?? ''; + const hostMissionDir = `${hostBaseWorkDir}/${mission.id}` + + return fs.promises.writeFile( + this.hostMissionDevcontainerFileDir, + (() => { + const devcontainer: Partial<DockerfileSpecific & Base & VSCodespecific & LifecycleScripts> = { + name: `deadlock-${this.params.missionId}`, + image: this.dockerImageURL, + extensions: ['Deadlock.deadlock-coding'], + remoteUser: 'deadlock', + mounts: [ + `source=${userSshKeyFolderPath},target=/tmp/.ssh,type=bind,consistency=cached,readonly`, + `source=${this.userMissionConfigPath},target=/home/config/,type=bind,consistency=cached,readonly`, + 'source=/etc/hosts,target=/etc/hosts,type=bind,consistency=cached,readonly', + ], + userEnvProbe: 'interactiveShell', + settings: { + 'terminal.integrated.defaultProfile.linux': 'bash', + 'terminal.integrated.profiles.linux': { + bash: { + path: '/bin/bash', + }, + }, + }, + overrideCommand: false, + shutdownAction: 'stopContainer', + workspaceMount: `source=${this.hostMissionMountDir},target=${this.remoteMissionDir},type=bind`, + workspaceFolder: `${this.remoteMissionDir}`, + onCreateCommand: `cp -R ${this.remoteGiteaWorkDir}/. ${this.remoteMissionDir}`, + runArgs: ['--privileged'], + ...options, + }; + return JSON.stringify(devcontainer, null, 2); + })(), + ); + +} diff --git a/deadlock-plugins/deadlock-extension/src/core/mission/MissionDocker.ts b/deadlock-plugins/deadlock-extension/src/core/mission/MissionDocker.ts new file mode 100644 index 0000000000000000000000000000000000000000..139597f9cb07c5d48bed18984ec4747f4b4f3438 --- /dev/null +++ b/deadlock-plugins/deadlock-extension/src/core/mission/MissionDocker.ts @@ -0,0 +1,2 @@ + + diff --git a/deadlock-plugins/deadlock-extension/src/core/mission/MissionResource.ts b/deadlock-plugins/deadlock-extension/src/core/mission/MissionResource.ts new file mode 100644 index 0000000000000000000000000000000000000000..dc3f3ca4c5eed56e8745f971ddab59d4ef4c0b78 --- /dev/null +++ b/deadlock-plugins/deadlock-extension/src/core/mission/MissionResource.ts @@ -0,0 +1,8 @@ + +export class MissionResource { + constructor(public missionDir: string, public id: string, public version: string) { + + } +} + + diff --git a/deadlock-plugins/deadlock-extension/src/core/mission.ts b/deadlock-plugins/deadlock-extension/src/core/mission/mission.ts similarity index 82% rename from deadlock-plugins/deadlock-extension/src/core/mission.ts rename to deadlock-plugins/deadlock-extension/src/core/mission/mission.ts index da765ccb2724f4dd0f1f04f3e7c6401fdbb22783..31a37f5ba7298373cf2839cc8068fe077541d4f2 100644 --- a/deadlock-plugins/deadlock-extension/src/core/mission.ts +++ b/deadlock-plugins/deadlock-extension/src/core/mission/mission.ts @@ -2,11 +2,11 @@ import { exec as _exec } from 'child_process'; import * as fs from 'fs'; import * as util from 'util'; import * as vscode from 'vscode'; -import { error as err, log } from '../recorder/utils'; -import ExtensionStore from './extensionStore'; -import { userSshKeyFolderPath } from './config'; -import { GiteaPublicProperties } from '../model/giteaPublicProperties.model'; -import { User, UserChallengeJson } from '../model/user.model'; +import { error as err, log } from '../../recorder/utils'; +import ExtensionStore from '../extensionStore'; +import { userSshKeyFolderPath } from '../config'; +import { GiteaPublicProperties } from '../../model/giteaPublicProperties.model'; +import { User, UserChallengeJson } from '../../model/user.model'; /** * {@link https://nodejs.org/api/child_process.html#child_process_child_process_exec_command_options_callback} @@ -47,12 +47,6 @@ export default class Mission { this.giteaPublicProperties = giteaPublicProperties; } - static async pullImage(url) { - const { stdout, stderr } = await exec(`docker pull ${url}`); - log(stdout); - err(stderr); - } - public async setup(options?: Partial<DockerfileSpecific & Base & VSCodespecific & LifecycleScripts>) { await fs.promises.mkdir(this.hostMissionDevcontainerDir, { recursive: true }); await fs.promises.mkdir(this.hostMissionMountDir, { recursive: true }); @@ -131,47 +125,4 @@ export default class Mission { export function getDockerImageURL(base, missionId, missionVersion) { return `${base}/${missionId}:${missionVersion}`; } -interface DockerfileSpecific { - image?; - dockerFile?; - context?; - 'build.args'?; - 'build.target'?; - 'build.cacheFrom'?; - containerEnv?; - containerUser?; - mounts?; - workspaceMount?; - workspaceFolder?; - runArgs?; -} - -interface Base { - name?; - forwardPorts?; - portsAttributes?; - otherPortsAttributes?; - remoteEnv?; - remoteUser?; - updateRemoteUserUID?; - userEnvProbe?; - overrideCommand?; - features?; - shutdownAction?; -} - -interface VSCodespecific { - extensions?; - settings?; - devPort?; -} -interface LifecycleScripts { - initializeCommand?; - onCreateCommand?; - updateContentCommand?; - postCreateCommand?; - postStartCommand?; - postAttachCommand?; - waitFor?; -} diff --git a/deadlock-plugins/deadlock-extension/src/extension.ts b/deadlock-plugins/deadlock-extension/src/extension.ts index e3f711319ec858062167cfa41d305f03a339eae5..1d049e8a7943e1de2d348d6fc643b33cd16e06fb 100644 --- a/deadlock-plugins/deadlock-extension/src/extension.ts +++ b/deadlock-plugins/deadlock-extension/src/extension.ts @@ -4,12 +4,10 @@ import { error } from './recorder/utils'; import { DepNodeProvider } from './theia/deadlockPanel'; import UserConfigTheia from './theia/userConfigTheia'; -export const userConfig = new UserConfigTheia(); export async function activate(context: vscode.ExtensionContext) { vscode.window.showInformationMessage('Bienvenue sur Deadlock!'); - - const controller = new Controller(context); + extensionContext = context; const workspaceFolders = vscode.workspace.workspaceFolders?.toString() ?? ''; if (!workspaceFolders) vscode.window.showInformationMessage('Pas de répertoires ouverts');