diff --git a/src/main/server/run.ts b/src/main/server/run.ts index dde1f0fb..34c3f0b4 100644 --- a/src/main/server/run.ts +++ b/src/main/server/run.ts @@ -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((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 => { diff --git a/src/renderer/plugins/code-runners.tsx b/src/renderer/plugins/code-runners.tsx index 763632b5..e8e26bb1 100644 --- a/src/renderer/plugins/code-runners.tsx +++ b/src/renderer/plugins/code-runners.tsx @@ -111,6 +111,7 @@ export default { ctx.runner.registerRunner({ name: 'javascript', order: 100, + nonInterruptible: true, match (language) { return ['js', 'javascript'].includes(language.toLowerCase()) }, diff --git a/src/renderer/plugins/markdown-code-run.ts b/src/renderer/plugins/markdown-code-run.ts index 71e5da5a..f08f0ebb 100644 --- a/src/renderer/plugins/markdown-code-run.ts +++ b/src/renderer/plugins/markdown-code-run.ts @@ -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() const hash = computed(() => md5(props.language + props.code)) const runner = ref() + 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; diff --git a/src/renderer/support/api.ts b/src/renderer/support/api.ts index 7939aef8..c43f5326 100644 --- a/src/renderer/support/api.ts +++ b/src/renderer/support/api.ts @@ -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 -export async function runCode (cmd: string | { cmd: string, args: string[] }, code: string, outputStream?: false): Promise -export async function runCode (cmd: string | { cmd: string, args: string[] }, code: string, outputStream = false): Promise { +export async function runCode (cmd: string | { cmd: string, args: string[] }, code: string, opts?: { stream?: false, signal?: AbortSignal }): Promise +export async function runCode (cmd: string | { cmd: string, args: string[] }, code: string, opts?: { stream?: false, signal?: AbortSignal }): Promise +export async function runCode (cmd: string | { cmd: string, args: string[] }, code: string, opts?: { stream?: false, signal?: AbortSignal }): Promise { 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 { diff --git a/src/renderer/types.ts b/src/renderer/types.ts index aa47dfa6..ba1694a4 100644 --- a/src/renderer/types.ts +++ b/src/renderer/types.ts @@ -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, }>; diff --git a/src/share/i18n/languages/en.ts b/src/share/i18n/languages/en.ts index fe2bf5e6..6fbe527b 100644 --- a/src/share/i18n/languages/en.ts +++ b/src/share/i18n/languages/en.ts @@ -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...', diff --git a/src/share/i18n/languages/zh-CN.ts b/src/share/i18n/languages/zh-CN.ts index be7d3578..9d26e5f1 100644 --- a/src/share/i18n/languages/zh-CN.ts +++ b/src/share/i18n/languages/zh-CN.ts @@ -494,6 +494,7 @@ const data: BaseLanguage = { }, 'code-run': { 'run': '运行', + 'stop': '停止', 'run-in-xterm-tips': '在终端中运行代码,%s + 单击不退出解释器', 'run-in-xterm': '终端中运行', 'running': '运行中……', diff --git a/src/share/i18n/languages/zh-TW.ts b/src/share/i18n/languages/zh-TW.ts index 35fee476..f3f56025 100644 --- a/src/share/i18n/languages/zh-TW.ts +++ b/src/share/i18n/languages/zh-TW.ts @@ -494,6 +494,7 @@ const data: BaseLanguage = { }, 'code-run': { 'run': '執行', + 'stop': '停止', 'run-in-xterm-tips': '在終端中執行程式碼,%s + 點擊不退出解釋器', 'run-in-xterm': '終端中執行', 'running': '執行中……',