diff --git a/recorder/.gitignore b/.gitignore_recorder similarity index 100% rename from recorder/.gitignore rename to .gitignore_recorder diff --git a/Dockerfile b/Dockerfile index c444190ceb2c540f52b5d3f8e57a38626c2c5263..94d44a9511cd648766e00a1809720973a02baf05 100644 --- a/Dockerfile +++ b/Dockerfile @@ -6,6 +6,7 @@ RUN apt update RUN apt upgrade -y RUN apt install strace -y RUN apt install rsyslog -y +RUN apt install rsync -y COPY bash.bashrc . RUN cat bash.bashrc >> /etc/bash.bashrc @@ -16,9 +17,10 @@ COPY server.js /home/theia/src-gen/backend/server.js COPY start.sh . -COPY recorder/command-recorder.js . -COPY recorder/commit.js . -COPY recorder/.gitignore . +COPY plugins/deadlock-extension/out deadlock/ +COPY plugins/deadlock-extension/package* deadlock/ +COPY .gitignore_recorder deadlock/recorder/.gitignore +RUN cd deadlock/ && npm install && cd - ENTRYPOINT ["bash", "start.sh"] diff --git a/build-plugins.sh b/build-plugins.sh index bb5e7248aae07af5ea8d11fcbc59d5ba40b4c93f..d18f136e7aae8fe4c7a5fc68b9ce090fbd7dc03f 100755 --- a/build-plugins.sh +++ b/build-plugins.sh @@ -1,5 +1,7 @@ #!/bin/sh +set -e + for dir in plugins/*/; do echo "Building $dir" cd $dir diff --git a/plugins/deadlock-extension/compile.sh b/plugins/deadlock-extension/compile.sh index 049d9f384d23efd7a13829eb77eca4ea377dd01e..e0089bbb04d730c85784ad6ac8ab3aeb4b432841 100755 --- a/plugins/deadlock-extension/compile.sh +++ b/plugins/deadlock-extension/compile.sh @@ -1,5 +1,8 @@ #!/bin/sh +#Allow to build React part to a bundle then export it to be usable +#within vsCode + cd front/ yarn install yarn build diff --git a/plugins/deadlock-extension/package-lock.json b/plugins/deadlock-extension/package-lock.json index 743451f8b85435df9b4dfacb9f573aba80e53d74..db1946e0dbd227cbd1b9e2bc208a478bb18ff29c 100644 --- a/plugins/deadlock-extension/package-lock.json +++ b/plugins/deadlock-extension/package-lock.json @@ -220,6 +220,12 @@ "color-convert": "^1.9.0" } }, + "arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true + }, "argparse": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", @@ -235,6 +241,11 @@ "integrity": "sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg==", "dev": true }, + "async": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.0.tgz", + "integrity": "sha512-TR2mEZFVOj2pLStYxLht7TyfuRzaydfpxr3k9RpHIzMgw7A64dzsdqCxH1WJyQdoe8T10nDXd9wnEigmiuHIZw==" + }, "balanced-match": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", @@ -256,6 +267,12 @@ "concat-map": "0.0.1" } }, + "buffer-from": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", + "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==", + "dev": true + }, "callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -340,6 +357,12 @@ "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=", "dev": true }, + "diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true + }, "doctrine": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", @@ -862,6 +885,12 @@ "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==", "dev": true }, + "make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true + }, "marked": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/marked/-/marked-1.1.1.tgz", @@ -1093,6 +1122,22 @@ } } }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "source-map-support": { + "version": "0.5.19", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.19.tgz", + "integrity": "sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==", + "dev": true, + "requires": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, "sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", @@ -1207,6 +1252,19 @@ "os-tmpdir": "~1.0.2" } }, + "ts-node": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-9.0.0.tgz", + "integrity": "sha512-/TqB4SnererCDR/vb4S/QvSZvzQMJN8daAslg7MeaiHvD8rDZsSfXmNeNumyZZzMned72Xoq/isQljYSt8Ynfg==", + "dev": true, + "requires": { + "arg": "^4.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "source-map-support": "^0.5.17", + "yn": "3.1.1" + } + }, "tslib": { "version": "1.11.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.11.1.tgz", @@ -1243,6 +1301,11 @@ "punycode": "^2.1.0" } }, + "uuid": { + "version": "8.3.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.1.tgz", + "integrity": "sha512-FOmRr+FmWEIG8uhZv6C2bTgEVXsHk08kE7mPlrBbEe+c3r9pjceVPgupIfNIhc4yx55H69OXANrUaSuu9eInKg==" + }, "v8-compile-cache": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.1.0.tgz", @@ -1278,6 +1341,12 @@ "requires": { "mkdirp": "^0.5.1" } + }, + "yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true } } } diff --git a/plugins/deadlock-extension/package.json b/plugins/deadlock-extension/package.json index 6d8578e5d9ae148cc1930b97616c3e8323ca6e4c..6208de62c9fd1d4024d32dd009bd434d7127ab70 100644 --- a/plugins/deadlock-extension/package.json +++ b/plugins/deadlock-extension/package.json @@ -61,16 +61,19 @@ }, "dependencies": { "@types/marked": "^1.1.0", + "async": "^3.2.0", "keycloak-js": "^11.0.0", "marked": "^1.1.1", - "simple-git": "^2.21.0" + "simple-git": "^2.21.0", + "uuid": "^8.3.1" }, "devDependencies": { "@types/node": "^12.12.0", + "@types/vscode": "^1.38.0", "@typescript-eslint/eslint-plugin": "^3.0.2", "@typescript-eslint/parser": "^3.0.2", "eslint": "^7.1.0", - "typescript": "^3.9.4", - "@types/vscode": "^1.38.0" + "ts-node": "^9.0.0", + "typescript": "^3.9.4" } } diff --git a/plugins/deadlock-extension/setup-dev-env.sh b/plugins/deadlock-extension/setup-dev-env.sh index be27979ba9c299de8b3967347c822f7e3df678a8..66a4ee53c991fe38b81de07812ea8f21f0572533 100755 --- a/plugins/deadlock-extension/setup-dev-env.sh +++ b/plugins/deadlock-extension/setup-dev-env.sh @@ -6,10 +6,12 @@ mkdir -p /home/$USER/deadlock-extension/project mkdir -p /home/$USER/deadlock-extension/project-theia mkdir -p /home/$USER/deadlock-extension/config +mkdir -p /home/$USER/deadlock-extension/docs + echo ' # Briefing dev sample HELL0 WORLD! -' > /home/$USER/deadlock-extension/project/README.md +' > /home/$USER/deadlock-extension/docs/briefing.md echo ' { "paths": { diff --git a/plugins/deadlock-extension/src/config.prod.ts b/plugins/deadlock-extension/src/config.prod.ts index 30085e122407a166159ed52f219454631f2acc70..a5dde5c8861b725970991578f6c8a4b968cda568 100644 --- a/plugins/deadlock-extension/src/config.prod.ts +++ b/plugins/deadlock-extension/src/config.prod.ts @@ -6,3 +6,6 @@ export const CONFIG_PATH = '/home/config/'; export const USER_CHALLENGE_PATH = path.join(CONFIG_PATH, 'user-challenge.json'); export const PROJECT_SRC_PATH = '/project' export const PROJECT_THEIA_PATH = path.join('/home/project/') +export const DOCS_PATH = path.join('/home/theia/docs'); + +export const BRIEFING_FILE_NAME = 'briefing.md'; \ No newline at end of file diff --git a/plugins/deadlock-extension/src/config.ts b/plugins/deadlock-extension/src/config.ts index 34d4aeff0212b5578f464fc2d1079c4ff8d50cbd..17f701d2e3448662d1e45e166e532a610faefee8 100644 --- a/plugins/deadlock-extension/src/config.ts +++ b/plugins/deadlock-extension/src/config.ts @@ -7,8 +7,13 @@ if (!process.env['HOME']) { } const home = process.env['HOME'] ? process.env['HOME'] : ""; + export const DEADLOCK_EXTENSION_PATH = path.join(home, 'deadlock-extension'); +export const DOCS_PATH = path.join(DEADLOCK_EXTENSION_PATH, 'docs'); export const CONFIG_PATH = path.join(DEADLOCK_EXTENSION_PATH, 'config'); export const USER_CHALLENGE_PATH = path.join(CONFIG_PATH, 'user-challenge.json'); export const PROJECT_SRC_PATH = path.join(DEADLOCK_EXTENSION_PATH, 'project') -export const PROJECT_THEIA_PATH = path.join(DEADLOCK_EXTENSION_PATH, 'project-theia') \ No newline at end of file +export const PROJECT_THEIA_PATH = path.join(DEADLOCK_EXTENSION_PATH, 'project-theia') + + +export const BRIEFING_FILE_NAME = 'briefing.md'; \ No newline at end of file diff --git a/plugins/deadlock-extension/src/extension.ts b/plugins/deadlock-extension/src/extension.ts index d518c1b162dc447e8a50d69dc9fb6c680ab29f6e..b26928e57a261e47f295992937e5c2975e3d4c0d 100644 --- a/plugins/deadlock-extension/src/extension.ts +++ b/plugins/deadlock-extension/src/extension.ts @@ -1,56 +1,16 @@ -// eslint-disable-next-line @typescript-eslint/no-var-requires -const cp = require('child_process'); - import * as vscode from 'vscode'; import { DepNodeProvider } from './deadlockPanel'; import BriefingView, { BRIEFING_ID } from './view/briefingView'; import View from './view/view'; import { OPEN_BRIEFING_COMMAND } from './command'; -import { PROJECT_SRC_PATH, PROJECT_THEIA_PATH } from './config'; - -import userConfig from './userConfig'; -import gitMission from './gitMission'; export function initViews(extensionPath: string) { new BriefingView(extensionPath); } -function copyProjectSrouces() { - cp.exec(`cp ${PROJECT_SRC_PATH}/* ${PROJECT_THEIA_PATH} -R`, (err) => { - if (err) { - console.error('copy project sources error: ' + err); - } - }); -} - -function setupProjectDir() { - cp.exec(`ls -1 ${PROJECT_THEIA_PATH} | wc -l`, (err, stdout, stderr) => { - if (!err) { - if (parseInt(stdout) === 0) { - copyProjectSrouces(); - } - } else { - console.error('set up project dir failed', stderr); - console.error(err); - } - }); -} - export async function activate(context: vscode.ExtensionContext) { - userConfig.init().then(() => { - console.log('USER CONFIG OK') - return gitMission.init(); - }).then(() => { - console.log('GITMISSION OK') - return gitMission.pull(); - }).finally(() => { - console.log('FINALLY') - setupProjectDir(); - }).catch((e) => { - console.error(e); - }); initViews(context.extensionPath); - + View.getView(BRIEFING_ID).createOrShow(); // @ts-ignore const deadlockPanelProvider = new DepNodeProvider(vscode.workspace.rootPath); @@ -65,5 +25,4 @@ export async function activate(context: vscode.ExtensionContext) { }) ); - View.getView(BRIEFING_ID).createOrShow(); } diff --git a/plugins/deadlock-extension/src/gitMission.ts b/plugins/deadlock-extension/src/gitMission.ts index 7f6efc17c6026dd3790bfd664361eccb587ce454..67f8373710bf4b346b0b040f780e465f558910d4 100644 --- a/plugins/deadlock-extension/src/gitMission.ts +++ b/plugins/deadlock-extension/src/gitMission.ts @@ -1,15 +1,18 @@ -import userConfig, { UserConfig } from "./userConfig"; import simpleGit, { SimpleGit, SimpleGitOptions } from 'simple-git'; import { PROJECT_SRC_PATH } from './config'; +import { UserConfig } from './userConfig'; -const DEFAULT_REMOTE = 'deadlock'; +const util = require('util'); +const exec = util.promisify(require('child_process').exec); + +const DEFAULT_REMOTE = 'origin'; const DEFAULT_BRANCH = 'master'; -class GitMission { +export default class GitMission { private git: SimpleGit; - constructor() { + constructor(private userConfig: UserConfig) { const options: SimpleGitOptions = { baseDir: PROJECT_SRC_PATH, binary: 'git', @@ -19,34 +22,85 @@ class GitMission { this.git = simpleGit(options); } + async setupSshAgent() { + const { err, stderr, stdout } = await exec(`eval "$(ssh-agent -s)" && ssh-keyscan -p ${this.userConfig.getGiteaSshPort()} -H ${this.userConfig.getGiteaHost()} >> ~/.ssh/known_hosts`); + + console.log(stdout); + + if (err) { + console.error(stderr); + throw new Error(err); + } + + } + async init() { try { + + console.log('Setup ssh agent'); + await this.setupSshAgent(); + console.log('Init Git mission..') - console.log('prijectSRC PATH', PROJECT_SRC_PATH); - const remote = (await this.git.remote([]) || '').replace(/(\r\n|\n|\r)/gm, ''); + const remote = await this.readRemote(); if (remote === DEFAULT_REMOTE) { - return Promise.resolve(); + return Promise.resolve(this); } - const remotePath = `ssh://git@${userConfig.getGiteaHost()}:${userConfig.getGiteaSshPort()}/${userConfig.getUsername()}/${userConfig.getMissionId()}`; + const remotePath = this.getRemotePath(); await this.git.init(); await this.git.addRemote(DEFAULT_REMOTE, remotePath); - return Promise.resolve(); + return Promise.resolve(this); } catch (e) { console.error(e); - return Promise.reject(); + return Promise.reject(e); } } + private getRemotePath() { + return `ssh://git@${this.userConfig.getGiteaHost()}:${this.userConfig.getGiteaSshPort()}/${this.userConfig.getUsername()}/${this.userConfig.getMissionId()}`; + } + + async readRemote() { + try { + return (await this.git.remote([]) || '').replace(/(\r\n|\n|\r)/gm, ''); + } catch (e) { + //ignore it, maybe the git repo does not exist + } + return ''; + } + + fetch() { + return this.git.fetch(DEFAULT_REMOTE); + } + pull() { - return this.git.pull(DEFAULT_REMOTE, DEFAULT_BRANCH, {'--rebase': 'true'}) + return this.git.pull(DEFAULT_REMOTE, DEFAULT_BRANCH, { '--rebase': 'true' }) + } + + addAll() { + return this.git.add('.'); } -} + commit(message) { + return this.git.commit(message, + undefined, + { '--author': `${this.userConfig.getUsername()} "<${this.userConfig.getEmail()}>"` }); + } -const gitMission = new GitMission(); + push() { + return this.git.push(DEFAULT_REMOTE, DEFAULT_BRANCH); + } -export default gitMission; + async isRemoteRepoExist() { + try { + const remotes = await this.git.listRemote(); + return remotes.length !== 0; + } catch { + // error, Gitea throws Gitea: Unauthorized when not found + return false; + } + } +} \ No newline at end of file diff --git a/plugins/deadlock-extension/src/recorder/README.md b/plugins/deadlock-extension/src/recorder/README.md new file mode 100644 index 0000000000000000000000000000000000000000..acb5a763e59ad8b9c7b30752f4bdf3ab95ac3c27 --- /dev/null +++ b/plugins/deadlock-extension/src/recorder/README.md @@ -0,0 +1,5 @@ +## RUN DEV + +``` +ts-node recorder.ts +``` \ No newline at end of file diff --git a/plugins/deadlock-extension/src/recorder/command-recorder.ts b/plugins/deadlock-extension/src/recorder/command-recorder.ts new file mode 100644 index 0000000000000000000000000000000000000000..82bc364ef1d6bac98eec7ffaf604d17a2a7d84fe --- /dev/null +++ b/plugins/deadlock-extension/src/recorder/command-recorder.ts @@ -0,0 +1,110 @@ +import GitMission from '../gitMission'; +import { error, commitAndPushCode } from './utils'; +const async = require("async"); + +// regex to match command line in ps aux +/** + * group 0: Full match + * group 1: USER (theia) + * group 2: PID + * group 3: START + * group 4: TIME + * group 5: Command + */ + +const child = require('child_process'); + +const regexPsCommand = /(theia).+?([\d]+).+?([\d]+\:[\d]+).+?([\d+]\:[\d+]+) (.+)/ + +class Command { + + public still: boolean; + + constructor(public pid: number, private command: string) { + this.pid = pid; + this.command = command; + + this.still = true; + } +} + +export default class CommandRecorder { + + private commandsInProgress = new Map<number, Command>(); + private readonly queue: any; + + constructor(private gitMission: GitMission) { + // create a queue object with concurrency 1 + this.queue = async.queue(async function (task) { + await task.apply(); + }, 1); + + this.queue.error((err, _task) => { + error(`Error on task: `, err); + }); + + } + + somethingExecuted(pid, command) { + if (!this.commandsInProgress.has(pid)) { + + this.commandsInProgress.set(pid, new Command(pid, command)); + try { + this.queue.push(async () => await commitAndPushCode(this.gitMission)); + } catch (e) { + console.error('Cannot send user code to git'); + console.error(e); + } + + } else { + this.commandsInProgress.get(pid)!.still = true; + } + } + + run() { + setInterval(() => { + try { + const spawn = child.spawn('ps', ['aux']); + + this.commandsInProgress.forEach(command => command.still = false); + + spawn.stdout.setEncoding('utf8'); + spawn.stdout.on('data', (data) => { + const lines = data.toString().split('\n'); + for (const line of lines) { + const lineMatcher = line.match(regexPsCommand); + if (lineMatcher && lineMatcher.length > 4) { + + const pid = lineMatcher[2]; + const command = lineMatcher[5]; + + if (line.indexOf('/opt/ibm/java/bin/java ') !== -1 && + line.indexOf('org.eclipse.jdt.ls.core') === -1 && + line.indexOf('-version') === -1 + ) { + // java program + this.somethingExecuted(pid, command); + } else if (line.indexOf('npm run start') !== -1 || + line.indexOf('yarn serve') !== -1 || + line.indexOf('yarn start') !== -1) { + // npm/yarn program + this.somethingExecuted(pid, command); + } + } + } + // clear command down (means still = false) + this.commandsInProgress.forEach(command => { + if (!command.still) { + this.commandsInProgress.delete(command.pid); + } + }) + }); + } catch (e) { + console.error(e); + } + }, 350); + } +} + + + diff --git a/plugins/deadlock-extension/src/recorder/preStop.ts b/plugins/deadlock-extension/src/recorder/preStop.ts new file mode 100644 index 0000000000000000000000000000000000000000..9c1cf471fbdda7b913b1d6ab77168ecf27d00e27 --- /dev/null +++ b/plugins/deadlock-extension/src/recorder/preStop.ts @@ -0,0 +1,48 @@ +import UserConfigNode from "./userConfigNode"; +import GitMission from "../gitMission"; +import { log, error, commitAndPushCode } from "./utils"; +import { PROJECT_SRC_PATH, PROJECT_THEIA_PATH } from "../config.prod"; +const util = require("util"); +const exec = util.promisify(require("child_process").exec); + +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}`); + // 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 user created new file or added a directory + return true; + } + } else { + // print error + error(result.stderr); + } + } + return false; +} + +(async () => { + try { + log("Container will die"); + log("Save user code.."); + + const userConfig = new UserConfigNode(); + await userConfig.init(); + const gitMission = await new GitMission(userConfig).init(); + if (await containsDiff()) { + await commitAndPushCode(gitMission); + } + } catch (e) { + error("Cannot push user code at the end.."); + error(e); + } +})(); diff --git a/plugins/deadlock-extension/src/recorder/recorder.ts b/plugins/deadlock-extension/src/recorder/recorder.ts new file mode 100644 index 0000000000000000000000000000000000000000..61a2d26e364e19cff116c92e5e9f083cdf70558e --- /dev/null +++ b/plugins/deadlock-extension/src/recorder/recorder.ts @@ -0,0 +1,42 @@ +import CommandRecorder from "./command-recorder" +import GitMission from "../gitMission"; +import UserConfigNode from "./userConfigNode"; +import { PROJECT_SRC_PATH, PROJECT_THEIA_PATH } from '../config' +import { pathContainsFiles, copyProjectSources, clearFilesExceptGit, log } from "./utils"; + +async function setupProject() { + await pathContainsFiles(PROJECT_THEIA_PATH); + await copyProjectSources(PROJECT_SRC_PATH, PROJECT_THEIA_PATH); +} + +export default class Recorder { + + async run() { + const userConfig = new UserConfigNode(); + + try { + await userConfig.init(); + const gitMission = await new GitMission(userConfig).init(); + const isRemoteRepoExist = await gitMission.isRemoteRepoExist(); + if (isRemoteRepoExist) { + // rm all except git directory pull remote code and setup + log('Cleaning files to pull repo'); + + await clearFilesExceptGit(PROJECT_SRC_PATH); + + log('Pulling user repo'); + await gitMission.pull(); + } + log('Setup user project..'); + await setupProject(); + + log('Starting CommandRecorder..'); + new CommandRecorder(gitMission).run(); + } catch (e) { + console.error('Cannot setup user repo.'); + console.error(e); + } + } +} + +new Recorder().run(); \ No newline at end of file diff --git a/plugins/deadlock-extension/src/recorder/userConfigNode.ts b/plugins/deadlock-extension/src/recorder/userConfigNode.ts new file mode 100644 index 0000000000000000000000000000000000000000..0a643228fc652ecf348432e3d02f67429c083b55 --- /dev/null +++ b/plugins/deadlock-extension/src/recorder/userConfigNode.ts @@ -0,0 +1,14 @@ +import { UserConfig } from "../userConfig"; +import { USER_CHALLENGE_PATH } from '../config' + +const fs = require('fs'); + + +export default class UserConfigNode extends UserConfig { + async loadText(): Promise<string> { + const text = fs.readFileSync(USER_CHALLENGE_PATH, 'utf8'); + + return Promise.resolve(text); + } + +} \ No newline at end of file diff --git a/plugins/deadlock-extension/src/recorder/utils.ts b/plugins/deadlock-extension/src/recorder/utils.ts new file mode 100644 index 0000000000000000000000000000000000000000..c3ff762581107435feea3f10d349fb7ee28caaf3 --- /dev/null +++ b/plugins/deadlock-extension/src/recorder/utils.ts @@ -0,0 +1,66 @@ +import GitMission from "../gitMission"; +import { PROJECT_SRC_PATH, PROJECT_THEIA_PATH } from "../config.prod"; +import { execSync } from "child_process"; +import { v4 as uuid } from 'uuid'; + +const util = require('util'); +const unlink = util.promisify(require('fs').unlink); +const exec = util.promisify(require('child_process').exec); +const fs = require('fs'); +const readdirSync = require('fs').readdirSync; +const Path = require('path'); + +const PREFIX = '[DEADLOCK-RECORDER]' + +export const log = (message, args?) => { + if (args) { + console.log(`${PREFIX} ${message}`, args); + } else { + console.log(`${PREFIX} ${message}`); + } +} +export const error = (message, args?) => { + if (args) { + console.error(`${PREFIX} ${message}`, args); + } else { + console.error(`${PREFIX} ${message}`); + } +} + +export async function copyProjectSources(srcPath: string, theiaPath: string) { + return exec(`rsync -a ${srcPath}/* ${theiaPath} --exclude .git/ && chown -R theia:theia /home/project`); +} + +export async function pathContainsFiles(path: string) { + return exec(`ls -1 ${path} | wc -l`); +} + +export async function clearFilesExceptGit(path) { + readdirSync(path).forEach(async (file) => { + if (file !== '.git') { + const curPath = Path.join(path, file); + if (fs.lstatSync(curPath).isDirectory()) { + fs.rmdirSync(curPath, { recursive: true }); + } else { + await unlink(curPath); + } + } + }); +} + +export async function commitAndPushCode(gitMission: GitMission) { + try { + log(`Commit & push`); + await clearFilesExceptGit(PROJECT_SRC_PATH); + + execSync(`rsync -r --exclude .git --exclude npm --exclude target ${PROJECT_THEIA_PATH}/* ${PROJECT_SRC_PATH} && cp ${Path.join(__dirname, '.gitignore')} ${PROJECT_SRC_PATH} && chown -R root:root /project`); + + await gitMission.addAll(); + await gitMission.commit(uuid()); + await gitMission.push(); + log(`Commit & push done.`) + } catch (e) { + error(`[${e.status}] cannot commitAndPush code: ${e.stderr}`); + error(e.message); + } +} diff --git a/plugins/deadlock-extension/src/userConfig.ts b/plugins/deadlock-extension/src/userConfig.ts index ac0135907bf7300b768790815a86578d22ea1b20..587330d647035ed05c992875567f718b5f31da3f 100644 --- a/plugins/deadlock-extension/src/userConfig.ts +++ b/plugins/deadlock-extension/src/userConfig.ts @@ -1,6 +1,3 @@ -import * as vscode from 'vscode'; - -import { USER_CHALLENGE_PATH } from './config'; /** * Example: @@ -19,12 +16,9 @@ import { USER_CHALLENGE_PATH } from './config'; * } */ -export class UserConfig { - private static instance: UserConfig; +export abstract class UserConfig { private userConfigJson: any | undefined; - private constructor() { } - getPaths(): Map<number, string> { return this.userConfigJson?.paths; } @@ -53,27 +47,22 @@ export class UserConfig { return this.userConfigJson?.missionId; } - public static getInstance(): UserConfig { - if (!UserConfig.instance) { - UserConfig.instance = new UserConfig(); - } - - return UserConfig.instance; + getEmail(): string { + return this.userConfigJson?.email; } + abstract async loadText(): Promise<string>; + public async init() { try { - const userConfig = await vscode.workspace.openTextDocument(vscode.Uri.parse(USER_CHALLENGE_PATH)); + const userConfig = await this.loadText(); - this.userConfigJson = JSON.parse(userConfig.getText()); + this.userConfigJson = JSON.parse(userConfig); return Promise.resolve(); } catch (e) { + console.error('Cannot load userConfig'); console.error(e); return Promise.reject(); } } -} - -const userConfig = UserConfig.getInstance(); - -export default userConfig; +} \ No newline at end of file diff --git a/plugins/deadlock-extension/src/userConfigTheia.ts b/plugins/deadlock-extension/src/userConfigTheia.ts new file mode 100644 index 0000000000000000000000000000000000000000..409fddea8352a0f3be6db696100197b2f066a643 --- /dev/null +++ b/plugins/deadlock-extension/src/userConfigTheia.ts @@ -0,0 +1,12 @@ +import { UserConfig } from "./userConfig"; +import * as vscode from 'vscode'; + +import { USER_CHALLENGE_PATH } from './config' + +export default class UserConfigTheia extends UserConfig { + async loadText(): Promise<string> { + const textDocument = await vscode.workspace.openTextDocument(vscode.Uri.parse(USER_CHALLENGE_PATH)); + return Promise.resolve(textDocument.getText()); + } + +} \ No newline at end of file diff --git a/plugins/deadlock-extension/src/view/briefingView.ts b/plugins/deadlock-extension/src/view/briefingView.ts index 2a839ad2b12fe01cf9cb641b7dabf9f8ef96b8a4..a4d5fb5ca24b9eabea9e62e11344292965326287 100644 --- a/plugins/deadlock-extension/src/view/briefingView.ts +++ b/plugins/deadlock-extension/src/view/briefingView.ts @@ -1,75 +1,82 @@ -import View from './view'; +import View from "./view"; -import * as vscode from 'vscode'; +import * as vscode from "vscode"; -import * as path from 'path'; +import * as path from "path"; // eslint-disable-next-line @typescript-eslint/no-var-requires -const marked = require('marked'); +const marked = require("marked"); -import { USER_CHALLENGE_PATH } from '../config'; +import { USER_CHALLENGE_PATH, DOCS_PATH, BRIEFING_FILE_NAME } from "../config"; -export const BRIEFING_ID = 'brefingView'; +export const BRIEFING_ID = "brefingView"; export default class BriefingView extends View { private briefingContent: string | undefined; private userChallengeConfig: any | undefined; + + private errorLoadingBriefing: boolean = false; + private errorLoadingUserConfig: boolean = false; constructor(extensionPath: string) { - super(BRIEFING_ID, extensionPath, 'Briefing', 'Briefing'); + super(BRIEFING_ID, extensionPath, "Briefing", "Briefing"); } load() { - const currentWorkspace = vscode.workspace.workspaceFolders; - if (currentWorkspace) { - // means at least one folder is open within vscode - + if (vscode.workspace) { vscode.workspace .openTextDocument( - vscode.Uri.parse(path.join(currentWorkspace[0].uri.path, 'README.md')) - ) - .then((document) => { + vscode.Uri.parse(path.join(DOCS_PATH, BRIEFING_FILE_NAME)) + ).then((document) => { this.briefingContent = document.getText(); this.update(); - }) - } + }, (error) => { + console.error('Cannot load briefing', error); + this.errorLoadingBriefing = true; + this.update(); + }); - vscode.workspace - .openTextDocument(vscode.Uri.parse(USER_CHALLENGE_PATH)) - .then((userConfig) => { - this.userChallengeConfig = JSON.parse(userConfig.getText()); - this.update(); - }); + vscode.workspace + .openTextDocument(vscode.Uri.parse(USER_CHALLENGE_PATH)) + .then((userConfig) => { + this.userChallengeConfig = JSON.parse(userConfig.getText()); + this.update(); + }, (error) => { + console.error('Cannot load user_challenge config', error); + this.errorLoadingUserConfig = true; + this.update(); + }); + } } render() { - let output = ''; + let output = ""; if (this.briefingContent) { output += `<h1>Mission Goal 🕶</h1>${marked(this.briefingContent)}`; - } else if (this.loaded) { - output += 'Cannot load Briefing.'; + } else if (this.errorLoadingBriefing) { + output += "Cannot load briefing."; } else { - output += 'Loading briefing..'; + output += "Loading briefing.."; } - output += '<br/>'; + output += "<br/>"; if (this.userChallengeConfig) { output += this.renderUserChallengeConfig(); - } else if (this.loaded) { - output += 'Cannot load help.'; + } else if (this.errorLoadingUserConfig) { + output += "Cannot load user config." } else { - output += 'Loading help..'; + output += "Loading help.."; } return output; } private renderUserChallengeConfig() { - let adresses = ''; + let adresses = ""; let pathsLength = 0; for (const key in this.userChallengeConfig?.paths) { - if (key !== '3000') { + if (key !== "3000") { pathsLength++; const path = this.userChallengeConfig?.paths[key]; adresses += `<li>${key} binded on <a href="https://${this.userChallengeConfig?.host}/${path}/">${path}</a></li>`; @@ -79,7 +86,7 @@ export default class BriefingView extends View { if (pathsLength > 0) { return `<h3>Configuration</h3>You have the following adresses availables for your challenge : <ul>${adresses}</ul>`; } - - return ''; + + return ""; } -} \ No newline at end of file +} diff --git a/plugins/deadlock-extension/src/view/view.ts b/plugins/deadlock-extension/src/view/view.ts index 173b06727f21fa45f91ff83fb4ef0b235ccc3ab6..32e34c4add87fef7ff2f639536da133ba31bf593 100644 --- a/plugins/deadlock-extension/src/view/view.ts +++ b/plugins/deadlock-extension/src/view/view.ts @@ -17,8 +17,6 @@ export default abstract class View { public currentWebviewPanel: vscode.WebviewPanel | undefined; private _disposables: vscode.Disposable[] = []; - protected loaded: boolean; - protected initiated: boolean; private isRegisteredOnWebviewPanelSerializer: boolean; constructor( @@ -27,8 +25,6 @@ export default abstract class View { public readonly panelName: string, public readonly title: string ) { - this.loaded = false; - this.initiated = false; this.isRegisteredOnWebviewPanelSerializer = false; if (View.views.has(id)) { @@ -36,6 +32,7 @@ export default abstract class View { return; } View.views.set(id, this); + this.load(); } static getView(id: string): View { @@ -46,6 +43,7 @@ export default abstract class View { } private init(currentWebviewPanel) { + // Listen for when the panel is disposed // This happens when the user closes the panel or when the panel is closed programatically currentWebviewPanel.onDidDispose( @@ -90,11 +88,6 @@ export default abstract class View { }); this.isRegisteredOnWebviewPanelSerializer = true; } - - this.load(); - this.loaded = false; - this.update(); - this.initiated = true; } public createOrShow() { @@ -118,12 +111,16 @@ export default abstract class View { enableScripts: true, // And restrict the webview to only loading content from our extension's `media` directory. - localResourceRoots: [ - vscode.Uri.file(path.join(this.extensionPath, 'media')), - ], + // for now we don't need to include the media folder, because React bundle is not used + // localResourceRoots: [ + // vscode.Uri.file(path.join(this.extensionPath, 'media')), + // ], } ); + // update first time to force render of webviewPanel + this.update(); this.init(this.currentWebviewPanel); + this.update(); return this.currentWebviewPanel; } @@ -150,36 +147,37 @@ export default abstract class View { if (this.currentWebviewPanel) { const webview = this.currentWebviewPanel.webview; this.currentWebviewPanel.title = this.title; - this.currentWebviewPanel.webview.html = this._getHtmlForWebview(webview); + webview.html = this._getHtmlForWebview(webview); } else { console.warn(`Cannot update ${this.panelName} has not been created.`); } } private _getHtmlForWebview(webview: vscode.Webview) { + // /!\ EXPERIMENTAL /!\ // Local path to main script run in the webview - const scriptPathOnDisk = vscode.Uri.file( - path.join(this.extensionPath, 'media', 'static/bundle.js') - ); - const cssPathOnDisk = vscode.Uri.file( - path.join(this.extensionPath, 'media', 'static/bundle.css') - ); + // const scriptPathOnDisk = vscode.Uri.file( + // path.join(this.extensionPath, 'media', 'static/bundle.js') + // ); + // const cssPathOnDisk = vscode.Uri.file( + // path.join(this.extensionPath, 'media', 'static/bundle.css') + // ); // And the uri we use to load this script in the webview - const scriptUri = webview.asWebviewUri(scriptPathOnDisk); - const cssUri = webview.asWebviewUri(cssPathOnDisk); + // const scriptUri = webview.asWebviewUri(scriptPathOnDisk); + // const cssUri = webview.asWebviewUri(cssPathOnDisk); + // append in the HTML head <link href="${cssUri}" rel="stylesheet"></head> // Use a nonce to whitelist which scripts can be run - const nonce = getNonce(); + // const nonce = getNonce(); - //<script nonce="${nonce}" src="${scriptUri}"></script> + // append in the HTML head<script nonce="${nonce}" src="${scriptUri}"></script> return `<!DOCTYPE html><html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Deadlock</title> - <link href="${cssUri}" rel="stylesheet"></head> </head> <body> <div> diff --git a/recorder/command-recorder.js b/recorder/command-recorder.js deleted file mode 100644 index 8d085c0acba3036cbc6b867b6cdf6c53c0d4059c..0000000000000000000000000000000000000000 --- a/recorder/command-recorder.js +++ /dev/null @@ -1,103 +0,0 @@ -const PREFIX = '[DEADLOCK-RECORDER]' - -const log = (message, args) => { - if (args) { - console.log(`${PREFIX} ${message}`, args); - } else { - console.log(`${PREFIX} ${message}`); - } -} -const error = (message, args) => { - if (args) { - console.error(`${PREFIX} ${message}`, args); - } else { - console.error(`${PREFIX} ${message}`); - } -} - -log('Starting..') - -// regex to match command line in ps aux -/** - * group 0: Full match - * group 1: USER (theia) - * group 2: PID - * group 3: START - * group 4: TIME - * group 5: Command - */ - -const child = require('child_process'); - -const regexPsCommand = /(theia).+?([\d]+).+?([\d]+\:[\d]+).+?([\d+]\:[\d+]+) (.+)/ - -class Command { - constructor(pid, command) { - this.pid = pid; - this.command = command; - - this.still = true; - } -} - -const commandsInProgress = new Map(); - -function logCommit(commitChild) { - commitChild.stdout.on('data', (data) => { - log('> commit:', data.toString()); - }); - commitChild.stderr.on('data', (data) => { - error('> commit:', data.toString()); - }); -}; - -setInterval(() => { - try { - const spawn = child.spawn('ps', ['aux']); - - commandsInProgress.forEach(command => command.still = false); - - spawn.stdout.setEncoding('utf8'); - spawn.stdout.on('data', function (data) { - const lines = data.toString().split('\n'); - for (line of lines) { - const lineMatcher = line.match(regexPsCommand); - if (lineMatcher && lineMatcher.length > 4) { - - const pid = lineMatcher[2]; - const command = lineMatcher[5]; - - if (line.indexOf('/opt/ibm/java/bin/java ') !== -1 && - line.indexOf('org.eclipse.jdt.ls.core') === -1 && - line.indexOf('-version') === -1 - ) { - if (!commandsInProgress.has(pid)) { - commandsInProgress.set(pid, new Command(pid, command)); - - // commit user code - log('commit code'); - logCommit(child.spawn('node', ['commit.js'])); - } else { - commandsInProgress.get(pid).still = true; - } - } else if (line.indexOf('npm run start') !== -1 || - line.indexOf('yarn serve') !== -1 || - line.indexOf('yarn start') !== -1) { - - log('npm program executed'); - console.log(lineMatcher[5]); - } - } - } - // clear command down (means still = false) - commandsInProgress.forEach(command => { - if (!command.still) { - commandsInProgress.delete(command.pid); - } - }) - }); - } catch (e) { - console.error(e); - } -}, 700); - diff --git a/recorder/commit.js b/recorder/commit.js deleted file mode 100644 index 5198ac3b4b49cc8c1133b0f13af814703b79f375..0000000000000000000000000000000000000000 --- a/recorder/commit.js +++ /dev/null @@ -1,30 +0,0 @@ - - -const fs = require('fs'); -const os = require('os'); -const path = require('path'); -const { exec } = require('child_process'); -const userCodePath = '/home/project/*'; - -// create tmp folder to commit and do not block current user -fs.mkdtemp(path.join(os.tmpdir(), 'commit-'), (err, tmpFolder) => { - console.log('tmpFolder', tmpFolder); - - console.log(`cp ${userCodePath} ${tmpFolder} && cp .gitignore ${tmpFolder}`); - exec(`cp -r ${userCodePath} ${tmpFolder} && cp .gitignore ${tmpFolder}`, (error, stdout, stderr) => { - console.log('exec done'); - if (error) { - console.log(`error: ${error.message}`); - return; - } - if (stderr) { - console.log(`stderr: ${stderr}`); - return; - } - console.log('commit CP done', stdout); - console.log('commit done'); - }); -}); - - - diff --git a/recorder/test.js b/recorder/test.js deleted file mode 100644 index c71eb61c4d0b60decbb504c1158580e8b91b4b28..0000000000000000000000000000000000000000 --- a/recorder/test.js +++ /dev/null @@ -1,47 +0,0 @@ -const child = require('child_process'); -// const watch = child.spawn('watch', ['-n', '0.5', 'ps aux']); - -const regexPsCommand = /(alex).+?([\d]+).+?([\d]+\:[\d]+).+?([\d+]\:[\d+]+) (.+)/ - -setInterval(() => { - const spawn = child.spawn('ps', ['aux']); - - spawn.stdout.setEncoding('utf8'); - spawn.stdout.on('data', function(data) { - const lines = data.toString().split('\n'); - - child.spawn('ls'); - for (line of lines) { - const lineMatcher = line.match(regexPsCommand); - if (lineMatcher && lineMatcher.length > 4) { - console.log(lineMatcher[5]); - } - } - }); -}, 500); - - -// var execute = function(callback) { -// child.exec('ls', {maxBuffer: 1024 * 500}, function(error, stdout, stderr){ -// console.log('error', error); -// console.log('stdout', stdout); -// console.log('stderr', stderr); -// }); -// }; - - -// const regexTopCommand = /([\d]+) (theia).+?([\d]+\.[\d]+) +([\d]+\.[\d]+) +([\d]+\:[\d]+\.[\d]+) (.+)/ -// watch.stdout.addListener('data', (data) => { -// try { -// console.log('================='); -// console.log(data.toString()); -// } catch (e) { -// //catch anything to avoid the program exit -// console.error(e); -// } -// }); - -// watch.on('exit', function (code) { -// console.error(`exit command recorder ${code}`) -// process.exit(code); -// }); diff --git a/server.js b/server.js index c0a60e69953d0b02e6649c354e312d34486ef1ba..39e7d1718b44a61d65b4b16a87afc31b872d11e6 100644 --- a/server.js +++ b/server.js @@ -2,7 +2,7 @@ * This file allow CORS for *.deadlock.io * from the CORS middleware * To generate the file you have to run Theia Docker-Java version from https://hub.docker.com/r/theiaide/theia-java - * then you will find this file under https://hub.docker.com/r/theiaide/theia-java in the container + * then you will find the content within /home/theia/src-gen/backend/server.js in the container */ diff --git a/start.sh b/start.sh index 212b598937dbed887d76ddc00fcbf4a72eea126b..bc195975bcc51aa07e3eab0cb0127fa8de0f5228 100755 --- a/start.sh +++ b/start.sh @@ -3,16 +3,13 @@ # starting service as ROOT service rsyslog restart -# setup ssh key for theia user +# setup ssh key for root user # must be installed by the API first within /tmp/.ssh -mkdir /home/theia/.ssh -cp /tmp/.ssh/* /home/theia/.ssh/ -chown -R theia:theia /home/theia/.ssh -chmod 700 ~/.ssh -chmod 744 ~/.ssh/id_rsa.pub +mkdir ~/.ssh +cp /tmp/.ssh/* ~/.ssh/ # start command recorder -node command-recorder.js & +node deadlock/recorder/recorder.js & # starting theia as THEIA su theia --command "yarn theia start /home/project --hostname=0.0.0.0 --plugins=local-dir:/home/plugins" \ No newline at end of file