diff --git a/src/cli/cli_common.h b/src/cli/cli_common.h new file mode 100644 index 00000000..1e7fe855 --- /dev/null +++ b/src/cli/cli_common.h @@ -0,0 +1,24 @@ +#ifndef cli_common_h +#define cli_common_h + +#include +#include + +inline char* cli_strdup(const char* s) { + size_t len = strlen(s) + 1; + char* m = (char*)malloc(len); + if (m == NULL) return NULL; + return memcpy(m, s, len); +} + +inline char* cli_strndup(const char* s, size_t n) { + char* m; + size_t len = strlen(s); + if (n < len) len = n; + m = (char*)malloc(len + 1); + if (m == NULL) return NULL; + m[len] = '\0'; + return memcpy(m, s, len); +} + +#endif \ No newline at end of file diff --git a/src/cli/modules.c b/src/cli/modules.c index a3c7f6c1..f87a501c 100644 --- a/src/cli/modules.c +++ b/src/cli/modules.c @@ -32,6 +32,7 @@ extern void processCwd(WrenVM* vm); extern void processPid(WrenVM* vm); extern void processPpid(WrenVM* vm); extern void processVersion(WrenVM* vm); +extern void processExec(WrenVM* vm); extern void statPath(WrenVM* vm); extern void statBlockCount(WrenVM* vm); extern void statBlockSize(WrenVM* vm); @@ -180,6 +181,7 @@ static ModuleRegistry modules[] = STATIC_METHOD("pid", processPid) STATIC_METHOD("ppid", processPpid) STATIC_METHOD("version", processVersion) + STATIC_METHOD("exec_(_,_,_,_,_)", processExec) END_CLASS END_MODULE MODULE(repl) diff --git a/src/module/os.c b/src/module/os.c index c13cd33f..3eb483e7 100644 --- a/src/module/os.c +++ b/src/module/os.c @@ -1,6 +1,11 @@ #include "os.h" #include "uv.h" #include "wren.h" +#include "vm.h" +#include "scheduler.h" +#include "cli_common.h" + +#include #if __APPLE__ #include "TargetConditionals.h" @@ -128,3 +133,121 @@ void processVersion(WrenVM* vm) { wrenEnsureSlots(vm, 1); wrenSetSlotString(vm, 0, WREN_VERSION_STRING); } + +// Called when the UV handle for a process is done, so we can free it +static void processOnClose(uv_handle_t* req) +{ + free((void*)req); +} + +// Called when a process is finished running +static void processOnExit(uv_process_t* req, int64_t exit_status, int term_signal) +{ + ProcessData* data = (ProcessData*)req->data; + WrenHandle* fiber = data->fiber; + + uv_close((uv_handle_t*)req, processOnClose); + + int index = 0; + char* arg = data->options.args[index]; + while (arg != NULL) + { + free(arg); + index += 1; + arg = data->options.args[index]; + } + + index = 0; + if (data->options.env) { + char* env = data->options.env[index]; + while (env != NULL) + { + free(env); + index += 1; + env = data->options.env[index]; + } + } + + free((void*)data); + + schedulerResume(fiber, true); + wrenSetSlotDouble(getVM(), 2, (double)exit_status); + schedulerFinishResume(); +} + +// 1 2 3 4 5 +// exec_(cmd, args, cwd, env, fiber) +void processExec(WrenVM* vm) +{ + ProcessData* data = (ProcessData*)malloc(sizeof(ProcessData)); + memset(data, 0, sizeof(ProcessData)); + + //:todo: add env + cwd + flags args + + char* cmd = cli_strdup(wrenGetSlotString(vm, 1)); + + if (wrenGetSlotType(vm, 3) != WREN_TYPE_NULL) { + const char* cwd = wrenGetSlotString(vm, 3); + data->options.cwd = cwd; + } + + data->options.file = cmd; + data->options.exit_cb = processOnExit; + data->fiber = wrenGetSlotHandle(vm, 5); + + wrenEnsureSlots(vm, 6); + + if (wrenGetSlotType(vm, 4) == WREN_TYPE_NULL) { + // no environment specified + } else if (wrenGetSlotType(vm, 4) == WREN_TYPE_LIST) { + int envCount = wrenGetListCount(vm, 4); + int envSize = sizeof(char*) * (envCount + 1); + + data->options.env = (char**)malloc(envSize); + data->options.env[envCount] = NULL; + + for (int i = 0; i < envCount ; i++) + { + wrenGetListElement(vm, 4, i, 6); + if (wrenGetSlotType(vm, 6) != WREN_TYPE_STRING) { + wrenSetSlotString(vm, 0, "arguments to env are supposed to be strings"); + wrenAbortFiber(vm, 0); + } + char* envKeyPlusValue = cli_strdup(wrenGetSlotString(vm, 6)); + data->options.env[i] = envKeyPlusValue; + } + } + + int argCount = wrenGetListCount(vm, 2); + int argsSize = sizeof(char*) * (argCount + 2); + + // First argument is the cmd, last+1 is NULL + data->options.args = (char**)malloc(argsSize); + data->options.args[0] = cmd; + data->options.args[argCount + 1] = NULL; + + for (int i = 0; i < argCount; i++) + { + wrenGetListElement(vm, 2, i, 3); + if (wrenGetSlotType(vm, 3) != WREN_TYPE_STRING) { + wrenSetSlotString(vm, 0, "arguments to args are supposed to be strings"); + wrenAbortFiber(vm, 0); + } + char* arg = cli_strdup(wrenGetSlotString(vm, 3)); + data->options.args[i + 1] = arg; + } + + uv_process_t* child_req = (uv_process_t*)malloc(sizeof(uv_process_t)); + memset(child_req, 0, sizeof(uv_process_t)); + + child_req->data = data; + + int r; + if ((r = uv_spawn(getLoop(), child_req, &data->options))) + { + // should be stderr??? but no idea how to make tests work/pass with that + fprintf(stdout, "Could not launch %s, reason: %s\n", cmd, uv_strerror(r)); + wrenSetSlotString(vm, 0, "Could not spawn process."); + wrenAbortFiber(vm, 0); + } +} diff --git a/src/module/os.h b/src/module/os.h index b9988f80..55c8beed 100644 --- a/src/module/os.h +++ b/src/module/os.h @@ -1,10 +1,16 @@ #ifndef process_h #define process_h +#include "uv.h" #include "wren.h" #define WREN_PATH_MAX 4096 +typedef struct { + WrenHandle* fiber; + uv_process_options_t options; +} ProcessData; + // Stores the command line arguments passed to the CLI. void osSetArguments(int argc, const char* argv[]); diff --git a/src/module/os.wren b/src/module/os.wren index 4d9ec25a..80a4c8f6 100644 --- a/src/module/os.wren +++ b/src/module/os.wren @@ -1,3 +1,5 @@ +import "scheduler" for Scheduler + class Platform { foreign static homePath foreign static isPosix @@ -10,6 +12,34 @@ class Process { // TODO: This will need to be smarter when wren supports CLI options. static arguments { allArguments.count >= 2 ? allArguments[2..-1] : [] } + static exec(cmd) { + return exec(cmd, null, null, null) + } + + static exec(cmd, args) { + return exec(cmd, args, null, null) + } + + static exec(cmd, args, cwd) { + return exec(cmd, args, cwd, null) + } + + static exec(cmd, args, cwd, envMap) { + var env = [] + args = args || [] + if (envMap is Map) { + for (entry in envMap) { + env.add([entry.key, entry.value].join("=")) + } + } else if (envMap == null) { + env = null + } else { + Fiber.abort("environment vars must be passed as a Map") + } + return Scheduler.await_ { exec_(cmd, args, cwd, env, Fiber.current) } + } + + foreign static exec_(cmd, args, cwd, env, fiber) foreign static allArguments foreign static cwd foreign static pid diff --git a/src/module/os.wren.inc b/src/module/os.wren.inc index 8c7c8522..8c12009b 100644 --- a/src/module/os.wren.inc +++ b/src/module/os.wren.inc @@ -2,6 +2,8 @@ // from `src/module/os.wren` using `util/wren_to_c_string.py` static const char* osModuleSource = +"import \"scheduler\" for Scheduler\n" +"\n" "class Platform {\n" " foreign static homePath\n" " foreign static isPosix\n" @@ -14,6 +16,34 @@ static const char* osModuleSource = " // TODO: This will need to be smarter when wren supports CLI options.\n" " static arguments { allArguments.count >= 2 ? allArguments[2..-1] : [] }\n" "\n" +" static exec(cmd) {\n" +" return exec(cmd, null, null, null)\n" +" }\n" +"\n" +" static exec(cmd, args) {\n" +" return exec(cmd, args, null, null)\n" +" }\n" +"\n" +" static exec(cmd, args, cwd) { \n" +" return exec(cmd, args, cwd, null) \n" +" }\n" +" \n" +" static exec(cmd, args, cwd, envMap) { \n" +" var env = []\n" +" args = args || []\n" +" if (envMap is Map) {\n" +" for (entry in envMap) {\n" +" env.add([entry.key, entry.value].join(\"=\"))\n" +" }\n" +" } else if (envMap == null) {\n" +" env = null\n" +" } else {\n" +" Fiber.abort(\"environment vars must be passed as a Map\")\n" +" }\n" +" return Scheduler.await_ { exec_(cmd, args, cwd, env, Fiber.current) }\n" +" }\n" +"\n" +" foreign static exec_(cmd, args, cwd, env, fiber)\n" " foreign static allArguments\n" " foreign static cwd\n" " foreign static pid\n" diff --git a/test/os/process/exec.wren b/test/os/process/exec.wren new file mode 100644 index 00000000..c2db18da --- /dev/null +++ b/test/os/process/exec.wren @@ -0,0 +1,57 @@ +import "os" for Platform, Process + +var TRY = Fn.new { |fn| + var fiber = Fiber.new { + fn.call() + } + return fiber.try() +} + +var result +if(Platform.name == "Windows") { + result = Process.exec("cmd.exe") +} else { + result = Process.exec("true") +} +System.print(result) // expect: 0 + +// basics + +if (Platform.isWindows) { + // TODO: more windows argument specific tests +} else { + // known output of success/fail based on only command name + System.print(Process.exec("true")) // expect: 0 + System.print(Process.exec("false")) // expect: 1 + // these test that our arguments are being passed as it proves + // they effect the result code returned + System.print(Process.exec("test", ["2", "-eq", "2"])) // expect: 0 + System.print(Process.exec("test", ["2", "-eq", "3"])) // expect: 1 +} + +// cwd + +if (Platform.isWindows) { + // TODO: can this be done with dir on windows? +} else { + // tests exists in our project folder + System.print(Process.exec("ls", ["test"])) // expect: 0 + // but does not in our `src` folder + System.print(Process.exec("ls", ["test"], "./src/")) // expect: 1 +} + +// env + +if (Platform.name == "Windows") { + // TODO: how? +} else { + System.print(Process.exec("true",[],null,{})) // expect: 0 + var result = TRY.call { + Process.exec("ls",[],null,{"PATH": "/whereiscarmen/"}) + } + System.print(result) + // TODO: should be on stderr + // expect: Could not launch ls, reason: no such file or directory + // TODO: should this be a runtime error????? + // expect: Could not spawn process. +} \ No newline at end of file