From bf2644d18ef0cb05acd3882c0a7e851bc3f6872d Mon Sep 17 00:00:00 2001
From: Mohamed AZIKIOU <mazikiou@takima.fr>
Date: Tue, 12 Jul 2022 14:46:33 +0000
Subject: [PATCH] feat: submit code to api

---
 .../src/core/api.service.ts                   |  17 ++-
 .../deadlock-extension/src/core/config.ts     |   2 +
 .../deadlock-extension/src/core/controller.ts |   7 +-
 .../src/core/mission/missionDevContainer.ts   |   3 +-
 .../src/core/mission/model/userMission.ts     |  16 +-
 .../deadlock-extension/src/extension.ts       |   2 +-
 .../deadlock-extension/src/model/attempt.ts   |  68 +++++++++
 .../deadlock-extension/src/model/mission.ts   |  74 +++++++---
 .../src/recorder/utils/gitea.ts               |  35 +++--
 .../src/view/CommandTree.ts                   | 138 ++++++++++++------
 .../src/view/startedMissionsView.ts           |   4 +-
 11 files changed, 275 insertions(+), 91 deletions(-)
 create mode 100644 deadlock-plugins/deadlock-extension/src/model/attempt.ts

diff --git a/deadlock-plugins/deadlock-extension/src/core/api.service.ts b/deadlock-plugins/deadlock-extension/src/core/api.service.ts
index 73686796..9d029564 100644
--- a/deadlock-plugins/deadlock-extension/src/core/api.service.ts
+++ b/deadlock-plugins/deadlock-extension/src/core/api.service.ts
@@ -3,6 +3,7 @@ import { API_QUERY_REFERER, API_URL } from '../config';
 import { GiteaPublicProperties } from '../model/giteaPublicProperties.model';
 import { SshKeyPair } from '../model/sshKeyPair.model';
 import { extensionError as error } from '../recorder/utils/log';
+import { Attempt } from '../model/attempt';
 import Controller from './controller';
 import ExtensionStore from './extensionStore';
 import KeycloakOAuth2DeviceFlowConnection from './keycloakOAuth2DeviceFlowConnection';
@@ -168,13 +169,19 @@ export default class ApiService {
       .then((res) => res.data);
   }
 
