From 8c4392861bc279e01990db4fe338058df83f710d Mon Sep 17 00:00:00 2001 From: Nimaoth Date: Sat, 26 Oct 2024 17:47:49 +0200 Subject: [PATCH] Add basic process plugin api --- scripting/plugin_api.nim | 2 ++ scripting/plugin_runtime.nim | 12 +++++++++++ scripting/process_api_wasm.nim | 22 +++++++++++++++++++ src/desktop_main.nim | 1 + src/misc/async_process.nim | 19 +++++++++++------ src/plugin_api/process.nim | 39 ++++++++++++++++++++++++++++++++++ 6 files changed, 88 insertions(+), 7 deletions(-) create mode 100644 scripting/process_api_wasm.nim create mode 100644 src/plugin_api/process.nim diff --git a/scripting/plugin_api.nim b/scripting/plugin_api.nim index d51e059e..716c8799 100644 --- a/scripting/plugin_api.nim +++ b/scripting/plugin_api.nim @@ -25,6 +25,8 @@ import plugins_api_wasm export plugins_api_wasm import popup_selector_api_wasm export popup_selector_api_wasm +import process_api_wasm +export process_api_wasm import registers_api_wasm export registers_api_wasm import session_api_wasm diff --git a/scripting/plugin_runtime.nim b/scripting/plugin_runtime.nim index 851e81bd..7134800a 100644 --- a/scripting/plugin_runtime.nim +++ b/scripting/plugin_runtime.nim @@ -184,6 +184,18 @@ proc loadWorkspaceFile*(path: string, action: proc(content: Option[string]): voi addScriptAction(key, "", @[("path", "string"), ("callback", "proc(content: Option[string])")], "", false) loadWorkspaceFile(path, key) +proc runProcess*(process: string, args: seq[string], workingDir: Option[string] = string.none, eval: bool = false, callback: proc(output: string, err: string): void {.closure, gcsafe.} = nil) = + let key = "$runProcess" & $callbackId + callbackId += 1 + scriptActions[key] = proc(args: JsonNode): JsonNode = + let args = args.jsonTo(Option[tuple[output: string, err: string]]) + if callback != nil and args.isSome: + callback(args.get.output, args.get.err) + scriptActions.del(key) + return newJNull() + addScriptAction(key, "", @[("process", "string"), ("args", "seq[string]"), ("workingDir", "Option[string]"), ("callback", "proc(content: Option[string])")], "", false) + runProcess(process, args, key.some, workingDir, eval) + macro addCommand*(context: string, keys: string, action: string, args: varargs[untyped]): untyped = let (stmts, str) = bindArgs(args) return genAst(stmts, context, keys, action, str): diff --git a/scripting/process_api_wasm.nim b/scripting/process_api_wasm.nim new file mode 100644 index 00000000..73fe1be9 --- /dev/null +++ b/scripting/process_api_wasm.nim @@ -0,0 +1,22 @@ +import std/[json, options] +import scripting_api, misc/myjsonutils + +## This file is auto generated, don't modify. + + +proc process_runProcess_void_PluginService_string_seq_string_Option_string_Option_string_bool_wasm( + arg: cstring): cstring {.importc.} +proc runProcess*(process: string; args: seq[string]; + callback: Option[string] = string.none; + workingDir: Option[string] = string.none; eval: bool = false) {. + gcsafe, raises: [].} = + var argsJson = newJArray() + argsJson.add process.toJson() + argsJson.add args.toJson() + argsJson.add callback.toJson() + argsJson.add workingDir.toJson() + argsJson.add eval.toJson() + let argsJsonString = $argsJson + let res {.used.} = process_runProcess_void_PluginService_string_seq_string_Option_string_Option_string_bool_wasm( + argsJsonString.cstring) + diff --git a/src/desktop_main.nim b/src/desktop_main.nim index 790f6b19..0710be15 100644 --- a/src/desktop_main.nim +++ b/src/desktop_main.nim @@ -245,6 +245,7 @@ import scripting/scripting_base import vcs/vcs_api import wasm3, wasm3/[wasm3c, wasmconversions] import selector_popup, collab, layout, config_provider, document_editor, session, events, register, selector_popup_builder_impl, vfs_service +import plugin_api/[process] generatePluginBindings() static: diff --git a/src/misc/async_process.nim b/src/misc/async_process.nim index 1129667a..45edd9ad 100644 --- a/src/misc/async_process.nim +++ b/src/misc/async_process.nim @@ -417,14 +417,19 @@ type RunProcessThreadArgs = tuple workingDir: string captureOut: bool = true captureErr: bool = true + evalCommand: bool = false proc readProcessOutputThread(args: RunProcessThreadArgs): (seq[string], seq[string], ref Exception) {.gcsafe.} = try: when debugAsyncProcess: asyncProcessDebugOutput.send(fmt"Start process {args}") + var options: set[ProcessOption] = {poUsePath, poDaemon} + if args.evalCommand: + options.incl poEvalCommand + let process = startProcess(args.processName, workingDir=args.workingDir, args=args.args, - options={poUsePath, poDaemon}) + options=options) if args.captureOut: var outp = process.outputStream @@ -457,21 +462,21 @@ proc readProcessOutputThread(args: RunProcessThreadArgs): (seq[string], seq[stri result[2] = getCurrentException() proc runProcessAsync*(name: string, args: seq[string] = @[], workingDir: string = "", - maxLines: int = int.high): Future[seq[string]] {.async.} = + maxLines: int = int.high, eval: bool = false): Future[seq[string]] {.async.} = log lvlInfo, fmt"[runProcessAsync] {name}, {args}, '{workingDir}', {maxLines}" - let (lines, _, err) = await spawnAsync(readProcessOutputThread, (name, args, maxLines, workingDir, true, false)) + let (lines, _, err) = await spawnAsync(readProcessOutputThread, (name, args, maxLines, workingDir, true, false, eval)) if err != nil: - raise newException(IOError, "", err) + raise newException(IOError, err.msg, err) return lines proc runProcessAsyncOutput*(name: string, args: seq[string] = @[], workingDir: string = "", - maxLines: int = int.high): Future[tuple[output: string, err: string]] {.async.} = + maxLines: int = int.high, eval: bool = false): Future[tuple[output: string, err: string]] {.async.} = log lvlInfo, fmt"[runProcessAsync] {name}, {args}, '{workingDir}', {maxLines}" - let (outLines, errLines, err) = await spawnAsync(readProcessOutputThread, (name, args, maxLines, workingDir, true, true)) + let (outLines, errLines, err) = await spawnAsync(readProcessOutputThread, (name, args, maxLines, workingDir, true, true, eval)) if err != nil: - raise newException(IOError, "", err) + raise newException(IOError, err.msg, err) return (outLines.join("\n"), errLines.join("\n")) proc readProcessOutputCallback(process: AsyncProcess, diff --git a/src/plugin_api/process.nim b/src/plugin_api/process.nim new file mode 100644 index 00000000..45937855 --- /dev/null +++ b/src/plugin_api/process.nim @@ -0,0 +1,39 @@ +import std/[tables, options, json, os, strutils] +import results +import misc/[custom_async, custom_logger, myjsonutils, util, async_process] +import scripting/[expose, scripting_base] +import workspaces/workspace +import service, dispatch_tables, vfs, vfs_local, vfs_service + +{.push gcsafe.} +{.push raises: [].} + +logCategory "plugin-api-process" + +########################################################################### + +proc runProcessImpl(self: PluginService, process: string, args: seq[string], callback: Option[string] = string.none, workingDir: Option[string] = string.none, eval: bool = false) {.async: (raises: []).} = + type ResultType = tuple[output: string, err: string] + + let workingDir = if workingDir.getSome(wd): + let vfs = self.services.getService(VFSService).get.vfs + vfs.localize(wd) + else: + let workspace = self.services.getService(Workspace).get + workspace.path + + try: + let (output, err) = await runProcessAsyncOutput(process, args, workingDir=workingDir, eval=eval) + log lvlDebug, &"runProcess '{process} {args.join($' ')}' in '{workingDir}'" + if callback.getSome(c): + discard self.callScriptAction(c, (output: output, err: err).some.toJson) + + except CatchableError as e: + log lvlError, &"Failed to run process '{process}': {e.msg}" + if callback.getSome(c): + discard self.callScriptAction(c, ResultType.none.toJson) + +proc runProcess*(self: PluginService, process: string, args: seq[string], callback: Option[string] = string.none, workingDir: Option[string] = string.none, eval: bool = false) {.expose("process").} = + asyncSpawn self.runProcessImpl(process, args, callback, workingDir, eval) + +addGlobalDispatchTable "process", genDispatchTable("process")