Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
243 changes: 128 additions & 115 deletions app/main/runtime/runtimeHandler.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { ipcMain, IpcMainInvokeEvent, app } from "electron"
import { spawn, ChildProcessWithoutNullStreams } from "node:child_process"
import { spawn, ChildProcessWithoutNullStreams, SpawnOptionsWithoutStdio } from "node:child_process"
import fs from "fs"
import path from "node:path"
import { RunPythonPayload } from "../payloads"
Expand All @@ -26,122 +26,142 @@ type RunPythonResult =
interpreter: string
}

const getSystemPythonCommands = (): string[] => {
if (process.platform === "win32") {
return ["python", "python3", "py"]
}

return ["python3", "python"]
}

const spawnPython = (
command: string,
args: string[],
options: SpawnOptionsWithoutStdio
): Promise<ChildProcessWithoutNullStreams> => {
return new Promise((resolve, reject) => {
const child = spawn(command, args, options) as ChildProcessWithoutNullStreams

const handleSpawn = (): void => {
child.removeListener("error", handleError)
resolve(child)
}

const handleError = (error: Error): void => {
child.removeListener("spawn", handleSpawn)
reject(error)
}

child.once("spawn", handleSpawn)
child.once("error", handleError)
})
}

ipcMain.handle(
"run-python-code",
(
async (
_: IpcMainInvokeEvent,
data: RunPythonPayload
): Promise<RunPythonResult> => {
return new Promise((resolve) => {
let runPath: string
let isTempFile = false
let resolved = false

const filePath = data.filePath
const code = data.code
const useEmbed = data.useEmbed

const tempDir = path.join(app.getAppPath(), "temp")

const embeddedPy = app.isPackaged
? path.join(process.resourcesPath, "runtime", "python", "python.exe")
: path.join(__dirname, "..", "runtime", "python", "python.exe")

const cleanup = (): void => {
if (isTempFile && runPath && fs.existsSync(runPath)) {
try {
fs.unlinkSync(runPath)
} catch {}
let runPath: string
let isTempFile = false

const filePath = data.filePath
const code = data.code
const useEmbed = data.useEmbed

const tempDir = path.join(app.getAppPath(), "temp")

const embeddedPy = app.isPackaged
? path.join(process.resourcesPath, "runtime", "python", "python.exe")
: path.join(__dirname, "..", "runtime", "python", "python.exe")

const cleanup = (): void => {
if (isTempFile && runPath && fs.existsSync(runPath)) {
try {
fs.unlinkSync(runPath)
} catch {}
}
}

try {
if (filePath) {
if (!fs.existsSync(filePath)) {
return {
type: "file_not_found",
result: `File not found: ${filePath}`
}
}

runPath = filePath
} else if (code) {
if (!fs.existsSync(tempDir)) {
fs.mkdirSync(tempDir, { recursive: true })
}

runPath = path.join(tempDir, `temp-${Date.now()}.py`)
fs.writeFileSync(runPath, code, "utf8")
isTempFile = true
} else {
return {
type: "no_input",
result: "No code or filePath provided"
}
}

const finish = (result: RunPythonResult): void => {
if (resolved) return
resolved = true
cleanup()
resolve(result)
const spawnOptions: SpawnOptionsWithoutStdio = {
cwd: path.dirname(runPath)
}

const trySpawn = (
command: string,
args: string[],
options: any = {}
): ChildProcessWithoutNullStreams | null => {
const candidates = useEmbed && fs.existsSync(embeddedPy)
? [embeddedPy]
: getSystemPythonCommands()

let py: ChildProcessWithoutNullStreams | null = null
let pyCommand = ""

for (const command of candidates) {
try {
return spawn(command, args, options)
py = await spawnPython(command, [runPath], spawnOptions)
pyCommand = command
break
} catch {
return null
continue
}
}

try {
if (filePath) {
if (!fs.existsSync(filePath)) {
return finish({
type: "file_not_found",
result: `File not found: ${filePath}`
})
}

runPath = filePath
} else if (code) {
if (!fs.existsSync(tempDir)) {
fs.mkdirSync(tempDir, { recursive: true })
}

runPath = path.join(tempDir, `temp-${Date.now()}.py`)
fs.writeFileSync(runPath, code, "utf8")
isTempFile = true
} else {
return finish({
type: "no_input",
result: "No code or filePath provided"
})
if (!py) {
cleanup()
return {
type: "python_not_found",
result: useEmbed
? "Embedded Python not found"
: "System Python not found"
}
}

let pyCommand: string
let pyArgs: string[] = [runPath]

if (useEmbed) {
if (!fs.existsSync(embeddedPy)) {
return finish({
type: "python_not_found",
result: "Embedded Python not found"
})
}
let stdout = ""
let stderr = ""

pyCommand = embeddedPy
} else {
pyCommand = "python"
}
const result = await new Promise<RunPythonResult>((resolve) => {
let settled = false
let timeout: NodeJS.Timeout | null = null

let py = trySpawn(pyCommand, pyArgs, {
cwd: path.dirname(runPath)
})
const settle = (value: RunPythonResult): void => {
if (settled) return
settled = true

if (!py && !useEmbed) {
pyCommand = "py"
py = trySpawn(pyCommand, pyArgs, {
cwd: path.dirname(runPath)
})
}
if (timeout) {
clearTimeout(timeout)
}

if (!py) {
return finish({
type: "python_not_found",
result: useEmbed
? "Embedded Python not found"
: "System Python not found"
})
resolve(value)
}

let stdout = ""
let stderr = ""
timeout = setTimeout(() => {
py?.kill()

const timeout = setTimeout(() => {
py!.kill()

finish({
settle({
type: "timeout",
result: "Execution timed out"
})
Expand All @@ -156,25 +176,14 @@ ipcMain.handle(
})

py.on("error", () => {
clearTimeout(timeout)

if (!useEmbed && pyCommand === "python") {
py = spawn("py", pyArgs, {
cwd: path.dirname(runPath)
})
return
}

finish({
settle({
type: "spawn_error",
result: "Failed to start Python process"
})
})

py.on("close", (exitCode: number | null) => {
clearTimeout(timeout)

finish({
settle({
type: exitCode === 0 ? "success" : "error",
stdout,
stderr,
Expand All @@ -183,16 +192,20 @@ ipcMain.handle(
interpreter: pyCommand
})
})
})

} catch (err: unknown) {
const message =
err instanceof Error ? err.message : String(err)
cleanup()
return result
} catch (err: unknown) {
cleanup()

finish({
type: "internal_error",
result: message
})
const message =
err instanceof Error ? err.message : String(err)

return {
type: "internal_error",
result: message
}
})
}
}
)
)
29 changes: 17 additions & 12 deletions assets/js/settings.js
Original file line number Diff line number Diff line change
Expand Up @@ -136,23 +136,28 @@ export async function handleSettings(settingsObject) {

themeSelect.appendTo(document.querySelector("#setting_theme"))

if(platform == "win32") {
const pyInfo = await window.electron.getPython()
const pyInfo = await window.electron.getPython()
const installedPythonLabel = pyInfo != false
? `${gls.get("modals.appearance.editor.pythonRunner.select.userDefined")} (Python ${pyInfo.version})`
: gls.get("modals.appearance.editor.pythonRunner.select.userDefined")

if(platform == "win32") {
pythonRunnerMethodSelect.add("builtin", gls.get("modals.appearance.editor.pythonRunner.select.builtIn")).default()
}

if(pyInfo != false) {
pythonRunnerMethodSelect.add("installed", `${gls.get("modals.appearance.editor.pythonRunner.select.userDefined")} (Python ${pyInfo.version})`)
}

pythonRunnerMethodSelect.appendTo(document.querySelector("#setting_pythonRunMethod"))
pythonRunnerMethodSelect.on("click", (e) => {
const ID = e.id
const installedPythonOption = pythonRunnerMethodSelect.add("installed", installedPythonLabel)

Setting.pythonRunnerMethod(ID)
})
if(platform != "win32") {
installedPythonOption.default()
}

pythonRunnerMethodSelect.appendTo(document.querySelector("#setting_pythonRunMethod"))
pythonRunnerMethodSelect.on("click", (e) => {
const ID = e.id

Setting.pythonRunnerMethod(ID)
})

if(aviableLanguages) {
for(const index in aviableLanguages) {
const id = aviableLanguages[index]
Expand Down Expand Up @@ -353,4 +358,4 @@ export class Setting {
await window.electron.setSettings({ app: { language: value }})
}
}
}
}