diff --git a/packages/container/print.js b/packages/container/print.js index e2b6f95..7a98bc4 100644 --- a/packages/container/print.js +++ b/packages/container/print.js @@ -65,7 +65,6 @@ function printLine(content, delay, color) { } async function printResponses(responses, delay = 100) { - await waitTime(1000) console.log() console.log(colorize('-------------', 'white')) for (const response of responses) { diff --git a/packages/models/quest.js b/packages/models/quest.js index 8ef7879..39a35ab 100644 --- a/packages/models/quest.js +++ b/packages/models/quest.js @@ -11,9 +11,15 @@ const responseSchema = new Schema({ }) const fileConditionSchema = new Schema({ - path: String, + path: { + type: String, + required: true, + }, type: String, - exists: Boolean, + exists: { + type: Boolean, + default: true, + }, }) const conditionSchema = new Schema({ diff --git a/quests/helloworld/game.yml b/quests/helloworld/game.yml index ad47ff6..0b4c575 100644 --- a/quests/helloworld/game.yml +++ b/quests/helloworld/game.yml @@ -136,22 +136,16 @@ stages: content: - You are using `less` or the output contains `wow`. - - id: nested-condition - task: Test nested condition + - id: test-file-exists + task: Test file exists repeatable: true condition: - $or: - - pwd: - - '^/etc/zsh$' - $not: - command: - - ls - - $not: - pwd: - - '^/etc/zsh$' - command: - - ls + command: + - ls + files: + - path: /home/commander/jail + type: folder responses: - type: narrative content: - - You are in `/etc/zsh` and not using `ls`, or you are not in `/etc/zsh` and using `ls`. + - There is a jail diff --git a/server/src/containers/docker.js b/server/src/containers/docker.js index be652f5..12b81cd 100644 --- a/server/src/containers/docker.js +++ b/server/src/containers/docker.js @@ -1,4 +1,6 @@ +import { PassThrough } from 'stream' import Docker from 'dockerode' +import { buildFileCheckCmd } from '@linux-odyssey/utils' import config from '../config.js' const engine = new Docker() @@ -77,3 +79,47 @@ export async function deleteContainer(id) { } await container.remove() } + +// Utility function to convert a stream to a string +function streamToString(stream) { + const chunks = [] + return new Promise((resolve, reject) => { + stream.on('data', (chunk) => chunks.push(chunk)) + stream.on('error', reject) + stream.on('end', () => resolve(Buffer.concat(chunks).toString('utf8'))) + setTimeout(() => resolve(Buffer.concat(chunks).toString('utf8')), 100) + }) +} + +export async function checkFiles(id, files) { + const container = engine.getContainer(id) + if (!container) { + throw new Error('Container not found') + } + + const cmd = buildFileCheckCmd(files) + console.log(cmd) + const exec = await container.exec({ + AttachStdout: true, + AttachStderr: true, + Cmd: ['sh', '-c', cmd], + }) + const execStream = await exec.start() + const outStream = new PassThrough() + const errStream = new PassThrough() + + console.log('exec started') + + exec.modem.demuxStream(execStream, outStream, errStream) + const [output, errorOutput] = await Promise.all([ + streamToString(outStream), + streamToString(errStream), + ]) + + console.log('output', output, 'errorOutput', errorOutput) + + const execOutput = await exec.inspect() + console.log('exit code', execOutput.ExitCode) + console.log('exit code === 0', execOutput.ExitCode === 0) + return execOutput.ExitCode === 0 +} diff --git a/server/src/game/commandHandler.js b/server/src/game/commandHandler.js index da00864..5fe13c5 100644 --- a/server/src/game/commandHandler.js +++ b/server/src/game/commandHandler.js @@ -3,6 +3,7 @@ import { FileGraph } from '@linux-odyssey/file-graph' import { Quest } from '@linux-odyssey/models' import { pushToSession } from '../api/socket.js' import SessionHandler from './sessionHandler.js' +import { checkFiles } from '../containers/docker.js' // 檢查 pattern 是否符合 input const checkMatch = (pattern, input) => { @@ -53,15 +54,31 @@ export default class CommandHandler extends SessionHandler { this.session.graph = graph } - isMatch(condition) { + async isMatch(condition) { const keys = ['command', 'output', 'error', 'pwd'] - console.log('condition', condition) - return ( - keys.every((k) => checkMatch(condition[k], this.commandInput[k])) && - (condition.$or.length === 0 || - condition.$or.some((c) => this.isMatch(c))) && - (!condition.$not || !this.isMatch(condition.$not)) - ) + if (keys.some((k) => !checkMatch(condition[k], this.commandInput[k]))) { + return false + } + if (condition.files.length > 0) { + const match = await this.checkFiles(condition.files) + if (!match) return false + } + if (condition.$not) { + const match = await this.isMatch(condition.$not) + if (match) return false + } + if (condition.$or.length > 0) { + const matches = await Promise.all( + condition.$or.map((c) => this.isMatch(c)) + ) + if (matches.every((m) => m === false)) return false + } + return true + } + + checkFiles(files) { + console.log('checkFiles', files) + return checkFiles(this.session.containerId, files) } async run() { @@ -75,9 +92,18 @@ export default class CommandHandler extends SessionHandler { this.handleEvent() this.handleCommand() - const response = stages - .filter((s) => this.isMatch(s.condition)) - .reduce((r, s) => ({ ...r, ...this.execute(s) }), {}) + const matches = await Promise.all( + stages.map((stage) => this.isMatch(stage.condition)) + ) + + console.log('matches', matches) + + const filteredStages = stages.filter((_, i) => matches[i]) + console.log('filteredStages', filteredStages) + const response = filteredStages.reduce( + (r, s) => ({ ...r, ...this.execute(s) }), + {} + ) return { end: this.session.status === 'finished',