-  async startUpdateWorkTime(): Promise<MissionUser> {
+  async startUpdateWorkTime() {
+    return this.axiosInstance
+      .post(`users/${(await this.getCurrentUser()).id}/missions/${UserMission.getInstance().missionId}/worktime`, {
+        action: 'START',
+      })
+      .then((res) => res.data);
+  }
+
+  async submitAttempt(attempt: Attempt): Promise<MissionUser> {
     return this.axiosInstance
       .post<MissionUser>(
-        `users/${(await this.getCurrentUser()).id}/missions/${UserMission.getInstance().missionId}/worktime`,
-        {
-          action: 'START',
-        },
+        `/users/${(await this.getCurrentUser()).id}/missions/${UserMission.getInstance().missionId}/solve/desktop`,
+        attempt,
       )
       .then((res) => res.data);
   }
diff --git a/deadlock-plugins/deadlock-extension/src/core/config.ts b/deadlock-plugins/deadlock-extension/src/core/config.ts
index 7c9e0f06..1fa0a2c7 100644
--- a/deadlock-plugins/deadlock-extension/src/core/config.ts
+++ b/deadlock-plugins/deadlock-extension/src/core/config.ts
@@ -17,6 +17,8 @@ export const GITEA_PATH_IC = '/project';
 
 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 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 f61a10f0..0cd96e52 100644
--- a/deadlock-plugins/deadlock-extension/src/core/controller.ts
+++ b/deadlock-plugins/deadlock-extension/src/core/controller.ts
@@ -101,6 +101,7 @@ export default class Controller {
         const missionId = queryParams.get('missionId');
         const missionVersion = queryParams.get('missionVersion');
         const userId = queryParams.get('userId') ?? undefined;
+        const storyId = queryParams.get('storyId') ?? undefined;
         log('Opening link', uri);
 
         switch (action) {
@@ -108,7 +109,7 @@ export default class Controller {
             if (!missionId || !missionVersion) {
               window.showErrorMessage('Identifiant ou version de la mission incorrect');
             } else {
-              that.launchMission(missionId, missionVersion, userId);
+              that.launchMission(missionId, missionVersion, userId, storyId);
             }
 
             break;
@@ -162,7 +163,7 @@ export default class Controller {
     commands.executeCommand(openUrlInBrowserCommand.command, Uri.parse(url));
   }
 
-  public async launchMission(missionId: string, missionVersion: string, revieweeId?: string) {
+  public async launchMission(missionId: string, missionVersion: string, revieweeId?: string, storyId?: string) {
     window.showInformationMessage(`vous lancez la mission ${missionId}`);
 
     const hadBeenConnected = (await this._extensionStore.getAccessToken()) !== undefined;
@@ -195,7 +196,7 @@ export default class Controller {
       }
     }
 
-    const missionDevcontainer = new MissionDevContainer(missionId, missionVersion, revieweeId);
+    const missionDevcontainer = new MissionDevContainer(missionId, missionVersion, revieweeId, storyId);
 
     try {
       await missionDevcontainer.open();
diff --git a/deadlock-plugins/deadlock-extension/src/core/mission/missionDevContainer.ts b/deadlock-plugins/deadlock-extension/src/core/mission/missionDevContainer.ts
index a4abb9a0..13d3329e 100644
--- a/deadlock-plugins/deadlock-extension/src/core/mission/missionDevContainer.ts
+++ b/deadlock-plugins/deadlock-extension/src/core/mission/missionDevContainer.ts
@@ -40,6 +40,7 @@ export class MissionDevContainer {
     private readonly missionId: string,
     private readonly missionVersion: string,
     private readonly revieweeId?: string,
+    private readonly storyId?: string,
   ) {
     let prefix: string;
     if (revieweeId) {
@@ -84,7 +85,7 @@ export class MissionDevContainer {
 
   private async createUserChallengeJsonFile() {
     try {
-      await UserMission.writeFile(this.missionId, this.missionVersion, this.revieweeId);
+      await UserMission.writeFile(this.missionId, this.missionVersion, this.revieweeId, this.storyId);
     } catch (e) {
       extensionError(e);
       window.showErrorMessage(`Une erreur est survenue lors de la création du fichier userChallenge.json`);
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 95676b09..359af9cb 100644
--- a/deadlock-plugins/deadlock-extension/src/core/mission/model/userMission.ts
+++ b/deadlock-plugins/deadlock-extension/src/core/mission/model/userMission.ts
@@ -9,6 +9,7 @@ import { getReviewedStudentWorkdirPath } from '../../utils/mission.utils';
 interface UserMissionModel {
   missionId: string;
   missionVersion: string;
+  storyId?: string;
   reviewee?: User;
 }
 
@@ -19,6 +20,7 @@ export default class UserMission implements UserMissionModel {
     this.missionId = userMission.missionId;
     this.missionVersion = userMission.missionVersion;
     this.reviewee = userMission.reviewee;
+    this.storyId = userMission.storyId;
     UserMission._instance = this;
   }
 
@@ -43,6 +45,7 @@ export default class UserMission implements UserMissionModel {
   }
 
   public readonly missionId: string;
+  public readonly storyId?: string;
   public readonly missionVersion: string;
   public readonly reviewee?: User;
 
@@ -50,21 +53,22 @@ export default class UserMission implements UserMissionModel {
     return join(revieweeId ? getReviewedStudentWorkdirPath(revieweeId) : missionWorkdir, missionId, '.config');
   }
 
-  public static async writeFile(missionId: string, missionVersion: string, revieweeId?: string): Promise<void> {
+  public static async writeFile(
+    missionId: string,
+    missionVersion: string,
+    revieweeId?: string,
+    storyId?: string,
+  ): Promise<void> {
     const workdir = this.getMissionUserFolder(missionId, revieweeId);
     await createDirectories(workdir);
-    const user = await ApiService.getInstance().getCurrentUser();
-    const username = (revieweeId ?? user.id).split('-').join('');
     const path = join(workdir, 'user-challenge.json');
     await writeFile(
       path,
       JSON.stringify(
         {
-          username,
-          email: `${username}@deadlock.io`,
           missionId,
+          storyId,
           missionVersion,
-          remoteGitUsername: username,
           reviewee: revieweeId ? await ApiService.getInstance().getUser(revieweeId) : undefined,
         } as UserMissionModel,
         null,
diff --git a/deadlock-plugins/deadlock-extension/src/extension.ts b/deadlock-plugins/deadlock-extension/src/extension.ts
index 01ee2de5..ba3d660e 100644
--- a/deadlock-plugins/deadlock-extension/src/extension.ts
+++ b/deadlock-plugins/deadlock-extension/src/extension.ts
@@ -19,7 +19,7 @@ export async function activate(context: ExtensionContext) {
 
   if (isDocker()) {
     commands.executeCommand('setContext', 'deadlock.inContainer', true);
-    window.registerTreeDataProvider('commandTree', new CommandTreeProvider(context));
+    window.registerTreeDataProvider('commandTree', new CommandTreeProvider());
     try {
       Recorder.instance.run();
     } catch (e) {
diff --git a/deadlock-plugins/deadlock-extension/src/model/attempt.ts b/deadlock-plugins/deadlock-extension/src/model/attempt.ts
new file mode 100644
index 00000000..1c964409
--- /dev/null
+++ b/deadlock-plugins/deadlock-extension/src/model/attempt.ts
@@ -0,0 +1,68 @@
+export default class AttemptBuilder {
+  public id?: string;
+  public date?: number;
+  public files?: Map<string, string | null>;
+  public score = -1;
+  public readonly treated: false;
+  private _consoleContent: string;
+  private _exitCode? = 408; // timeout
+
+  constructor(
+    public readonly userId: string,
+    public readonly missionId: string,
+    public readonly isASubmit: boolean,
+    public readonly storyId?: string,
+  ) {
+    this.treated = false;
+    this._consoleContent = '';
+  }
+
+  public addLog(log: string): void {
+    this._consoleContent += `[${Date.now()}] ${log}\n`;
+  }
+
+  public setExitCode(exitCode?: number): void {
+    this._exitCode = exitCode;
+  }
+
+  public get isSolved(): boolean {
+    return this._exitCode === 0;
+  }
+
+  public get consoleContent(): string {
+    return this._consoleContent;
+  }
+
+  build(commit?: string): Attempt {
+    const files = new Map<string, string | null>();
+    files['exitCode'] = this._exitCode ?? null;
+    files['commit'] = commit;
+    return {
+      id: this.id,
+      date: this.date,
+      files: files,
+      score: this.score,
+      treated: this.treated,
+      consoleContent: this.consoleContent,
+      asubmit: this.isASubmit,
+      storyId: this.storyId,
+      solved: this.isSolved,
+      userId: this.userId,
+      missionId: this.missionId,
+    };
+  }
+}
+
+export interface Attempt {
+  storyId?: string;
+  score: number;
+  solved: boolean;
+  userId: string;
+  missionId: string;
+  treated: false;
+  id?: string;
+  date?: number;
+  consoleContent: string;
+  files?: Map<string, string | null>;
+  asubmit: boolean;
+}
diff --git a/deadlock-plugins/deadlock-extension/src/model/mission.ts b/deadlock-plugins/deadlock-extension/src/model/mission.ts
index 3595f571..21df72d6 100644
--- a/deadlock-plugins/deadlock-extension/src/model/mission.ts
+++ b/deadlock-plugins/deadlock-extension/src/model/mission.ts
@@ -1,28 +1,62 @@
+import { readFileSync } from 'fs';
+import { parse } from 'yaml';
+import { MISSION_FILE_IC } from '../core/config';
+
+type level =
+  | 'Jajarbinks' // le plus facile
+  | 'Ewok'
+  | 'Padawan'
+  | 'Jedi'
+  | 'Master'; // le plus difficile
+
 interface Mission {
-  version: string;
-  name: string;
-  label: string;
-  description: string;
-  level:
-    | 'Jajarbinks' // le plus facile
-    | 'Ewok'
-    | 'Padawan'
-    | 'Jedi'
-    | 'Master'; // le plus difficile
-  type: 'DESKTOP';
-  xp: Map<string, number>;
-  desktop: {
+  readonly version: string;
+  readonly name: string;
+  readonly label: string;
+  readonly description: string;
+  readonly level: level;
+  readonly type: 'DESKTOP';
+  readonly xp: Map<string, number>;
+  readonly desktop: {
     scripts: MissionCommand[];
   };
 }
 
-export class MissionCommand {
-  constructor(
-    public readonly name: string,
-    public readonly command: string,
-    public readonly description?: string,
-    public readonly type: 'submit' | 'setup' | 'run' = 'run',
-  ) {}
+export class MissionHandler implements Mission {
+  public readonly version: string;
+  public readonly name: string;
+  public readonly label: string;
+  public readonly description: string;
+  public readonly level: level;
+  public readonly type: 'DESKTOP';
+  public readonly desktop: { scripts: MissionCommand[] };
+  public readonly xp: Map<string, number>;
+  private static _instance: MissionHandler;
+
+  public static get instance(): MissionHandler {
+    if (!MissionHandler._instance) {
+      MissionHandler._instance = new MissionHandler(parse(readFileSync(MISSION_FILE_IC, 'utf-8')));
+    }
+    return MissionHandler._instance;
+  }
+
+  private constructor(mission: Mission) {
+    this.version = mission.version;
+    this.name = mission.name;
+    this.label = mission.label;
+    this.description = mission.description;
+    this.level = mission.level;
+    this.type = mission.type;
+    this.desktop = mission.desktop;
+    this.xp = mission.xp;
+  }
+}
+
+export interface MissionCommand {
+  readonly name: string;
+  readonly command: string;
+  readonly description?: string;
+  type?: 'submit' | 'setup' | 'run';
 }
 
 export default Mission;
diff --git a/deadlock-plugins/deadlock-extension/src/recorder/utils/gitea.ts b/deadlock-plugins/deadlock-extension/src/recorder/utils/gitea.ts
index 196e081e..f307b2b5 100644
--- a/deadlock-plugins/deadlock-extension/src/recorder/utils/gitea.ts
+++ b/deadlock-plugins/deadlock-extension/src/recorder/utils/gitea.ts
@@ -4,7 +4,7 @@ import { format } from 'date-fns';
 import { execSync } from 'child_process';
 import { existsSync, promises } from 'fs';
 import { join } from 'path';
-import { GitError } from 'simple-git';
+import { CommitResult, GitError } from 'simple-git';
 import { queue } from 'async';
 import { extensionWarn, recorderError as error, recorderLog as log } from './log';
 import { clearFilesExceptGit, copyGitUserFiles } from './workdir';
@@ -17,25 +17,35 @@ const gitQueue = queue(async (task: CallableFunction) => {
   await task();
 }, 1);
 
-async function pushOnCommitQueue(gitMission: GitMission, from: 'Run' | 'Auto' | 'HttpServer') {
+async function pushOnCommitQueue(
+  gitMission: GitMission,
+  from: 'Run' | 'Auto' | 'HttpServer',
+  callback?: (commitResult: CommitResult) => void,
+) {
   if (from === 'Run') {
     gitQueue.push(async () => {
-      await mergeMaster(gitMission);
+      const commitResult = await mergeMaster(gitMission);
+      callback?.(commitResult);
     });
   } else if (gitQueue.length() == 0) {
     gitQueue.push(async () => {
-      await commitAndPushCode(gitMission);
+      const commitResult = await commitAndPushCode(gitMission);
+      callback?.(commitResult);
     });
   }
 }
 
-export async function pushOnCommitQueueIfNotReviewing(gitMission: GitMission, from: 'Run' | 'Auto' | 'HttpServer') {
+export async function pushOnCommitQueueIfNotReviewing(
+  gitMission: GitMission,
+  from: 'Run' | 'Auto' | 'HttpServer',
+  callback?: (commitResult: CommitResult) => void,
+) {
   if (!UserMission.getInstance().isReviewing()) {
-    await pushOnCommitQueue(gitMission, from);
+    await pushOnCommitQueue(gitMission, from, callback);
   }
 }
 
-async function commitAndPushCode(gitMission: GitMission) {
+async function commitAndPushCode(gitMission: GitMission): Promise<CommitResult> {
   try {
     await gitMission.checkout('live');
     await clearFilesExceptGit(GITEA_PATH_IC);
@@ -48,9 +58,10 @@ async function commitAndPushCode(gitMission: GitMission) {
 
     const currentDate = format(new Date(), "HH'H'mm'_'dd/LL/y");
     await gitMission.addAll();
-    await gitMission.commit(currentDate);
+    const commitResult = await gitMission.commit(currentDate);
     await gitMission.push();
     log('Commit & push done.');
+    return commitResult;
   } catch (e) {
     if (e instanceof GitError) {
       error(`[${e.task?.commands}] cannot commitAndPush`);
@@ -63,7 +74,7 @@ async function commitAndPushCode(gitMission: GitMission) {
   }
 }
 
-async function mergeMaster(gitMission: GitMission) {
+async function mergeMaster(gitMission: GitMission): Promise<CommitResult> {
   try {
     log('Merging live to master');
 
@@ -71,10 +82,11 @@ async function mergeMaster(gitMission: GitMission) {
 
     await gitMission.checkout();
     await gitMission.merge(['--squash', '-X', 'theirs', 'live']);
-    await gitMission.commit(currentDate);
+    const commitResult = await gitMission.commit(currentDate);
     await gitMission.push();
     log('Merge to master done');
     await gitMission.checkout('live');
+    return commitResult;
   } catch (e) {
     if (e instanceof GitError) {
       error(`[${e.task?.commands}] cannot commitAndPush`);
@@ -98,5 +110,6 @@ export async function getIgnorePatternFromIgnoreFile(ignoreFile: string): Promis
   const filesPatterns = lines
     .filter((line) => line.trimLeft().startsWith('#') || !!line.trim())
     .map((line_1) => `**/${line_1}`);
-  return [...filesPatterns, '.git'];
+  filesPatterns.push('.git');
+  return filesPatterns;
 }
diff --git a/deadlock-plugins/deadlock-extension/src/view/CommandTree.ts b/deadlock-plugins/deadlock-extension/src/view/CommandTree.ts
index cdb024f0..e172e422 100644
--- a/deadlock-plugins/deadlock-extension/src/view/CommandTree.ts
+++ b/deadlock-plugins/deadlock-extension/src/view/CommandTree.ts
@@ -1,24 +1,14 @@
-import isDocker from '../core/utils/isdocker';
-import Mission, { MissionCommand } from '../model/mission';
-import { parse } from 'yaml';
-import { readFileSync } from 'fs';
-import { commands, ExtensionContext, ThemeColor, ThemeIcon, TreeDataProvider, TreeItem, window } from 'vscode';
-import { pushOnCommitQueueIfNotReviewing } from '../recorder/utils/gitea';
+import { MissionCommand, MissionHandler } from '../model/mission';
+import { commands, Event, EventEmitter, ThemeColor, ThemeIcon, TreeDataProvider, TreeItem, window } from 'vscode';
 import Recorder from '../recorder/recorder';
+import { MISSION_PATH_IC } from '../core/config';
+import { ChildProcessWithoutNullStreams, spawn } from 'child_process';
+import AttemptBuilder from '../model/attempt';
+import ApiService from '../core/api.service';
+import UserMission from '../core/mission/model/userMission';
+import { pushOnCommitQueueIfNotReviewing } from '../recorder/utils/gitea';
 
 export class CommandTreeProvider implements TreeDataProvider<CommandItem> {
-  challenge: Mission;
-  context: ExtensionContext;
-  constructor(context: ExtensionContext) {
-    this.context = context;
-    if (isDocker()) {
-      this.challenge = parse(readFileSync('/deadlock/challenge.yaml', 'utf8'));
-    } else {
-      window.showErrorMessage('No mission found');
-      throw new Error('No mission found');
-    }
-  }
-
   getTreeItem(element: CommandItem): TreeItem {
     return element;
   }
@@ -27,18 +17,58 @@ export class CommandTreeProvider implements TreeDataProvider<CommandItem> {
     if (element) {
       return Promise.resolve([]);
     } else {
-      return Promise.resolve(this.challenge.desktop?.scripts?.map((script) => new CommandItem(script)) ?? []);
+      return Promise.resolve(
+        MissionHandler.instance.desktop?.scripts?.map((script) => new CommandItem(script, this)) ?? [],
+      );
     }
   }
+
+  private _onDidChangeTreeData: EventEmitter<CommandItem> = new EventEmitter<CommandItem>();
+  readonly onDidChangeTreeData: Event<CommandItem> = this._onDidChangeTreeData.event;
+
+  refresh(commandItem: CommandItem): void {
+    this._onDidChangeTreeData.fire(commandItem);
+  }
 }
 
-const terminalName = `Deadlock`;
+const outputPrefix = `Deadlock`;
 
 class CommandItem extends TreeItem {
-  constructor(private readonly missionCommand: MissionCommand) {
+  public readonly parced: {
+    cmd: string;
+    args: string[];
+  };
+
+  private _isRunning: boolean;
+  private spawnHandler?: ChildProcessWithoutNullStreams;
+
+  public get isRunning(): boolean {
+    return this._isRunning;
+  }
+
+  private set isRunning(value: boolean) {
+    this._isRunning = value;
+    if (this._isRunning) {
+      this.apply();
+    } else {
+      if (this.spawnHandler?.exitCode === null) {
+        this.spawnHandler?.kill();
+      }
+      this.spawnHandler = undefined;
+    }
+    this.iconPath = new ThemeIcon(
+      this._isRunning ? 'notebook-stop' : 'notebook-execute',
+      new ThemeColor(this.missionCommand.type === 'submit' ? 'debugIcon.stopForeground' : 'debugIcon.startForeground'),
+    );
+    this.parent.refresh(this);
+  }
+
+  constructor(private readonly missionCommand: MissionCommand, private readonly parent: CommandTreeProvider) {
     super(missionCommand.name);
+    missionCommand.type = missionCommand.type ?? 'run';
     this.tooltip = missionCommand.description ?? missionCommand.name;
     this.description = missionCommand.description ?? missionCommand.name;
+    this._isRunning = false;
     this.iconPath = new ThemeIcon(
       'notebook-execute',
       new ThemeColor(missionCommand.type === 'submit' ? 'debugIcon.stopForeground' : 'debugIcon.startForeground'),
@@ -48,32 +78,56 @@ class CommandItem extends TreeItem {
       command: missionCommand.name,
       tooltip: this.tooltip,
     };
+    const split = missionCommand.command.split(' ').map((s) => s.replace(/\$([^$/]+)/, (_, n) => process.env[n] ?? ''));
+    this.parced = { cmd: split[0], args: split.slice(1) };
     commands.registerCommand(missionCommand.name, () => {
-      if (missionCommand.type === 'submit') {
-        window
-          .showInformationMessage('Êtes-vous sûr de soumettre votre solution ?', { modal: true }, 'Oui', 'Non')
-          .then((answer) => {
-            if (answer === 'Oui') {
-              this.apply();
-            }
-          });
+      if (this.isRunning) {
+        this.isRunning = false;
       } else {
-        this.apply();
+        if (missionCommand.type === 'submit') {
+          window
+            .showInformationMessage('Êtes-vous sûr de soumettre votre solution ?', { modal: true }, 'Oui')
+            .then((answer) => {
+              if (answer === 'Oui') {
+                this.isRunning = true;
+              }
+            });
+        } else {
+          this.isRunning = true;
+        }
       }
     });
   }
 
-  apply() {
-    const terminal =
-      window.terminals.filter((terminal) => terminal.name === terminalName)[0] ??
-      window.createTerminal({
-        cwd: process.env.WORKDIR,
-        name: terminalName,
-      });
-    terminal.sendText(this.missionCommand.command);
-    terminal.show();
-    if (this.missionCommand.type === 'submit' || this.missionCommand.type === 'run') {
-      pushOnCommitQueueIfNotReviewing(Recorder.instance.gitMission, 'Run');
-    }
+  async apply() {
+    const output = window.createOutputChannel(`${outputPrefix} - ${this.missionCommand.name}`);
+    output.show();
+    const attempt = new AttemptBuilder(
+      (await ApiService.getInstance().getCurrentUser()).id,
+      UserMission.getInstance().missionId,
+      this.missionCommand.type === 'submit',
+      UserMission.getInstance().storyId,
+    );
+    this.spawnHandler = spawn(this.parced.cmd, this.parced.args, {
+      cwd: MISSION_PATH_IC,
+    });
+    this.spawnHandler.stdout.addListener('data', (data) => {
+      output.append(data.toString());
+      attempt.addLog(data.toString());
+    });
+    this.spawnHandler.stderr.addListener('data', (data) => {
+      output.append(data.toString());
+      attempt.addLog(data.toString());
+    });
+    this.spawnHandler.addListener('exit', async (code) => {
+      output.appendLine(`${outputPrefix} - ${this.missionCommand.name} - exited with code ${code}`);
+      attempt.setExitCode(code ?? undefined);
+      if (this.missionCommand.type === 'submit' || this.missionCommand.type === 'run') {
+        pushOnCommitQueueIfNotReviewing(Recorder.instance.gitMission, 'Run', async (commit) => {
+          await ApiService.getInstance().submitAttempt(attempt.build(commit?.commit));
+        });
+      }
+      this.isRunning = false;
+    });
   }
 }
diff --git a/deadlock-plugins/deadlock-extension/src/view/startedMissionsView.ts b/deadlock-plugins/deadlock-extension/src/view/startedMissionsView.ts
index c0354e5a..54efd4b9 100644
--- a/deadlock-plugins/deadlock-extension/src/view/startedMissionsView.ts
+++ b/deadlock-plugins/deadlock-extension/src/view/startedMissionsView.ts
@@ -27,8 +27,8 @@ export default class StartedMissionsView implements WebviewViewProvider {
     switch (message.command) {
       case 'openMission': {
         const path = join(missionWorkdir, message.mission, '.config', 'user-challenge.json');
-        const userChallenge: UserMission = JSON.parse(readFileSync(path, 'utf8'));
-        Controller.getInstance().launchMission(userChallenge.missionId, userChallenge.missionVersion);
+        const userMission: UserMission = JSON.parse(readFileSync(path, 'utf8'));
+        Controller.getInstance().launchMission(userMission.missionId, userMission.missionVersion, userMission.storyId);
         return;
       }
     }
-- 
GitLab