Skip to content

Commit 92a6bf0

Browse files
committed
python実行の排他制御
1 parent 404de61 commit 92a6bf0

6 files changed

Lines changed: 51 additions & 15 deletions

File tree

app/terminal/python/embedded.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,15 @@ import { usePyodide } from "./pyodide";
66

77
export function PythonEmbeddedTerminal({ content }: { content: string }) {
88
const initCommands = useMemo(() => splitContents(content), [content]);
9-
const { init, initializing, ready, runPython, checkSyntax } = usePyodide();
9+
const { init, initializing, ready, runPython, checkSyntax, mutex } = usePyodide();
1010

1111
return (
1212
<TerminalComponent
1313
initRuntime={init}
1414
runtimeInitializing={initializing}
1515
runtimeReady={ready}
1616
initCommand={initCommands}
17+
mutex={mutex}
1718
prompt=">>> "
1819
promptMore="... "
1920
language="python"

app/terminal/python/page.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { TerminalComponent } from "../terminal";
44
import { usePyodide } from "./pyodide";
55

66
export default function PythonPage() {
7-
const { init, ready, initializing, runPython, checkSyntax } = usePyodide();
7+
const { init, ready, initializing, runPython, checkSyntax, mutex } = usePyodide();
88
return (
99
<div className="p-4">
1010
<TerminalComponent
@@ -16,6 +16,7 @@ export default function PythonPage() {
1616
promptMore="... "
1717
language="python"
1818
tabSize={4}
19+
mutex={mutex}
1920
sendCommand={runPython}
2021
checkSyntax={checkSyntax}
2122
/>

app/terminal/python/pyodide.tsx

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
useContext,
1212
} from "react";
1313
import { SyntaxStatus, TerminalOutput } from "../terminal";
14+
import { Mutex, MutexInterface } from "async-mutex";
1415

1516
declare global {
1617
interface Window {
@@ -19,9 +20,12 @@ declare global {
1920
}
2021

2122
interface IPyodideContext {
22-
init: () => void;
23-
initializing: boolean;
24-
ready: boolean;
23+
init: () => void; // Pyodideを初期化する
24+
initializing: boolean; // Pyodideの初期化が実行中
25+
ready: boolean; // Pyodideの初期化が完了した
26+
// runPython() などを複数の場所から同時実行すると結果が混ざる。
27+
// コードブロックの実行全体を mutex.runExclusive() で囲うことで同時実行を防ぐ必要がある
28+
mutex: MutexInterface;
2529
runPython: (code: string) => Promise<TerminalOutput[]>;
2630
checkSyntax: (code: string) => Promise<SyntaxStatus>;
2731
}
@@ -55,13 +59,14 @@ export function PyodideProvider({ children }: { children: ReactNode }) {
5559
const [ready, setReady] = useState<boolean>(false);
5660
const [initializing, setInitializing] = useState<boolean>(false);
5761
const pyodideOutput = useRef<TerminalOutput[]>([]);
62+
const mutex = useRef<MutexInterface>(new Mutex());
5863

5964
const init = useCallback(() => {
6065
// next.config.ts 内でpyodideをimportし、バージョンを取得している
6166
const PYODIDE_CDN = `https://cdn.jsdelivr.net/pyodide/v${process.env.PYODIDE_VERSION}/full/`;
6267

6368
const initPyodide = () => {
64-
if(initializing) return;
69+
if (initializing) return;
6570
setInitializing(true);
6671
window
6772
.loadPyodide({
@@ -109,6 +114,10 @@ export function PyodideProvider({ children }: { children: ReactNode }) {
109114

110115
const runPython = useCallback<(code: string) => Promise<TerminalOutput[]>>(
111116
async (code: string) => {
117+
if (!mutex.current.isLocked()) {
118+
throw new Error("mutex of PyodideContext must be locked for runPython");
119+
}
120+
112121
const pyodide = pyodideRef.current;
113122
if (!pyodide || !ready) {
114123
return [{ type: "error", message: "Pyodide is not ready yet." }];
@@ -165,6 +174,10 @@ export function PyodideProvider({ children }: { children: ReactNode }) {
165174
*/
166175
const checkSyntax = useCallback<(code: string) => Promise<SyntaxStatus>>(
167176
async (code) => {
177+
if (mutex.current.isLocked()) {
178+
throw new Error("mutex of PyodideContext must not be locked for checkSyntax");
179+
}
180+
168181
const pyodide = pyodideRef.current;
169182
if (!pyodide || !ready) return "invalid";
170183

@@ -191,6 +204,7 @@ export function PyodideProvider({ children }: { children: ReactNode }) {
191204
ready,
192205
runPython,
193206
checkSyntax,
207+
mutex: mutex.current,
194208
}}
195209
>
196210
{children}

app/terminal/terminal.tsx

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { FitAddon } from "@xterm/addon-fit";
66
import "@xterm/xterm/css/xterm.css";
77
import { highlightCodeToAnsi } from "./highlight";
88
import chalk from "chalk";
9+
import { MutexInterface } from "async-mutex";
910

1011
export interface TerminalOutput {
1112
type: "stdout" | "stderr" | "error" | "return"; // 出力の種類
@@ -23,6 +24,8 @@ interface TerminalComponentProps {
2324
promptMore?: string;
2425
language?: string;
2526
tabSize: number;
27+
// コードブロックの実行全体を mutex.runExclusive() で囲うことで同時実行を防ぐ
28+
mutex: MutexInterface;
2629
// コマンド実行時のコールバック関数
2730
sendCommand: (command: string) => Promise<TerminalOutput[]>;
2831
// 構文チェックのコールバック関数
@@ -45,6 +48,7 @@ export function TerminalComponent(props: TerminalComponentProps) {
4548
tabSize,
4649
sendCommand,
4750
checkSyntax,
51+
mutex,
4852
} = props;
4953

5054
// bufferを更新し、画面に描画する
@@ -237,13 +241,15 @@ export function TerminalComponent(props: TerminalComponentProps) {
237241
command: string;
238242
output: TerminalOutput[];
239243
}[] = [];
240-
for (const cmd of props.initCommand) {
241-
const outputs = await sendCommand(cmd.command);
242-
initCommandResult.push({
243-
command: cmd.command,
244-
output: outputs,
245-
});
246-
}
244+
await mutex.runExclusive(async () => {
245+
for (const cmd of props.initCommand!) {
246+
const outputs = await sendCommand(cmd.command);
247+
initCommandResult.push({
248+
command: cmd.command,
249+
output: outputs,
250+
});
251+
}
252+
});
247253
// 実際の実行結果でターミナルを再描画
248254
terminalInstanceRef.current!.clear();
249255
for (const cmd of initCommandResult) {
@@ -264,6 +270,7 @@ export function TerminalComponent(props: TerminalComponentProps) {
264270
onOutput,
265271
props.initCommand,
266272
sendCommand,
273+
mutex,
267274
]);
268275

269276
const keyHandler = useCallback(
@@ -294,7 +301,9 @@ export function TerminalComponent(props: TerminalComponentProps) {
294301
terminalInstanceRef.current.writeln("");
295302
const command = inputBuffer.current.join("\n").trim();
296303
inputBuffer.current = [];
297-
const outputs = await sendCommand(command);
304+
const outputs = await mutex.runExclusive(() =>
305+
sendCommand(command)
306+
);
298307
onOutput(outputs);
299308
}
300309
} else if (code === 127) {
@@ -331,7 +340,7 @@ export function TerminalComponent(props: TerminalComponentProps) {
331340
}
332341
}
333342
},
334-
[updateBuffer, sendCommand, onOutput, checkSyntax, tabSize]
343+
[updateBuffer, sendCommand, onOutput, checkSyntax, tabSize, mutex]
335344
);
336345
useEffect(() => {
337346
if (terminalInstanceRef.current && termReady && runtimeReady) {

package-lock.json

Lines changed: 10 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
"@opennextjs/cloudflare": "^1.6.3",
1919
"@xterm/addon-fit": "^0.11.0-beta.115",
2020
"@xterm/xterm": "^5.6.0-beta.115",
21+
"async-mutex": "^0.5.0",
2122
"chalk": "^5.5.0",
2223
"next": "<15.4",
2324
"prismjs": "^1.30.0",

0 commit comments

Comments
 (0)