feat: Add support for stop button in code runner

This commit is contained in:
purocean
2024-06-18 22:02:51 +08:00
parent 3430bfe388
commit cd211d8122
8 changed files with 88 additions and 17 deletions
+27 -6
View File
@@ -17,17 +17,38 @@ function execFile (file: string, args: string[], options?: cp.ExecFileOptions) {
})
process.on('spawn', () => {
const output = [process.stdout, process.stderr].filter(Boolean) as NodeJS.ReadableStream[]
resolve(mergeStreams(output))
const output = mergeStreams([process.stdout, process.stderr].filter(Boolean) as NodeJS.ReadableStream[])
output.on('close', () => {
console.log('execFile process closed')
process.kill()
})
resolve(output)
})
})
}
function execCmd (cmd: string, options?: cp.ExecOptions, onComplete?: () => void) {
const process = cp.exec(cmd, { timeout: 300 * 1000, ...options })
onComplete && process.on('close', onComplete)
const output = [process.stdout, process.stderr].filter(Boolean) as NodeJS.ReadableStream[]
return mergeStreams(output)
return new Promise<string | NodeJS.ReadableStream>((resolve) => {
const process = cp.exec(cmd, { timeout: 300 * 1000, ...options })
process.on('error', error => {
resolve(error.message)
})
process.on('spawn', () => {
const output = mergeStreams([process.stdout, process.stderr].filter(Boolean) as NodeJS.ReadableStream[])
output.on('close', () => {
console.log('execCmd process closed')
onComplete && onComplete()
process.kill()
})
resolve(output)
})
})
}
const runCode = async (cmd: { cmd: string, args: string[] } | string, code: string): Promise<string | NodeJS.ReadableStream> => {
+1
View File
@@ -111,6 +111,7 @@ export default {
ctx.runner.registerRunner({
name: 'javascript',
order: 100,
nonInterruptible: true,
match (language) {
return ['js', 'javascript'].includes(language.toLowerCase())
},
+46 -5
View File
@@ -1,4 +1,4 @@
import { computed, defineComponent, getCurrentInstance, h, onBeforeUnmount, ref, VNode, watch, watchEffect } from 'vue'
import { computed, defineComponent, getCurrentInstance, h, onBeforeUnmount, ref, shallowRef, VNode, watch, watchEffect } from 'vue'
import Markdown from 'markdown-it'
import { escape } from 'lodash-es'
import { Plugin } from '@fe/context'
@@ -30,8 +30,10 @@ const RunCode = defineComponent({
const { t } = useI18n()
const instance = getCurrentInstance()
const result = ref('')
const abortController = shallowRef<AbortController>()
const hash = computed(() => md5(props.language + props.code))
const runner = ref<CodeRunner>()
const status = computed(() => runner.value?.nonInterruptible ? 'idle' : (abortController.value ? 'running' : 'idle'))
const getTerminalCmd = computed(() => runner.value?.getTerminalCmd(props.language!, props.firstLine!))
let hasResult = false
@@ -51,7 +53,22 @@ const RunCode = defineComponent({
cache[hash.value] = result.value
}
const abort = () => {
if (abortController.value && !abortController.value.signal.aborted) {
abortController.value.abort()
abortController.value = undefined
if (!hasResult) {
result.value = ''
}
}
}
const run = async () => {
if (status.value === 'running') {
abort()
return
}
const { code, language } = props
hasResult = false
@@ -64,7 +81,11 @@ const RunCode = defineComponent({
result.value = t('code-run.running')
try {
const { type, value: val } = await runner.value.run(language!, code)
if (!runner.value.nonInterruptible) {
abortController.value = new AbortController()
}
const { type, value: val } = await runner.value.run(language!, code, { signal: abortController.value?.signal })
if (typeof val === 'string') {
appendLog?.(type, val)
@@ -73,6 +94,10 @@ const RunCode = defineComponent({
// read stream
while (true) {
if (abortController.value && abortController.value.signal.aborted) {
break
}
const { done, value } = await val.read()
if (done) {
logger.debug('run code done >', value)
@@ -99,7 +124,11 @@ const RunCode = defineComponent({
appendLog(type, valStr)
}
} catch (error: any) {
result.value = error.message
if (error.name !== 'AbortError') {
appendLog?.('plain', error.message)
}
} finally {
abort()
}
}
@@ -146,8 +175,8 @@ const RunCode = defineComponent({
return [
h('div', { class: 'p-mcr-run-code-action skip-export' }, [
h('div', {
title: t('code-run.run'),
class: 'p-mcr-run-btn',
title: status.value !== 'running' ? t('code-run.run') : t('code-run.stop'),
class: 'p-mcr-run-btn' + (status.value === 'running' ? ' p-mcr-run-btn-stop' : ''),
onClick: run
}),
h('div', {
@@ -220,6 +249,18 @@ export default {
outline: none;
}
.markdown-view .markdown-body .p-mcr-run-btn.p-mcr-run-btn-stop {
border: .56em #b7b3b3 solid;
border-radius: 4px;
animation: p-mcr-run-btn-stop 1.5s infinite;
}
@keyframes p-mcr-run-btn-stop {
0% { border-color: #b7b3b3; }
50% { border-color: #b7c3f3; }
100% { border-color: #b7b3b3; }
}
.markdown-view .markdown-body .p-mcr-run-xterm-btn {
position: absolute;
top: -.5em;
+9 -5
View File
@@ -481,18 +481,22 @@ export async function convertFile (
* Run code.
* @param cmd
* @param code
* @param outputStream
* @param opts
* @returns result
*/
export async function runCode (cmd: string | { cmd: string, args: string[] }, code: string, outputStream: true): Promise<ReadableStreamDefaultReader>
export async function runCode (cmd: string | { cmd: string, args: string[] }, code: string, outputStream?: false): Promise<string>
export async function runCode (cmd: string | { cmd: string, args: string[] }, code: string, outputStream = false): Promise<ReadableStreamDefaultReader | string> {
export async function runCode (cmd: string | { cmd: string, args: string[] }, code: string, opts?: { stream?: false, signal?: AbortSignal }): Promise<ReadableStreamDefaultReader>
export async function runCode (cmd: string | { cmd: string, args: string[] }, code: string, opts?: { stream?: false, signal?: AbortSignal }): Promise<string>
export async function runCode (cmd: string | { cmd: string, args: string[] }, code: string, opts?: { stream?: false, signal?: AbortSignal }): Promise<ReadableStreamDefaultReader | string> {
const response = await fetchHttp('/api/run', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ cmd, code })
body: JSON.stringify({ cmd, code }),
signal: opts?.signal
})
// compatible with old version
const outputStream = typeof opts === 'boolean' ? opts : opts?.stream
if (outputStream) {
return response.body.getReader()
} else {
+2 -1
View File
@@ -516,12 +516,13 @@ export type Renderer = {
export interface CodeRunner {
name: string;
order?: number;
nonInterruptible?: boolean
match: (language: string, magicComment: string) => boolean;
getTerminalCmd: (language: string, magicComment: string) => {
start: string,
exit: string,
} | null;
run: (language: string, code: string) => Promise<{
run: (language: string, code: string, opts?: { signal?: AbortSignal }) => Promise<{
type: 'html' | 'plain',
value: ReadableStreamDefaultReader | string,
}>;
+1
View File
@@ -493,6 +493,7 @@ const data = {
},
'code-run': {
'run': 'Run',
'stop': 'Stop',
'run-in-xterm-tips': 'Run code in terminal, %s + click do not exit',
'run-in-xterm': 'Run in terminal',
'running': 'Running...',
+1
View File
@@ -494,6 +494,7 @@ const data: BaseLanguage = {
},
'code-run': {
'run': '运行',
'stop': '停止',
'run-in-xterm-tips': '在终端中运行代码,%s + 单击不退出解释器',
'run-in-xterm': '终端中运行',
'running': '运行中……',
+1
View File
@@ -494,6 +494,7 @@ const data: BaseLanguage = {
},
'code-run': {
'run': '執行',
'stop': '停止',
'run-in-xterm-tips': '在終端中執行程式碼,%s + 點擊不退出解釋器',
'run-in-xterm': '終端中執行',
'running': '執行中……',