diff --git a/deadlock-plugins/deadlock-extension/package.json b/deadlock-plugins/deadlock-extension/package.json index acb460290121cc42bb73f8b76693923075fa9917..2c4d9e6071d796877d54f7257acda81c8349a2b0 100644 --- a/deadlock-plugins/deadlock-extension/package.json +++ b/deadlock-plugins/deadlock-extension/package.json @@ -33,7 +33,8 @@ { "command": "deadlock.disconnect", "title": "Clear cache", - "category": "Deadlock Coding" + "category": "Deadlock Coding", + "enablement": "!deadlock.inContainer" } ], "viewsContainers": { @@ -69,7 +70,8 @@ { "id": "help", "name": "Help", - "visibility": "visible" + "visibility": "visible", + "when": "!deadlock.inContainer" } ] }, diff --git a/deadlock-plugins/deadlock-extension/src/core/commandHandler.ts b/deadlock-plugins/deadlock-extension/src/core/commandHandler.ts index b73494fb57264fefeb9b8d2a4e2e0a2f43d975b1..455c8ad9484d0cdc6b0525e3fd83e4299ab9d198 100644 --- a/deadlock-plugins/deadlock-extension/src/core/commandHandler.ts +++ b/deadlock-plugins/deadlock-extension/src/core/commandHandler.ts @@ -1,5 +1,6 @@ import { Command, commands } from 'vscode'; import Controller from './controller'; +import isMissionStarted from './utils/mission.util'; export class CommandHandler { private static _instance?: CommandHandler; @@ -18,7 +19,12 @@ export class CommandHandler { this.disconnectCommand = { title: 'Disconnect', command: 'deadlock.disconnect' }; this.authenticateCommand = { title: 'Authenticate', command: 'deadlock.authenticate' }; this.openUrlInBrowserCommand = { title: 'Open url in browser', command: 'vscode.open' }; - commands.registerCommand(this.disconnectCommand.command, Controller.instance.disconnect.bind(Controller.instance)); + if (!isMissionStarted()) { + commands.registerCommand( + this.disconnectCommand.command, + Controller.instance.disconnect.bind(Controller.instance), + ); + } commands.registerCommand( this.authenticateCommand.command, Controller.instance.authenticate.bind(Controller.instance), diff --git a/deadlock-plugins/deadlock-extension/src/core/config.ts b/deadlock-plugins/deadlock-extension/src/core/config.ts index 1fa0a2c72b0812521a55a9c7607da36e4e633914..1c3a158013df241eb506d9b66ce540ccfd7fe2fa 100644 --- a/deadlock-plugins/deadlock-extension/src/core/config.ts +++ b/deadlock-plugins/deadlock-extension/src/core/config.ts @@ -1,9 +1,6 @@ import { homedir } from 'os'; import { join } from 'path'; -import isDocker from './utils/isdocker'; - -// if we are on container, means the directory will depend differently -const onContainer = isDocker(); +import isMissionStarted from './utils/mission.util'; const deadlockConfigPath = join(homedir(), '.deadlock'); @@ -19,7 +16,7 @@ export const MISSION_PATH_IC = `${homedir()}/mission`; export const MISSION_FILE_IC = join('/', 'deadlock', 'challenge.yaml'); -export const DEADLOCK_WORKDIR_PATH = onContainer ? '/deadlock' : deadlockConfigPath; +export const DEADLOCK_WORKDIR_PATH = isMissionStarted() ? '/deadlock' : deadlockConfigPath; export const USER_MISSION_PATH = join('/home/config/user-challenge.json'); diff --git a/deadlock-plugins/deadlock-extension/src/core/controller.ts b/deadlock-plugins/deadlock-extension/src/core/controller.ts index a60950a5a6948b8e416bd96dc8752907e825b035..602b7a89185ff514e70382e469f5747b5b8ee834 100644 --- a/deadlock-plugins/deadlock-extension/src/core/controller.ts +++ b/deadlock-plugins/deadlock-extension/src/core/controller.ts @@ -13,8 +13,8 @@ import { hasStatusNumber } from './utils/typeguards'; import { existsSync, mkdirSync, readdirSync, renameSync, rmSync, writeFileSync } from 'fs'; import { extract } from 'tar'; import { parse } from 'yaml'; -import isDocker from './utils/isdocker'; -import { getReviewedStudentWorkdirPath } from './utils/mission.utils'; +import { clearDevContainers } from './utils/docker.util'; +import isMissionStarted, { getReviewedStudentWorkdirPath } from './utils/mission.util'; import { join } from 'path'; import Mission from '../model/mission'; import ApiService from './api.service'; @@ -31,7 +31,7 @@ export default class Controller { throw new Error('Controller is a singleton'); } Controller._instance = this; - if (isDocker()) { + if (isMissionStarted()) { this.briefingView = new BriefingView(); } else { this.startedMissions = new StartedMissionsView(); @@ -46,7 +46,7 @@ export default class Controller { } getChallenge(missionId: string): Mission | undefined { - if (!isDocker()) { + if (!isMissionStarted()) { return parse(`${missionWorkdir}/${missionId}/challenge.yaml`); } } @@ -130,6 +130,12 @@ export default class Controller { extensionWarn('Could not clear extension store'); extensionWarn(e); } + try { + await clearDevContainers(); + } catch (e) { + extensionWarn('Could not clear dev containers'); + extensionWarn(e); + } this.quickSetupView.isAlreadyConnected = false; } diff --git a/deadlock-plugins/deadlock-extension/src/core/mission/missionDevContainer.ts b/deadlock-plugins/deadlock-extension/src/core/mission/missionDevContainer.ts index 13d3329e20819d8cacdb7afdb46fe73de5097e01..d76607bdd8310afb98a0304a8865d3d9dde4565f 100644 --- a/deadlock-plugins/deadlock-extension/src/core/mission/missionDevContainer.ts +++ b/deadlock-plugins/deadlock-extension/src/core/mission/missionDevContainer.ts @@ -7,7 +7,7 @@ import { join } from 'path'; import UserMission from './model/userMission'; import { REGISTRY_MISSION_URL } from '../../config'; -import { getReviewedStudentWorkdirPath } from '../utils/mission.utils'; +import { getReviewedStudentWorkdirPath } from '../utils/mission.util'; import { homedir } from 'os'; import { extensionError } from '../../recorder/utils/log'; import { createDirectories } from '../../recorder/utils/workdir'; @@ -136,8 +136,10 @@ export class MissionDevContainer { return writeFile( `${this.dirs.devcontainer}/devcontainer.json`, (() => { + const containerName = + `deadlock-mission-${this.missionId}-${this.missionVersion}` + (this.revieweeId ? `-${this.revieweeId}` : ''); const devcontainer: Partial<DockerfileSpecific & Base & VSCodespecific & LifecycleScripts> = { - name: `deadlock-${this.missionId}`, + name: containerName, image: `${REGISTRY_MISSION_URL}/${this.missionId}:${this.missionVersion}`, containerEnv: { WORKDIR: `${remoteMissionDir}`, @@ -152,7 +154,7 @@ export class MissionDevContainer { workspaceMount: `source=${this.dirs.mounted},target=${remoteMissionDir},type=bind`, workspaceFolder: `${remoteMissionDir}`, onCreateCommand: `cp -R ${remoteGiteaWorkDir}/* ${remoteMissionDir} && sudo bash /start.desktop.sh`, - runArgs: ['--privileged'], + runArgs: ['--name', containerName, '--privileged'], ...options, }; return JSON.stringify(devcontainer, null, 2); diff --git a/deadlock-plugins/deadlock-extension/src/core/mission/model/devContainer.ts b/deadlock-plugins/deadlock-extension/src/core/mission/model/devContainer.ts index 677faea6f2f59105971ffdf5add20a8eb7461224..e887cf378efadf581c26e7cfcec0b73d684685a7 100644 --- a/deadlock-plugins/deadlock-extension/src/core/mission/model/devContainer.ts +++ b/deadlock-plugins/deadlock-extension/src/core/mission/model/devContainer.ts @@ -2,7 +2,7 @@ export interface DockerfileSpecific { image?: string; dockerFile?: string; context?: string; - 'build.args'?: string[]; + build?: { args: Record<string, string> }; 'build.target'?: string; 'build.cacheFrom'?: string; containerEnv?: { [id: string]: string }; diff --git a/deadlock-plugins/deadlock-extension/src/core/mission/model/userMission.ts b/deadlock-plugins/deadlock-extension/src/core/mission/model/userMission.ts index 829a829430e584589db3d4d4df4b1eacd08e93b4..66b16d7a9b81d0f3d267b8affb041092b349242a 100644 --- a/deadlock-plugins/deadlock-extension/src/core/mission/model/userMission.ts +++ b/deadlock-plugins/deadlock-extension/src/core/mission/model/userMission.ts @@ -4,7 +4,7 @@ import { join } from 'path'; import { createDirectories } from '../../../recorder/utils/workdir'; import ApiService from '../../api.service'; import { missionWorkdir, USER_MISSION_PATH } from '../../config'; -import { getReviewedStudentWorkdirPath } from '../../utils/mission.utils'; +import { getReviewedStudentWorkdirPath } from '../../utils/mission.util'; interface UserMissionModel { missionId: string; diff --git a/deadlock-plugins/deadlock-extension/src/core/utils/docker.util.ts b/deadlock-plugins/deadlock-extension/src/core/utils/docker.util.ts new file mode 100644 index 0000000000000000000000000000000000000000..7a10f7580f56855d90cbfed26ccf65672c5b58e2 --- /dev/null +++ b/deadlock-plugins/deadlock-extension/src/core/utils/docker.util.ts @@ -0,0 +1,35 @@ +import { exec as execCallback } from 'child_process'; +import { promisify } from 'util'; + +const exec = promisify(execCallback); + +async function getDevContainerContainers(): Promise<string[]> { + return (await exec('docker container ps -a --format {{.Names}}')).stdout + .split('\n') + .filter((container) => container.includes('deadlock-mission-')); +} + +async function removeContainers(...containers: string[]) { + await exec(`docker container rm -f ${containers.join(' ')}`); +} + +async function getDevContainerImages(): Promise<string[]> { + return (await exec('docker image ls --format {{.Repository}}')).stdout + .split('\n') + .filter((image) => image.includes('code_')); +} + +async function removeImages(...images: string[]) { + await exec(`docker image rm -f ${images.join(' ')}`); +} + +export async function clearDevContainers() { + const containers = await getDevContainerContainers(); + const images = await getDevContainerImages(); + if (containers.length > 0) { + await removeContainers(...containers); + } + if (images.length > 0) { + await removeImages(...images); + } +} diff --git a/deadlock-plugins/deadlock-extension/src/core/utils/isdocker.ts b/deadlock-plugins/deadlock-extension/src/core/utils/isdocker.ts deleted file mode 100644 index fe8c3d1686c4b8203b7822be99715cac12ffbfab..0000000000000000000000000000000000000000 --- a/deadlock-plugins/deadlock-extension/src/core/utils/isdocker.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { readFileSync, statSync } from 'fs'; - -let isDockerCached: undefined | boolean; - -function hasDockerEnv() { - try { - statSync('/.dockerenv'); - return true; - } catch { - return false; - } -} - -function hasDockerCGroup() { - try { - return readFileSync('/proc/self/cgroup', 'utf8').includes('docker'); - } catch { - return false; - } -} - -export default function isDocker() { - // TODO: Use `??=` when targeting Node.js 16. - if (isDockerCached === undefined) { - isDockerCached = hasDockerEnv() || hasDockerCGroup(); - } - - return isDockerCached; -} diff --git a/deadlock-plugins/deadlock-extension/src/core/utils/mission.utils.ts b/deadlock-plugins/deadlock-extension/src/core/utils/mission.util.ts similarity index 80% rename from deadlock-plugins/deadlock-extension/src/core/utils/mission.utils.ts rename to deadlock-plugins/deadlock-extension/src/core/utils/mission.util.ts index 8238c3a78c585139b4badc5f00fbcb2132b9c52c..eb2828225571a8e9a890c389cd0d3fb424383b6f 100644 --- a/deadlock-plugins/deadlock-extension/src/core/utils/mission.utils.ts +++ b/deadlock-plugins/deadlock-extension/src/core/utils/mission.util.ts @@ -1,8 +1,7 @@ -import { PathLike, readFileSync } from 'fs'; +import { existsSync, PathLike, readFileSync } from 'fs'; import { join } from 'path'; import { missionWorkdir, USER_MISSION_PATH } from '../config'; import UserMission from '../mission/model/userMission'; -import isDocker from './isdocker'; export function getReviewedStudentWorkdirPath(userId: string): string { return join(missionWorkdir, 'students', `${userId}`); @@ -10,7 +9,7 @@ export function getReviewedStudentWorkdirPath(userId: string): string { export function getUserChallenge(userId: string, missionid: string, isReviewing = false): UserMission { let path: number | PathLike; - if (isDocker()) { + if (isMissionStarted()) { path = USER_MISSION_PATH; } else if (isReviewing) { path = join(getReviewedStudentWorkdirPath(userId), missionid, '.config', 'userChallenge.json'); @@ -19,3 +18,7 @@ export function getUserChallenge(userId: string, missionid: string, isReviewing } return JSON.parse(readFileSync(path, 'utf8')); } + +export default function isMissionStarted() { + return existsSync(USER_MISSION_PATH); +} diff --git a/deadlock-plugins/deadlock-extension/src/extension.ts b/deadlock-plugins/deadlock-extension/src/extension.ts index 95cd3b2cfedd9a3fc9f826d85f5c4981e962a3a7..4082bd01444f78114b87b36295bfc6f7ed8ad729 100644 --- a/deadlock-plugins/deadlock-extension/src/extension.ts +++ b/deadlock-plugins/deadlock-extension/src/extension.ts @@ -1,10 +1,10 @@ import { window, ExtensionContext, workspace, commands } from 'vscode'; -import isDocker from './core/utils/isdocker'; import Recorder from './recorder/recorder'; import { extensionError as error } from './recorder/utils/log'; import { DepNodeProvider } from './view/deadlockPanel'; import { CommandTreeProvider } from './view/CommandTree'; import Controller from './core/controller'; +import isMissionStarted from './core/utils/mission.util'; export async function activate(context: ExtensionContext) { new Controller(context); @@ -17,7 +17,7 @@ export async function activate(context: ExtensionContext) { if (Controller.instance.startedMissions) { window.registerWebviewViewProvider('startedMissions', Controller.instance.startedMissions); } - if (isDocker()) { + if (isMissionStarted()) { commands.executeCommand('setContext', 'deadlock.inContainer', true); window.registerTreeDataProvider('commandTree', new CommandTreeProvider()); try { diff --git a/deadlock-plugins/deadlock-extension/src/view/briefingView.ts b/deadlock-plugins/deadlock-extension/src/view/briefingView.ts index 71db47c9617ece2ea80bbbf8f9ec7136b3c95ba5..6f5e5e1baad4385434c829aa6fcd69cd87bd09ae 100644 --- a/deadlock-plugins/deadlock-extension/src/view/briefingView.ts +++ b/deadlock-plugins/deadlock-extension/src/view/briefingView.ts @@ -7,7 +7,7 @@ import ApiService from '../core/api.service'; import { BRIEFING_FILE_NAME, DOCS_PATH_IC } from '../core/config'; import { openBriefingCommand } from '../core/controller'; import UserMission from '../core/mission/model/userMission'; -import isDocker from '../core/utils/isdocker'; +import isMissionStarted from '../core/utils/mission.util'; import { WebviewBase } from './webviewBase'; export const briefingId = 'brefingView'; @@ -55,7 +55,7 @@ export default class BriefingView extends WebviewBase { console.error(e); this.briefingContent = 'Error while parsing your briefing.'; } - if (isDocker()) this.show(); + if (isMissionStarted()) this.show(); }, (error) => { console.error('Cannot load briefing', error);