Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found
Select Git revision
Loading items

Target

Select target project
  • deadlock-public/deadlock-desktop
1 result
Select Git revision
Loading items
Show changes

Commits on Source 24

Showing
with 1831 additions and 111 deletions
......@@ -14,7 +14,7 @@ build:
- docker:18.09.6-dind
parallel:
matrix:
- VERSION: [code, kube]
- VERSION: [code, kube, desktop]
script:
- ./build.sh $TAG $VERSION $CI_REGISTRY_IMAGE
- docker login -u gitlab-ci-token -p $CI_JOB_TOKEN registry.e-biz.fr
......
FROM node:alpine3.15
RUN apk update
RUN apk --no-cache add vim && apk --no-cache add nano \
&& apk --no-cache add rsync && apk --no-cache add sudo \
&& apk --no-cache add bash && apk --no-cache add openssh \
&& apk --no-cache add git && apk add --update --no-cache python3 && ln -sf python3 /usr/bin/python
## User account
RUN addgroup -S sudo && adduser --disabled-password --gecos '' deadlock && \
adduser deadlock sudo && \
echo '%sudo ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers
COPY setup_trace_desktop.py setup_trace.py
RUN chmod 700 setup_trace.py
RUN chown deadlock setup_trace.py
COPY recorder-out deadlock/
COPY .gitignore_recorder deadlock/.gitignore
COPY start.desktop.sh .
RUN chmod 504 deadlock/ -R
RUN chmod 500 start.desktop.sh
RUN mkdir /project && mkdir /tmp/.ssh && mkdir /home/deadlock/mission
RUN chown deadlock:deadlock /home/deadlock
ENTRYPOINT ["bash", "start.desktop.sh"]
\ No newline at end of file
......@@ -37,7 +37,7 @@
"webpack-cli": "^4.9.1"
},
"engines": {
"vscode": "^1.63.0"
"vscode": "^1.66.0"
}
},
"node_modules/@babel/code-frame": {
......
......@@ -64,7 +64,7 @@
{
"id": "help",
"name": "Help",
"visibility": "collapsed"
"visibility": "visible"
}
]
},
......
......@@ -2,4 +2,9 @@ export const KEYCLOAK_DEVICE_AUTH_URL =
'https://auth.deadlock.io/auth/realms/Deadlock/protocol/openid-connect/auth/device';
export const KEYCLOAK_TOKEN_CREATE_URL = 'https://auth.deadlock.io/auth/realms/Deadlock/protocol/openid-connect/token';
export const KEYCLOAK_USER_INFO_URL = 'https://auth.deadlock.io/auth/realms/Deadlock/protocol/openid-connect/userinfo';
export const REGISTRY_MISSION_URL = 'registry.e-biz.fr/deadlock/deadlock-challenges';
export const REJECT_UNAUTHORIZED = true;
export const ENABLE_HTTP_SERVER = false;
export const ENABLE_AUTOMATIC_SAVE = true;
export const RECORDER_HTTP_SERVER_PORT = 8751;
export const RECORDER_HTTP_SERVER_URL = `http://localhost:${RECORDER_HTTP_SERVER_PORT}`;
......@@ -4,4 +4,8 @@ export const KEYCLOAK_TOKEN_CREATE_URL =
'https://auth.staging.deadlock.io/auth/realms/Deadlock/protocol/openid-connect/token';
export const KEYCLOAK_USER_INFO_URL =
'https://auth.staging.deadlock.io/auth/realms/Deadlock/protocol/openid-connect/userinfo';
export const REGISTRY_MISSION_URL = 'registry.e-biz.fr/deadlock/deadlock-challenges';
export const REJECT_UNAUTHORIZED = true;
export const ENABLE_HTTP_SERVER = false;
export const HTTP_SERVER_PORT = 8751;
export const HTTP_SERVER_URL = `http://localhost:${HTTP_SERVER_PORT}`;
......@@ -4,4 +4,9 @@ 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.e-biz.fr/deadlock/deadlock-challenges';
export const REJECT_UNAUTHORIZED = false;
export const ENABLE_HTTP_SERVER = false;
export const ENABLE_AUTOMATIC_SAVE = true;
export const HTTP_SERVER_PORT = 8751;
export const HTTP_SERVER_URL = `http://localhost:${HTTP_SERVER_PORT}`;
......@@ -3,17 +3,17 @@ import * as path from 'path';
const homeDir = os.homedir();
// if we are on container, means the directory will depend differently
const onContainer = homeDir.includes('theia') || homeDir.includes('root');
const onContainer = homeDir.includes('theia') || homeDir.includes('root') || homeDir.includes('deadlock');
const deadlockExtensionPath = path.join(homeDir, 'deadlock-extension');
export const PROJECT_SRC_PATH = onContainer ? '/project' : path.join(homeDir, 'deadlock-extension', '/project');
export const PROJECT_THEIA_PATH = onContainer
? path.join('/home/project')
export const PROJECT_DEADLOCK_DESKTOP_PATH = onContainer
? path.join('/home/deadlock/mission')
: path.join(deadlockExtensionPath, 'project-theia');
export const DOCS_PATH = path.join(path.join(onContainer ? '/home/theia' : deadlockExtensionPath), 'docs');
export const DOCS_PATH = path.join(path.join(onContainer ? '/home/deadlock' : deadlockExtensionPath), 'docs');
export const CONFIG_PATH = onContainer ? '/home/config/' : path.join(deadlockExtensionPath, 'config');
......@@ -21,8 +21,8 @@ export const USER_CHALLENGE_PATH = path.join(CONFIG_PATH, 'user-challenge.json')
export const BRIEFING_FILE_NAME = 'briefing.md';
export const ENV_FILE_PATH = path.join(PROJECT_THEIA_PATH, '/.env');
export const ENV_FILE_PATH = path.join(PROJECT_DEADLOCK_DESKTOP_PATH, '/.env');
export const BASHRC_PATH = path.join(homeDir, '/.bashrc');
export const SERVICES_PATHS_PATH = path.join(PROJECT_THEIA_PATH, '/paths.json');
export const SERVICES_PATHS_PATH = path.join(PROJECT_DEADLOCK_DESKTOP_PATH, '/paths.json');
import * as vscode from 'vscode';
import { KEYCLOAK_DEVICE_AUTH_URL, KEYCLOAK_TOKEN_CREATE_URL, KEYCLOAK_USER_INFO_URL } from '../config';
import {
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 BriefingView from '../view/briefingView';
import QuickSetupView from '../view/quickSetupView';
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';
export default class Controller {
public connection: KeycloakOAuth2DeviceFlowConnection;
......@@ -13,7 +20,9 @@ export default class Controller {
private briefingView: BriefingView;
private quickSetupView: QuickSetupView;
private extensionStore: ExtensionStore;
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);
......@@ -22,7 +31,6 @@ export default class Controller {
KEYCLOAK_TOKEN_CREATE_URL,
KEYCLOAK_USER_INFO_URL,
);
this.extensionStore = ExtensionStore.getInstance();
this.init();
}
......@@ -32,10 +40,11 @@ export default class Controller {
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'));
that.launchMission(queryParams.get('missionId'), queryParams.get('missionVersion'));
break;
default:
......@@ -85,8 +94,8 @@ export default class Controller {
vscode.commands.executeCommand(OPEN_URL_IN_BROWSER_COMMAND.cmd, vscode.Uri.parse(url));
}
public async launchMission(missionId: string | null) {
if (missionId) {
public async launchMission(missionId: string | null, missionVersion: string | null) {
if (missionId && missionVersion) {
vscode.window.showInformationMessage(`vous lancez la mission ${missionId}`);
const hadMissionWorkdir = this.extensionStore.getMissionWorkdir() !== undefined;
if (!hadMissionWorkdir) {
......@@ -96,12 +105,24 @@ export default class Controller {
const hadBeenConnected = (await this.extensionStore.getAccessToken()) !== undefined;
if (!hadBeenConnected) {
this.authenticate();
await this.authenticate();
vscode.window.showInformationMessage('Nouvelle connexion validée');
} else {
vscode.window.showInformationMessage('Déjà connecté: session récupérée');
}
// 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,
});
vscode.window.showInformationMessage(
'opening inside folder ' + this.extensionStore.getMissionWorkdir()! + '/' + missionId,
);
await mission.setup({});
await mission.openEditorInFolder();
vscode.commands.executeCommand(OPEN_QUICK_SETUP_COMMAND.cmd);
}
}
......
......@@ -24,18 +24,12 @@ export default class ExtensionStore {
if (await this.secretStorage.get(StoreKey.RefreshTokenKey)) this.secretStorage.delete(StoreKey.RefreshTokenKey);
}
public static getInstance(): ExtensionStore {
if (!ExtensionStore.instance) {
throw new Error('ExtensionStore should be initiate with a storage first time');
}
return ExtensionStore.instance;
}
public static createInstance(context: ExtensionContext) {
public static getInstance(context?: ExtensionContext): ExtensionStore {
if (!ExtensionStore.instance) {
if (!context) throw new Error('ExtensionStore should be initiate with a storage first time');
ExtensionStore.instance = new ExtensionStore(context);
}
return ExtensionStore.instance;
}
......
......@@ -7,7 +7,12 @@ const util = require('util');
const exec = util.promisify(require('child_process').exec);
const DEFAULT_REMOTE = 'origin';
const DEFAULT_BRANCH = 'master';
export enum Branch {
MASTER = 'master',
DEFAULT = MASTER,
LIVE = 'live',
}
export default class GitMission {
private git: SimpleGit;
......@@ -47,6 +52,7 @@ export default class GitMission {
console.log('Init Git mission..');
const remote = await this.readRemote();
if (remote === DEFAULT_REMOTE) {
return Promise.resolve(this);
}
......@@ -54,6 +60,7 @@ export default class GitMission {
const remotePath = this.getRemotePath();
await this.git.init();
await this.git.addRemote(DEFAULT_REMOTE, remotePath);
await this.git.addConfig('user.email', this.userConfig.getCurrentUserDetails().email, false, 'system');
await this.git.addConfig(
......@@ -87,20 +94,47 @@ export default class GitMission {
return this.git.fetch(DEFAULT_REMOTE);
}
pull() {
return this.git.pull(DEFAULT_REMOTE, DEFAULT_BRANCH, { '--rebase': 'true' });
pull(branch?: string) {
return this.git.pull(DEFAULT_REMOTE, branch ?? Branch.DEFAULT, { '--rebase': 'true' });
}
addAll() {
return this.git.add('.');
}
commit(message) {
return this.git.commit(message);
commit(message: string, options?: string[]) {
return this.git.commit(message, options ?? []);
}
push() {
return this.git.push(DEFAULT_REMOTE, DEFAULT_BRANCH);
return this.git.push(DEFAULT_REMOTE);
}
createLocalBranch(branch: string) {
return this.git.checkout(['-b', branch]);
}
createRemoteBranch(branch: string) {
return this.git.push(['-u', DEFAULT_REMOTE, branch]);
}
setUpstream(branch: string) {
return this.git.branch(['-u', DEFAULT_REMOTE, branch]);
}
createBranch(branch: string) {
const createBranchLocally = this.createLocalBranch(branch);
const createRemoteBranch = this.createRemoteBranch(branch);
return Promise.all([createBranchLocally, createRemoteBranch]);
}
checkout(branch: string) {
return this.git.checkout(branch);
}
merge(options?: string[]) {
return this.git.merge(options ?? []);
}
async isRemoteRepoExist() {
......
import * as https from 'https';
import fetch, { Response } from 'node-fetch';
import { REJECT_UNAUTHORIZED } from '../config';
import { HttpStatusCode } from '../customTypings/HttpStatusCode';
import { TokenFetchErrorCode } from '../customTypings/KeycloakAPITypes';
import { error as err, log } from '../recorder/utils';
import { REJECT_UNAUTHORIZED } from '../config';
process.env['NODE_TLS_REJECT_UNAUTHORIZED'] = REJECT_UNAUTHORIZED ? '1' : '0'; // TODO: remove when SSL will work
process.env['NODE_TLS_REJECT_UNAUTHORIZED'] = REJECT_UNAUTHORIZED ? '1' : '0';
export default class KeycloakOAuth2DeviceFlowConnection {
private waitDuration: WaitDuration;
private accessToken: string;
private refreshToken: string;
private deviceAuthorizationRequestResponseData: DeviceAuthorizationRequestResponseData;
private deviceAuthorizationRequestResponse: DeviceAuthorizationRequestResponse;
constructor(private deviceUrl: string, private tokenUrl: string, private userInfoUrl?: string) {
constructor(private deviceUrl: string, private tokenUrl: string, private userInfoUrl: string) {
/*
* Arbitrary durations to wait in seconds \
* Waiting time between requests should be increasing
* if Keycloak api reponds with error code `slow_down` \
* otherwise, it will continue to respond with this error code. \
* The minimal waiting duration has been chosen to not be too short: \
* Keycloak may saturate and crash due to too many requests.
*/
this.waitDuration = new WaitDuration([5_000, 5_000, 5_000, 10_000, 10_000, 10_000, 30_000, 30_000, 100_000]);
this.accessToken = '';
this.refreshToken = '';
this.deviceAuthorizationRequestResponseData = {};
this.deviceAuthorizationRequestResponse = {};
}
/**
* userInfoUrl must be passed in constructor in order to use this
* @param accessToken
* @returns Promise
*/
public async tokenIsValid(accessToken: string) {
const url = this.userInfoUrl;
if (!url) {
return Promise.reject('tokenIsValid: missing user_info API endpoint');
}
if (!accessToken) {
return Promise.resolve(false);
}
const tokenValidationRequestResponse: Response = await fetch(url, {
const tokenValidationRequestResponse: Response = await fetch(this.userInfoUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
Authorization: `Bearer ${accessToken}`,
},
body: '',
agent: new https.Agent({ rejectUnauthorized: REJECT_UNAUTHORIZED }), // TODO: remove when SSL will work
agent: new https.Agent({ rejectUnauthorized: REJECT_UNAUTHORIZED }),
});
const tokenValidationRequestResponseCode = tokenValidationRequestResponse.status;
switch (tokenValidationRequestResponseCode) {
......@@ -48,15 +51,16 @@ export default class KeycloakOAuth2DeviceFlowConnection {
return Promise.resolve(true);
}
case HttpStatusCode.BAD_REQUEST: {
const badRequestResponse: FailedAuthenticationReponseData =
(await tokenValidationRequestResponse.json()) as FailedAuthenticationReponseData;
throw new Error('tokenIsValid: ' + badRequestResponse.error);
const badRequestResponse = (await tokenValidationRequestResponse.json()) as FailedAuthenticationResponse;
err(`${badRequestResponse.error!}: ${badRequestResponse.error_description}`);
throw new Error(`${badRequestResponse.error}: ${badRequestResponse.error_description}`);
}
case HttpStatusCode.UNAUTHORIZED: {
return Promise.resolve(false);
}
default: {
throw new Error(`tokenIsValid: Unhandled HTTP status: ${tokenValidationRequestResponseCode}`);
err(`Unhandled HTTP status: ${tokenValidationRequestResponseCode}`);
throw new Error(`Unhandled HTTP status: ${tokenValidationRequestResponseCode}`);
}
}
}
......@@ -77,17 +81,17 @@ export default class KeycloakOAuth2DeviceFlowConnection {
});
return Promise.resolve({ accessToken: this.accessToken, refreshToken: this.refreshToken });
}
if (!this._deviceIsRegistered()) {
if (!this.deviceIsRegistered()) {
await this.registerDevice();
}
try {
openLink(this.deviceAuthorizationRequestResponseData.verification_uri_complete!);
openLink(this.deviceAuthorizationRequestResponse.verification_uri_complete!);
await this.createUserAuthentication({
url: this.tokenUrl,
body: (() => {
const params = new URLSearchParams();
params.append('response_type', 'token');
params.append('device_code', this.deviceAuthorizationRequestResponseData.device_code ?? '');
params.append('device_code', this.deviceAuthorizationRequestResponse.device_code ?? '');
params.append('grant_type', 'urn:ietf:params:oauth:grant-type:device_code');
params.append('client_id', 'deadlock-desktop');
return params.toString();
......@@ -95,13 +99,12 @@ export default class KeycloakOAuth2DeviceFlowConnection {
});
return Promise.resolve({ accessToken: this.accessToken, refreshToken: this.refreshToken });
} catch (error) {
err(error);
return Promise.reject(error);
}
}
private _deviceIsRegistered(): boolean {
return !!this.deviceAuthorizationRequestResponseData.device_code;
private deviceIsRegistered(): boolean {
return !!this.deviceAuthorizationRequestResponse.device_code;
}
public async registerDevice() {
......@@ -114,8 +117,8 @@ export default class KeycloakOAuth2DeviceFlowConnection {
return params.toString();
})(),
});
this.deviceAuthorizationRequestResponseData =
(await deviceAuthorizationRequestResponse.json()) as DeviceAuthorizationRequestResponseData;
this.deviceAuthorizationRequestResponse =
(await deviceAuthorizationRequestResponse.json()) as DeviceAuthorizationRequestResponse;
}
private async createDeviceAuthorization(args: { url: string; body: string }): Promise<Response> {
......@@ -127,14 +130,14 @@ export default class KeycloakOAuth2DeviceFlowConnection {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: body,
agent: new https.Agent({ rejectUnauthorized: REJECT_UNAUTHORIZED }), // TODO: remove when SSL will work
agent: new https.Agent({ rejectUnauthorized: REJECT_UNAUTHORIZED }),
});
}
/**
*
* @param args API URL endpoint to ask for a new token & request form parameters
* @throw Error containing Keycloak API error_code
* @throw Error containing Keycloak API `error_code`
*/
private async createUserAuthentication(args: { url: string; body: string }) {
const { url, body } = args;
......@@ -147,26 +150,41 @@ export default class KeycloakOAuth2DeviceFlowConnection {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: body,
agent: new https.Agent({ rejectUnauthorized: REJECT_UNAUTHORIZED }), // TODO: remove when SSL will work
agent: new https.Agent({ rejectUnauthorized: REJECT_UNAUTHORIZED }),
});
userAuthenticationRequestResponseCode = userAuthenticationRequestResponse.status;
switch (userAuthenticationRequestResponseCode) {
case HttpStatusCode.BAD_REQUEST:
{
const badRequestResponse =
(await userAuthenticationRequestResponse.json()) as FailedAuthenticationReponseData;
log(`${badRequestResponse.error!}: ${badRequestResponse.error_description}`);
case HttpStatusCode.BAD_REQUEST: {
await this.onUserAuthenticationBadRequest(userAuthenticationRequestResponse);
break;
}
case HttpStatusCode.OK: {
await this.onUserAuthenticationSuccess(userAuthenticationRequestResponse);
break;
}
default: {
err(`Unhandled HTTP status: ${userAuthenticationRequestResponseCode}`);
throw new Error(`Unhandled HTTP status: ${userAuthenticationRequestResponseCode}`);
}
}
}
}
private async onUserAuthenticationBadRequest(userAuthenticationRequestResponse: Response) {
const badRequestResponse = (await userAuthenticationRequestResponse.json()) as FailedAuthenticationResponse;
const errorCode = TokenFetchErrorCode[badRequestResponse.error!];
switch (errorCode) {
case TokenFetchErrorCode.invalid_client:
case TokenFetchErrorCode.invalid_grant:
case TokenFetchErrorCode.unsupported_grant_type: {
err(`${badRequestResponse.error!}: ${badRequestResponse.error_description}`);
throw new Error('createUserAuthentication: ' + errorCode);
}
case TokenFetchErrorCode.authorization_pending: {
// I have to keep this `await sleep` and the while in the same function context
await sleep(this.waitDuration.getCurrentDuration());
continue;
break;
}
case TokenFetchErrorCode.slow_down: {
this.waitDuration.increase();
......@@ -174,23 +192,18 @@ export default class KeycloakOAuth2DeviceFlowConnection {
break;
}
default: {
throw new Error(`createUserAuthentication: Unhandled error code [ ${badRequestResponse.error} ]`);
err(`${badRequestResponse.error!}: ${badRequestResponse.error_description}`);
throw new Error(
`Unhandled error code [ ${badRequestResponse.error!}: ${badRequestResponse.error_description} ]`,
);
}
}
}
break;
case HttpStatusCode.OK: {
const successRequestResponse =
(await userAuthenticationRequestResponse.json()) as SuccessfulAuthenticationResponseData;
private async onUserAuthenticationSuccess(userAuthenticationRequestResponse: Response) {
const successRequestResponse = (await userAuthenticationRequestResponse.json()) as SuccessfulAuthenticationResponse;
this.accessToken = successRequestResponse.access_token ?? '';
this.refreshToken = successRequestResponse.refresh_token ?? '';
break;
}
default: {
throw new Error(`tokenIsValid: Unhandled HTTP status: ${userAuthenticationRequestResponseCode}`);
}
}
}
}
}
......@@ -198,7 +211,7 @@ export default class KeycloakOAuth2DeviceFlowConnection {
* KEEP the SAME case \
* to respect keycloak API return
*/
interface DeviceAuthorizationRequestResponseData {
interface DeviceAuthorizationRequestResponse {
device_code?: string;
user_code?: string;
verification_uri?: string;
......@@ -211,7 +224,7 @@ interface DeviceAuthorizationRequestResponseData {
* KEEP the SAME case \
* to respect keycloak API return
*/
interface SuccessfulAuthenticationResponseData {
interface SuccessfulAuthenticationResponse {
access_token?: string;
expires_in?: number;
'not-before-policy'?: number;
......@@ -222,7 +235,7 @@ interface SuccessfulAuthenticationResponseData {
token_type?: 'Bearer' | string;
}
interface FailedAuthenticationReponseData {
interface FailedAuthenticationResponse {
error?: string;
error_description?: string;
}
......
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';
/**
* {@link https://nodejs.org/api/child_process.html#child_process_child_process_exec_command_options_callback}
*/
const exec = util.promisify(_exec);
export default class Mission {
private readonly hostBaseWorkDir: string;
private readonly dockerImageURL: string;
private readonly hostMissionDir: string;
private readonly hostMissionDevcontainerDir: string;
private readonly hostMissionDevcontainerFileDir: string;
private readonly hostMissionMountDir: string;
private readonly remoteUserHomeDir: string;
private readonly remoteMissionDir: any;
private readonly remoteGiteaWorkDir: string;
constructor(private params: { registryBaseURL: string; missionId: string; missionVersion: string }) {
const { registryBaseURL, missionId, missionVersion } = params;
this.hostBaseWorkDir = ExtensionStore.getInstance().getMissionWorkdir() ?? '';
this.hostMissionDir = `${this.hostBaseWorkDir}/${missionId}`;
this.hostMissionDevcontainerDir = `${this.hostMissionDir}/.devcontainer`;
this.hostMissionDevcontainerFileDir = `${this.hostMissionDevcontainerDir}/devcontainer.json`;
this.hostMissionMountDir = `${this.hostMissionDir}/mounted`;
this.dockerImageURL = getDockerImageURL(registryBaseURL, missionId, missionVersion);
this.remoteUserHomeDir = '/home/deadlock';
this.remoteMissionDir = `${this.remoteUserHomeDir}/mission`;
this.remoteGiteaWorkDir = `/project`;
}
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 });
await 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=${this.hostMissionDir}/workspace,target=${'/workspace'},type=${'volume'},consistency=${'cached'}`,
// ],
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}`,
...options,
};
return JSON.stringify(devcontainer, null, 2);
})(),
);
}
public async openEditorInFolder(arbitraryPath?: string) {
if (arbitraryPath) {
return vscode.commands.executeCommand('remote-containers.openFolder', vscode.Uri.file(arbitraryPath));
}
if (!fs.existsSync(this.hostMissionDir)) {
log('WARN missing path ', this.hostMissionDir);
await fs.promises.mkdir(this.hostMissionDir, { recursive: true });
}
await vscode.commands.executeCommand('remote-containers.openFolder', vscode.Uri.file(this.hostMissionDir));
}
}
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?;
}
import { log } from './../recorder/utils';
/**
* Example:
* {
......
import * as vscode from 'vscode';
import { SERVICES_PATHS_PATH } from './core/config';
import Controller from './core/controller';
import ExtensionStore from './core/extensionStore';
import MetadataProvider from './core/metadataProvider';
import { error } from './recorder/utils';
import { DepNodeProvider } from './theia/deadlockPanel';
......@@ -11,12 +10,12 @@ export const userConfig = new UserConfigTheia();
export async function activate(context: vscode.ExtensionContext) {
vscode.window.showInformationMessage('Bienvenue sur Deadlock!');
ExtensionStore.createInstance(context);
const controller = new Controller(context);
// @ts-ignore
const deadlockPanelProvider = new DepNodeProvider(vscode.workspace.workspaceFolders);
const workspaceFolders = vscode.workspace.workspaceFolders?.toString() ?? '';
if (!workspaceFolders) vscode.window.showInformationMessage('Pas de répertoires ouverts');
const deadlockPanelProvider = new DepNodeProvider(workspaceFolders);
vscode.window.registerTreeDataProvider('deadlockPanel', deadlockPanelProvider);
try {
......
import GitMission from '../core/gitMission';
import { error, commitAndPushCode, CommitFrom, log } from './utils';
import { error, commitAndPushCode, CommitFrom, log, updateRemote } from './utils';
const async = require('async');
const fs = require('fs');
......@@ -33,7 +33,7 @@ export default class CommandRecorder {
if (!this.commandsInProgress.has(pid)) {
this.commandsInProgress.set(pid, new Command(pid, command));
try {
this.queue.push(async () => await commitAndPushCode(this.gitMission, CommitFrom.Run));
this.queue.push(async () => await updateRemote(this.gitMission, CommitFrom.Run));
} catch (e) {
console.error('Cannot send user code to git');
console.error(e);
......@@ -47,7 +47,7 @@ export default class CommandRecorder {
let lastLineIndexWatched = 0;
setInterval(() => {
try {
const trace = fs.readFileSync('/home/theia/.bash_history', 'utf8');
const trace = fs.readFileSync('/home/deadlock/.bash_history', 'utf8');
const lines = trace.split(/\r?\n/);
this.commandsInProgress.forEach((command) => (command.still = false));
......
import { Branch } from './../core/gitMission';
import { ENABLE_AUTOMATIC_SAVE } from './../config';
import CommandRecorder from './command-recorder';
import GitMission from '../core/gitMission';
import UserConfigNode from './userConfigNode';
import { PROJECT_SRC_PATH, PROJECT_THEIA_PATH } from '../core/config';
import { PROJECT_SRC_PATH, PROJECT_DEADLOCK_DESKTOP_PATH } from '../core/config';
import { copyProjectSources, clearFilesExceptGit, log, error, renameTempToUserGitFiles } from './utils';
import UserConfig from '../core/userConfig';
import HttpServer from './services/http-server';
import { ENABLE_HTTP_SERVER } from '../config';
import FileWatcher from './services/file-watcher';
import AutomaticSave from './services/automatic-save';
export default class Recorder {
async setupProject(userConfig: UserConfig, gitMission?: GitMission) {
log('Setup user project..');
if (!userConfig.isProfessor()) {
await copyProjectSources(PROJECT_SRC_PATH, PROJECT_THEIA_PATH, ['.git/']);
await copyProjectSources(PROJECT_SRC_PATH, PROJECT_DEADLOCK_DESKTOP_PATH, ['.git/']);
if (gitMission) {
renameTempToUserGitFiles(PROJECT_THEIA_PATH, gitMission.author);
renameTempToUserGitFiles(PROJECT_DEADLOCK_DESKTOP_PATH, gitMission.author);
log('Starting CommandRecorder..');
new CommandRecorder(gitMission).run();
......@@ -21,13 +26,14 @@ export default class Recorder {
error('Cannot start command recorder, gitMission not found');
}
} else {
await copyProjectSources(PROJECT_SRC_PATH, PROJECT_THEIA_PATH);
await copyProjectSources(PROJECT_SRC_PATH, PROJECT_DEADLOCK_DESKTOP_PATH);
}
}
async setupFromRemoteRepo(gitMission: GitMission) {
log('Check if remote repo exist');
const isRemoteRepoExist = await gitMission.isRemoteRepoExist();
if (isRemoteRepoExist) {
// rm all except git directory pull remote code and setup
log('Cleaning files to pull repo');
......@@ -35,7 +41,14 @@ export default class Recorder {
await clearFilesExceptGit(PROJECT_SRC_PATH);
log('Pulling user repo');
await gitMission.pull();
await gitMission.fetch();
await gitMission.checkout(Branch.MASTER);
await gitMission.checkout(Branch.LIVE);
} else {
await gitMission.commit('initial commit', ['--allow-empty']);
await gitMission.createRemoteBranch(Branch.DEFAULT);
await gitMission.createBranch(Branch.LIVE);
}
}
......@@ -49,7 +62,11 @@ export default class Recorder {
await userConfig.init();
log('Init GitMission');
gitMission = await new GitMission(userConfig).init();
if (ENABLE_HTTP_SERVER) new HttpServer(gitMission);
await this.setupFromRemoteRepo(gitMission);
if (ENABLE_AUTOMATIC_SAVE) new AutomaticSave(PROJECT_DEADLOCK_DESKTOP_PATH, gitMission);
} catch (e) {
error('Cannot setup user repo.');
error(e);
......
......@@ -9,6 +9,10 @@
"author": "",
"license": "MIT",
"dependencies": {
"deadlock-coding": "^0.0.1"
"chokidar": "^3.5.3",
"express": "^4.18.0"
},
"devDependencies": {
"@types/express": "^4.17.13"
}
}
import UserConfigNode from './userConfigNode';
import GitMission from '../core/gitMission';
import { PROJECT_SRC_PATH, PROJECT_THEIA_PATH } from '../core/config';
import { PROJECT_SRC_PATH, PROJECT_DEADLOCK_DESKTOP_PATH } from '../core/config';
import { log, error, commitAndPushCode, CommitFrom } from './utils';
import { log, error, updateRemote, CommitFrom } from './utils';
const util = require('util');
const exec = util.promisify(require('child_process').exec);
......@@ -10,13 +10,13 @@ async function containsDiff() {
try {
// https://man7.org/linux/man-pages/man1/diff.1.html
// Exit status is 0 if inputs are the same, 1 if different, 2 if trouble.
await exec(`diff -qr ${PROJECT_SRC_PATH} ${PROJECT_THEIA_PATH}`);
await exec(`diff -qr ${PROJECT_SRC_PATH} ${PROJECT_DEADLOCK_DESKTOP_PATH}`);
// When status code is 0 exec does not fail
} catch (result) {
// when status code is > 0
if (result.code === 1) {
const stdout = result.stdout;
if (stdout.indexOf('Files ') !== -1 || stdout.indexOf('Only in /home/project/') !== -1) {
if (stdout.indexOf('Files ') !== -1 || stdout.indexOf('Only in /home/deadlock/') !== -1) {
// if user created new file or added a directory
return true;
}
......@@ -39,7 +39,7 @@ async function containsDiff() {
log('Save user code..');
const gitMission = await new GitMission(userConfig).init();
if (await containsDiff()) {
await commitAndPushCode(gitMission, CommitFrom.Auto);
await updateRemote(gitMission, CommitFrom.Auto);
}
}
} catch (e) {
......