diff --git a/deadlock-plugins/deadlock-extension/package-lock.json b/deadlock-plugins/deadlock-extension/package-lock.json index fc5c9c6c2f51a9c1d827fe8bbf31a7445ac3f446..53a947b0a3226322d98da1bda1d99b69a478feea 100644 --- a/deadlock-plugins/deadlock-extension/package-lock.json +++ b/deadlock-plugins/deadlock-extension/package-lock.json @@ -8,6 +8,7 @@ "name": "deadlock-coding", "version": "0.1.10", "dependencies": { + "@types/tar": "^6.1.1", "@types/yaml": "^1.9.7", "@vscode/webview-ui-toolkit": "^1.0.0", "async": "^3.2.2", @@ -16,6 +17,7 @@ "marked": "^4.0.6", "node-fetch": "^2.6.7", "simple-git": "^3.7.0", + "tar": "^6.1.11", "yaml": "^2.1.1" }, "devDependencies": { @@ -1538,11 +1540,18 @@ "integrity": "sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==", "dev": true }, + "node_modules/@types/minipass": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@types/minipass/-/minipass-3.1.2.tgz", + "integrity": "sha512-foLGjgrJkUjLG/o2t2ymlZGEoBNBa/TfoUZ7oCTkOjP1T43UGBJspovJou/l3ZuHvye2ewR5cZNtp2zyWgILMA==", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/node": { "version": "14.18.21", "resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.21.tgz", - "integrity": "sha512-x5W9s+8P4XteaxT/jKF0PSb7XEvo5VmqEWgsMlyeY4ZlLK8I6aH6g5TPPyDlLAep+GYf4kefb7HFyc7PAO3m+Q==", - "dev": true + "integrity": "sha512-x5W9s+8P4XteaxT/jKF0PSb7XEvo5VmqEWgsMlyeY4ZlLK8I6aH6g5TPPyDlLAep+GYf4kefb7HFyc7PAO3m+Q==" }, "node_modules/@types/node-fetch": { "version": "2.6.1", @@ -1621,6 +1630,15 @@ "integrity": "sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==", "dev": true }, + "node_modules/@types/tar": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/@types/tar/-/tar-6.1.1.tgz", + "integrity": "sha512-8mto3YZfVpqB1CHMaYz1TUYIQfZFbh/QbEq5Hsn6D0ilCfqRVCdalmc89B7vi3jhl9UYIk+dWDABShNfOkv5HA==", + "dependencies": { + "@types/minipass": "*", + "@types/node": "*" + } + }, "node_modules/@types/vscode": { "version": "1.67.0", "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.67.0.tgz", @@ -4408,6 +4426,17 @@ "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", "dev": true }, + "node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/fs-monkey": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/fs-monkey/-/fs-monkey-1.0.3.tgz", @@ -6714,6 +6743,40 @@ "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==", "dev": true }, + "node_modules/minipass": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.3.tgz", + "integrity": "sha512-N0BOsdFAlNRfmwMhjAsLVWOk7Ljmeb39iqFlsV1At+jqRhSUP9yeof8FyJu4imaJiSUp8vQebWD/guZwGQC8iA==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/mkdirp-classic": { "version": "0.5.3", "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", @@ -8569,6 +8632,22 @@ "node": ">=6" } }, + "node_modules/tar": { + "version": "6.1.11", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.1.11.tgz", + "integrity": "sha512-an/KZQzQUkZCkuoAA64hM92X0Urb6VpRhAFllDzz44U2mcD5scmT3zBc4VgVpkugF580+DQn8eAFSyoQt0tznA==", + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^3.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 10" + } + }, "node_modules/tar-fs": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz", @@ -8611,6 +8690,14 @@ "node": ">= 6" } }, + "node_modules/tar/node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "engines": { + "node": ">=10" + } + }, "node_modules/terminal-link": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/terminal-link/-/terminal-link-2.1.1.tgz", @@ -9720,8 +9807,7 @@ "node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" }, "node_modules/yaml": { "version": "2.1.1", @@ -11034,11 +11120,18 @@ "integrity": "sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==", "dev": true }, + "@types/minipass": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@types/minipass/-/minipass-3.1.2.tgz", + "integrity": "sha512-foLGjgrJkUjLG/o2t2ymlZGEoBNBa/TfoUZ7oCTkOjP1T43UGBJspovJou/l3ZuHvye2ewR5cZNtp2zyWgILMA==", + "requires": { + "@types/node": "*" + } + }, "@types/node": { "version": "14.18.21", "resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.21.tgz", - "integrity": "sha512-x5W9s+8P4XteaxT/jKF0PSb7XEvo5VmqEWgsMlyeY4ZlLK8I6aH6g5TPPyDlLAep+GYf4kefb7HFyc7PAO3m+Q==", - "dev": true + "integrity": "sha512-x5W9s+8P4XteaxT/jKF0PSb7XEvo5VmqEWgsMlyeY4ZlLK8I6aH6g5TPPyDlLAep+GYf4kefb7HFyc7PAO3m+Q==" }, "@types/node-fetch": { "version": "2.6.1", @@ -11117,6 +11210,15 @@ "integrity": "sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==", "dev": true }, + "@types/tar": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/@types/tar/-/tar-6.1.1.tgz", + "integrity": "sha512-8mto3YZfVpqB1CHMaYz1TUYIQfZFbh/QbEq5Hsn6D0ilCfqRVCdalmc89B7vi3jhl9UYIk+dWDABShNfOkv5HA==", + "requires": { + "@types/minipass": "*", + "@types/node": "*" + } + }, "@types/vscode": { "version": "1.67.0", "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.67.0.tgz", @@ -13269,6 +13371,14 @@ "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", "dev": true }, + "fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "requires": { + "minipass": "^3.0.0" + } + }, "fs-monkey": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/fs-monkey/-/fs-monkey-1.0.3.tgz", @@ -15009,6 +15119,28 @@ "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==", "dev": true }, + "minipass": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.3.tgz", + "integrity": "sha512-N0BOsdFAlNRfmwMhjAsLVWOk7Ljmeb39iqFlsV1At+jqRhSUP9yeof8FyJu4imaJiSUp8vQebWD/guZwGQC8iA==", + "requires": { + "yallist": "^4.0.0" + } + }, + "minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "requires": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + } + }, + "mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==" + }, "mkdirp-classic": { "version": "0.5.3", "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", @@ -16444,6 +16576,26 @@ "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", "dev": true }, + "tar": { + "version": "6.1.11", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.1.11.tgz", + "integrity": "sha512-an/KZQzQUkZCkuoAA64hM92X0Urb6VpRhAFllDzz44U2mcD5scmT3zBc4VgVpkugF580+DQn8eAFSyoQt0tznA==", + "requires": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^3.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "dependencies": { + "chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==" + } + } + }, "tar-fs": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz", @@ -17263,8 +17415,7 @@ "yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" }, "yaml": { "version": "2.1.1", diff --git a/deadlock-plugins/deadlock-extension/package.json b/deadlock-plugins/deadlock-extension/package.json index 5cacb47740180ba5b093b73bc8699d9a4aa71598..a613b0827df752b672566f9b0e0a368950ab0019 100644 --- a/deadlock-plugins/deadlock-extension/package.json +++ b/deadlock-plugins/deadlock-extension/package.json @@ -98,6 +98,7 @@ "postversion": "git add package*.json && auto-changelog -p && git add CHANGELOG.md && git commit -m \"build: $npm_package_version\"" }, "dependencies": { + "@types/tar": "^6.1.1", "@types/yaml": "^1.9.7", "@vscode/webview-ui-toolkit": "^1.0.0", "async": "^3.2.2", @@ -106,6 +107,7 @@ "marked": "^4.0.6", "node-fetch": "^2.6.7", "simple-git": "^3.7.0", + "tar": "^6.1.11", "yaml": "^2.1.1" }, "devDependencies": { diff --git a/deadlock-plugins/deadlock-extension/src/core/api.service.ts b/deadlock-plugins/deadlock-extension/src/core/api.service.ts index d4e5330788ef9949fa85b535c4c2828df8933a15..3e1aa50c78a641ceb0e02c9f5ac75ff34929fb62 100644 --- a/deadlock-plugins/deadlock-extension/src/core/api.service.ts +++ b/deadlock-plugins/deadlock-extension/src/core/api.service.ts @@ -6,8 +6,8 @@ import { extensionError as error } from '../recorder/utils/log'; import Controller from './controller'; import ExtensionStore from './extensionStore'; import KeycloakOAuth2DeviceFlowConnection from './keycloakOAuth2DeviceFlowConnection'; -import { MissionUser } from './mission/models/missionUser'; -import { User } from './mission/models/userChallenge'; +import { MissionUser } from './mission/model/missionUser'; +import { User } from './mission/model/userChallenge'; export default class ApiService { private axiosInstance: AxiosInstance; @@ -130,6 +130,10 @@ export default class ApiService { return this.axiosInstance.get<User>(`users/${userId}`).then((res) => res.data); } + async getSolution(mission: string): Promise<NodeJS.ArrayBufferView> { + return this.axiosInstance.get<NodeJS.ArrayBufferView>(`missions/${mission}/solution`).then((res) => res.data); + } + async grantAccessToRepository(userId: string, missionId: string): Promise<number> { return this.axiosInstance .post<MissionUser>(`users/${userId}/missions/${missionId}/repository/collaborator`) diff --git a/deadlock-plugins/deadlock-extension/src/core/controller.ts b/deadlock-plugins/deadlock-extension/src/core/controller.ts index 087121c433ee9412d8254a669136e7548de7dd32..5fc0a303fec3b680370761eb36a3fb48c8a37ce2 100644 --- a/deadlock-plugins/deadlock-extension/src/core/controller.ts +++ b/deadlock-plugins/deadlock-extension/src/core/controller.ts @@ -7,13 +7,21 @@ import KeycloakOAuth2DeviceFlowConnection from './keycloakOAuth2DeviceFlowConnec import ApiService from './api.service'; import { GiteaPublicProperties } from '../model/giteaPublicProperties.model'; import { MissionDevContainer } from './mission/missionDevContainer'; -import { extensionLog as log } from '../recorder/utils/log'; +import { extensionError, extensionLog as log } from '../recorder/utils/log'; import { commands, ExtensionContext, Uri, window } from 'vscode'; import { createSshKeyFiles } from './sshKeyManager'; import { emptyDirectories, removeFilesOrDirectories } from '../recorder/utils/workdir'; import { missionWorkdir, userSshKeyFolderPath } from './config'; import { hasStatusNumber } from './utils/typeguards'; -import { User } from './mission/models/userChallenge'; +import { User } from './mission/model/userChallenge'; +import { existsSync, mkdirSync, writeFileSync } from 'fs'; +import { extract } from 'tar'; +import { parse } from 'yaml'; +import isDocker from './utils/isdocker'; +import * as fs from 'fs'; +import { getReviewedStudentWorkdirPath } from './utils/mission.utils'; +import { join } from 'path'; +import Mission from '../model/mission'; export default class Controller { private static instance: Controller; @@ -21,11 +29,19 @@ export default class Controller { private commandHandler: CommandHandler; private briefingView: BriefingView; private quickSetupView: QuickSetupView; - private extensionStore: ExtensionStore; private _apiService: ApiService; + private _extensionStore: ExtensionStore; + + public get extensionStore() { + return this._extensionStore; + } + + public get apiService() { + return this._apiService; + } private constructor(context: ExtensionContext) { - this.extensionStore = ExtensionStore.getInstance(context); + this._extensionStore = ExtensionStore.getInstance(context); this.briefingView = new BriefingView(); this.quickSetupView = new QuickSetupView(context.extensionUri); this.commandHandler = new CommandHandler(this); @@ -35,20 +51,53 @@ export default class Controller { KEYCLOAK_USER_INFO_URL, ); - this._apiService = new ApiService(this.connection, this.extensionStore, this); + this._apiService = new ApiService(this.connection, this._extensionStore, this); this.init(); } - public static getInstance(context: ExtensionContext): Controller { + public static getInstance(context?: ExtensionContext): Controller { if (!Controller.instance) { + if (context === undefined) { + throw new Error('Controller needs a context when instantiated'); + } Controller.instance = new Controller(context); } return Controller.instance; } - public get apiService(): ApiService { - return this._apiService; + getChallenge(missionId: string): Mission | undefined { + if (!isDocker()) { + return parse(`${missionWorkdir}/${missionId}/challenge.yaml`); + } + } + + public async getSolution(missionId: string, reviewedId: string) { + try { + const solution = await this._apiService.getSolution(missionId); + const solutionFolder = join(getReviewedStudentWorkdirPath(reviewedId), missionId, '.solution'); + const tarName = 'solution.tar.gz'; + const tarPath = join(solutionFolder, tarName); + + if (existsSync(solutionFolder)) { + fs.rmSync(solutionFolder, { recursive: true }); + } + + mkdirSync(solutionFolder); + + writeFileSync(tarPath, solution); + extract({ sync: true, file: `${solutionFolder}/${tarName}`, cwd: solutionFolder }); + await removeFilesOrDirectories(`${solutionFolder}/${tarName}`); + // list files in the folder + fs.readdirSync(solutionFolder).forEach((file) => { + const tmpSolutionPath = join(solutionFolder, file); + fs.readdirSync(tmpSolutionPath).map((f) => fs.renameSync(join(tmpSolutionPath, f), join(solutionFolder, f))); + fs.rmSync(tmpSolutionPath, { recursive: true }); + }); + } catch (e) { + window.showErrorMessage(`Une erreur est survenue lors de la récupération de la solution`); + extensionError(e); + } } private async init() { @@ -79,7 +128,7 @@ export default class Controller { }, }); - this.quickSetupView.isAlreadyConnected = (await this.extensionStore.getAccessToken()) !== undefined; + this.quickSetupView.isAlreadyConnected = (await this._extensionStore.getAccessToken()) !== undefined; } public async disconnect() { @@ -90,15 +139,15 @@ export default class Controller { } public async createSshKeyPair() { - const { publicKey, privateKey } = await this.apiService.getUserSshKey(); + const { publicKey, privateKey } = await this._apiService.getUserSshKey(); await createSshKeyFiles(publicKey, privateKey); } public async authenticate() { 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._extensionStore.setAccessToken(tokens.accessToken); + await this._extensionStore.setRefreshToken(tokens.refreshToken); await this.createSshKeyPair(); this.quickSetupView.isAlreadyConnected = true; } @@ -110,7 +159,7 @@ export default class Controller { public async launchMission(missionId: string, missionVersion: string, userId?: string) { window.showInformationMessage(`vous lancez la mission ${missionId}`); - const hadBeenConnected = (await this.extensionStore.getAccessToken()) !== undefined; + const hadBeenConnected = (await this._extensionStore.getAccessToken()) !== undefined; if (!hadBeenConnected) { await this.authenticate(); @@ -119,11 +168,11 @@ export default class Controller { const currentUser = await this.apiService.getCurrentUser(); - const user: User = userId ? await this.apiService.getUser(userId) : currentUser; + const user: User = userId ? await this._apiService.getUser(userId) : currentUser; if (!!userId && userId !== currentUser.id) { try { - await this.apiService.grantAccessToRepository(user.id, missionId); + await this._apiService.grantAccessToRepository(user.id, missionId); } catch (e) { if (hasStatusNumber(e)) { if (e.status === 403) { @@ -134,9 +183,15 @@ export default class Controller { throw e; } } + try { + await Controller.getInstance().getSolution(missionId, user.id); + } catch (e) { + window.showErrorMessage(`Une erreur est survenue lors de la récupération de la solution`); + extensionError(e); + } } - const giteaPublicProperties: GiteaPublicProperties = await this.apiService.getGiteaPublicProperties(); + const giteaPublicProperties: GiteaPublicProperties = await this._apiService.getGiteaPublicProperties(); const missionDevcontainer = new MissionDevContainer( user, diff --git a/deadlock-plugins/deadlock-extension/src/core/mission/missionDevContainer.ts b/deadlock-plugins/deadlock-extension/src/core/mission/missionDevContainer.ts index ddd9555cf09d61b23c6958aed83640df7287de16..4d155c96fe87454f7a8a8d1fbb50cf01ddfe6e34 100644 --- a/deadlock-plugins/deadlock-extension/src/core/mission/missionDevContainer.ts +++ b/deadlock-plugins/deadlock-extension/src/core/mission/missionDevContainer.ts @@ -1,14 +1,15 @@ import { missionWorkdir, userSshKeyFolderPath } from '../config'; -import { Base, DockerfileSpecific, LifecycleScripts, VSCodespecific } from './models/devContainer'; +import { Base, DockerfileSpecific, LifecycleScripts, VSCodespecific } from './model/devContainer'; import { mkdir, writeFile } from 'fs/promises'; import { GiteaPublicProperties } from '../../model/giteaPublicProperties.model'; -import { commands, Uri } from 'vscode'; +import { commands, Uri, window } from 'vscode'; import { existsSync, copyFileSync } from 'fs'; import { join } from 'path'; import assert = require('assert'); -import UserChallenge, { User } from './models/userChallenge'; +import UserChallenge, { User } from './model/userChallenge'; import { REGISTRY_MISSION_URL } from '../../config'; +import { getReviewedStudentWorkdirPath } from '../utils/mission.utils'; const remoteUserHomeDir = '/home/deadlock'; const remoteMissionDir = `${remoteUserHomeDir}/mission/`; @@ -27,7 +28,13 @@ export class MissionDevContainer { private readonly mounts: string[] = []; private readonly extentionPath: string; - private readonly dirs: { missionWorkdir: string; config: string; devcontainer: string; mounted: string }; + private readonly dirs: { + missionWorkdir: string; + config: string; + devcontainer: string; + mounted: string; + solution: string; + }; constructor( private readonly user: User, @@ -36,15 +43,18 @@ export class MissionDevContainer { private readonly missionVersion: string, private readonly giteaProperties: GiteaPublicProperties, ) { - let prefix = missionWorkdir; + let prefix: string; if (this.isReviewingStudent()) { - prefix += `/students/${this.user.details.lastName}_${this.user.details.firstName}_${this.user.id}`; + prefix = getReviewedStudentWorkdirPath(this.user.id); + } else { + prefix = `${missionWorkdir}`; } this.dirs = { missionWorkdir: `${prefix}/${this.missionId}`, config: `${prefix}/${this.missionId}/.config`, devcontainer: `${prefix}/${this.missionId}/.devcontainer`, mounted: `${prefix}/${this.missionId}/mounted`, + solution: `${prefix}/${this.missionId}/.solution`, }; if (process.env.DL_MOUNT_EXTENSION === 'true') { this.extentionPath = '/injected-extension/extension.vsix'; @@ -66,9 +76,14 @@ export class MissionDevContainer { throw new Error(`${MissionDevContainer.constructor.name} Already initialized`); } this.isInit = true; - await this.createDirectories(...Object.values(this.dirs)); - await this.createDevContainerFile(); - await this.createUserChallengeJsonFile(); + try { + await this.createDirectories(...Object.values(this.dirs)); + await this.createDevContainerFile(); + await this.createUserChallengeJsonFile(); + } catch (e) { + console.error(e); + window.showErrorMessage(`Une erreur est survenue lors de la création de la mission ${this.missionId}`); + } } private isReviewingStudent() { @@ -111,7 +126,8 @@ export class MissionDevContainer { } } - private setupMounts(): void { + private async setupMounts() { + this.mounts.splice(0, this.mounts.length); this.mounts.push( `source=${userSshKeyFolderPath},target=/tmp/.ssh,type=bind,consistency=cached,readonly`, `source=${this.dirs.config},target=/home/config/,type=bind,consistency=cached,readonly`, @@ -121,10 +137,15 @@ export class MissionDevContainer { this.mounts.push(`source=${process.env.EXTENSION_PATH},target=${this.extentionPath},type=bind`); } this.addInMountRCs(this.dirs.missionWorkdir, '.bashrc', '.zshrc'); + if (this.isReviewingStudent()) { + this.mounts.push(`source=${this.dirs.solution},target=${remoteMissionDir}/solution,type=bind`); + } } - private createDevContainerFile(options?: Partial<DockerfileSpecific & Base & VSCodespecific & LifecycleScripts>) { - this.setupMounts(); + private async createDevContainerFile( + options?: Partial<DockerfileSpecific & Base & VSCodespecific & LifecycleScripts>, + ) { + await this.setupMounts(); return writeFile( `${this.dirs.devcontainer}/devcontainer.json`, diff --git a/deadlock-plugins/deadlock-extension/src/core/mission/models/devContainer.ts b/deadlock-plugins/deadlock-extension/src/core/mission/model/devContainer.ts similarity index 100% rename from deadlock-plugins/deadlock-extension/src/core/mission/models/devContainer.ts rename to deadlock-plugins/deadlock-extension/src/core/mission/model/devContainer.ts diff --git a/deadlock-plugins/deadlock-extension/src/core/mission/models/missionUser.ts b/deadlock-plugins/deadlock-extension/src/core/mission/model/missionUser.ts similarity index 100% rename from deadlock-plugins/deadlock-extension/src/core/mission/models/missionUser.ts rename to deadlock-plugins/deadlock-extension/src/core/mission/model/missionUser.ts diff --git a/deadlock-plugins/deadlock-extension/src/core/mission/models/userChallenge.ts b/deadlock-plugins/deadlock-extension/src/core/mission/model/userChallenge.ts similarity index 97% rename from deadlock-plugins/deadlock-extension/src/core/mission/models/userChallenge.ts rename to deadlock-plugins/deadlock-extension/src/core/mission/model/userChallenge.ts index 9abad84f5038f6b7922d8b0bf3acacf36d4a1a86..cd1a92fc5e213b715bcb03c3b3530a1ccc243d6f 100644 --- a/deadlock-plugins/deadlock-extension/src/core/mission/models/userChallenge.ts +++ b/deadlock-plugins/deadlock-extension/src/core/mission/model/userChallenge.ts @@ -14,6 +14,7 @@ interface UserChallenge { export default UserChallenge; export interface UserDetails { + id: string; firstName: string; lastName: string; organization: string; diff --git a/deadlock-plugins/deadlock-extension/src/core/userConfig.ts b/deadlock-plugins/deadlock-extension/src/core/userConfig.ts index cc6a654a827011a533ba80ff99d130e4a9360a1c..39a5f8b20dcf87cea9810b8605b9f9d4ea35f29a 100644 --- a/deadlock-plugins/deadlock-extension/src/core/userConfig.ts +++ b/deadlock-plugins/deadlock-extension/src/core/userConfig.ts @@ -14,10 +14,12 @@ * "missionId":"code_persist_cdb_crud" * } */ +import { Uri, workspace } from 'vscode'; import { recorderError as error } from '../recorder/utils/log'; -import { UserDetails } from './mission/models/userChallenge'; +import { USER_CHALLENGE_PATH } from './config'; +import { UserDetails } from './mission/model/userChallenge'; -export default abstract class UserConfig { +export default class UserConfig { private userConfigJson: any | undefined; getPaths(): Map<number, string> { @@ -63,12 +65,11 @@ export default abstract class UserConfig { return this.userConfigJson?.remoteUserDetails; } - getCurrentUserId(): string { - return this.userConfigJson?.currentUserId; + async loadText(): Promise<string> { + const textDocument = await workspace.openTextDocument(Uri.parse(USER_CHALLENGE_PATH)); + return Promise.resolve(textDocument.getText()); } - abstract loadText(): Promise<string>; - public async init() { try { const userConfig = await this.loadText(); diff --git a/deadlock-plugins/deadlock-extension/src/core/utils/mission.utils.ts b/deadlock-plugins/deadlock-extension/src/core/utils/mission.utils.ts new file mode 100644 index 0000000000000000000000000000000000000000..d279e45fc4f9d44ad328c917658a1d7b6f7f7ceb --- /dev/null +++ b/deadlock-plugins/deadlock-extension/src/core/utils/mission.utils.ts @@ -0,0 +1,21 @@ +import { PathLike, readFileSync } from 'fs'; +import { join } from 'path'; +import { missionWorkdir, USER_CHALLENGE_PATH } from '../config'; +import UserChallenge from '../mission/model/userChallenge'; +import isDocker from './isdocker'; + +export function getReviewedStudentWorkdirPath(userId: string): string { + return join(missionWorkdir, 'students', `${userId}`); +} + +export function getUserChallenge(userId: string, missionid: string, isReviewing = false): UserChallenge { + let path: number | PathLike; + if (isDocker()) { + path = USER_CHALLENGE_PATH; + } else if (isReviewing) { + path = join(getReviewedStudentWorkdirPath(userId), missionid, '.config', 'userChallenge.json'); + } else { + path = join(missionWorkdir, missionid, '.config', 'userChallenge.json'); + } + return JSON.parse(readFileSync(path, 'utf8')); +} diff --git a/deadlock-plugins/deadlock-extension/src/extension.ts b/deadlock-plugins/deadlock-extension/src/extension.ts index 7aaf21e1dc8c32b9fc0325baf3485b211d6f76b5..a4f51b356bee9808a5b49e917018e5581fecf6ce 100644 --- a/deadlock-plugins/deadlock-extension/src/extension.ts +++ b/deadlock-plugins/deadlock-extension/src/extension.ts @@ -1,15 +1,14 @@ import { window, ExtensionContext, workspace, commands } from 'vscode'; import Controller from './core/controller'; +import UserConfig from './core/userConfig'; import isDocker from './core/utils/isdocker'; import Recorder from './recorder/recorder'; import { extensionError as error } from './recorder/utils/log'; import { DepNodeProvider } from './theia/deadlockPanel'; -import UserConfigTheia from './theia/userConfigTheia'; import { CommandTreeProvider } from './view/CommandTree'; import StartedMissionsView from './view/startedMissionsView'; -// TODO: refactor remove this -export const userConfig = new UserConfigTheia(); +export const userConfig = new UserConfig(); export async function runTimer(userId: string, missionId: string, context: ExtensionContext) { const apiService = Controller.getInstance(context).apiService; @@ -54,6 +53,6 @@ export async function activate(context: ExtensionContext) { error(JSON.stringify(e)); window.showErrorMessage("Le recorder n'a pas pu être lancé"); } - runTimer(userConfig.getCurrentUserId(), userConfig.getMissionId(), context); + runTimer(userConfig.getCurrentUserDetails().id, userConfig.getMissionId(), context); } } diff --git a/deadlock-plugins/deadlock-extension/src/theia/userConfigTheia.ts b/deadlock-plugins/deadlock-extension/src/theia/userConfigTheia.ts deleted file mode 100644 index 7bdeb915facb96639e2e930c77673648772f627d..0000000000000000000000000000000000000000 --- a/deadlock-plugins/deadlock-extension/src/theia/userConfigTheia.ts +++ /dev/null @@ -1,11 +0,0 @@ -import UserConfig from '../core/userConfig'; - -import { USER_CHALLENGE_PATH } from '../core/config'; -import { Uri, workspace } from 'vscode'; - -export default class UserConfigTheia extends UserConfig { - async loadText(): Promise<string> { - const textDocument = await workspace.openTextDocument(Uri.parse(USER_CHALLENGE_PATH)); - return Promise.resolve(textDocument.getText()); - } -} diff --git a/deadlock-plugins/deadlock-extension/src/view/startedMissionsView.ts b/deadlock-plugins/deadlock-extension/src/view/startedMissionsView.ts index 75378b46601b5ba45e649f4a994a70d977b8e44e..aaff928f3f95c3f0f749ea25e6a90b0e3ba84297 100644 --- a/deadlock-plugins/deadlock-extension/src/view/startedMissionsView.ts +++ b/deadlock-plugins/deadlock-extension/src/view/startedMissionsView.ts @@ -1,13 +1,12 @@ import { randomBytes } from 'crypto'; import { existsSync, readdirSync, readFileSync } from 'fs'; import { join } from 'path'; -import { ExtensionContext, Webview, WebviewView, WebviewViewProvider, window } from 'vscode'; -import { missionWorkdir } from '../core/config'; +import { ExtensionContext, Webview, WebviewView, WebviewViewProvider } from 'vscode'; import Controller from '../core/controller'; -import UserChallenge from '../core/mission/models/userChallenge'; +import UserChallenge from '../core/mission/model/userChallenge'; import { extensionWarn } from '../recorder/utils/log'; import { getUri } from './webviewBase'; - +import { missionWorkdir } from '../core/config'; export default class StartedMissionsView implements WebviewViewProvider { private readonly controller: Controller; constructor(private context: ExtensionContext) { @@ -27,10 +26,9 @@ export default class StartedMissionsView implements WebviewViewProvider { onMessageReceive(message: any): void { switch (message.command) { case 'openMission': { - window.showInformationMessage('click received: ' + message.mission); const path = join(missionWorkdir, message.mission, '.config', 'user-challenge.json'); const userChallenge: UserChallenge = JSON.parse(readFileSync(path, 'utf8')); - this.controller.launchMission(userChallenge.missionId, userChallenge.missionVersion); + Controller.getInstance().launchMission(userChallenge.missionId, userChallenge.missionVersion); return; } }