From c0952e62fd0ee16a3275bb69e0de04c836b39015 Mon Sep 17 00:00:00 2001 From: Lewis Russell Date: Wed, 7 Jun 2023 13:52:23 +0100 Subject: feat(lua): add `vim.system()` feat(lua): add vim.system() Problem: Handling system commands in Lua is tedious and error-prone: - vim.fn.jobstart() is vimscript and comes with all limitations attached to typval. - vim.loop.spawn is too low level Solution: Add vim.system(). Partly inspired by Python's subprocess module Does not expose any libuv objects. --- test/functional/lua/system_spec.lua | 57 +++++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 test/functional/lua/system_spec.lua (limited to 'test/functional/lua/system_spec.lua') diff --git a/test/functional/lua/system_spec.lua b/test/functional/lua/system_spec.lua new file mode 100644 index 0000000000..836d3a83b0 --- /dev/null +++ b/test/functional/lua/system_spec.lua @@ -0,0 +1,57 @@ +local helpers = require('test.functional.helpers')(after_each) +local clear = helpers.clear +local exec_lua = helpers.exec_lua +local eq = helpers.eq + +local function system_sync(cmd, opts) + return exec_lua([[ + return vim.system(...):wait() + ]], cmd, opts) +end + +local function system_async(cmd, opts) + exec_lua([[ + local cmd, opts = ... + _G.done = false + vim.system(cmd, opts, function(obj) + _G.done = true + _G.ret = obj + end) + ]], cmd, opts) + + while true do + if exec_lua[[return _G.done]] then + break + end + end + + return exec_lua[[return _G.ret]] +end + +describe('vim.system', function() + before_each(function() + clear() + end) + + for name, system in pairs{ sync = system_sync, async = system_async, } do + describe('('..name..')', function() + it('can run simple commands', function() + eq('hello\n', system({'echo', 'hello' }, { text = true }).stdout) + end) + + it('handle input', function() + eq('hellocat', system({ 'cat' }, { stdin = 'hellocat', text = true }).stdout) + end) + + it ('supports timeout', function() + eq({ + code = 0, + signal = 2, + stdout = '', + stderr = "Command timed out: 'sleep 10'" + }, system({ 'sleep', '10' }, { timeout = 1 })) + end) + end) + end + +end) -- cgit From a44521f46e6f79171d034e5cce1a4dc266d23e49 Mon Sep 17 00:00:00 2001 From: Lewis Russell Date: Sun, 3 Sep 2023 10:17:24 +0100 Subject: fix(vim.system): let on_exit handle cleanup after kill Fixes #25000 --- test/functional/lua/system_spec.lua | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) (limited to 'test/functional/lua/system_spec.lua') diff --git a/test/functional/lua/system_spec.lua b/test/functional/lua/system_spec.lua index 836d3a83b0..35b9d5cc37 100644 --- a/test/functional/lua/system_spec.lua +++ b/test/functional/lua/system_spec.lua @@ -54,4 +54,28 @@ describe('vim.system', function() end) end + it('kill processes', function() + exec_lua([[ + local signal + local cmd = vim.system({ 'cat', '-' }, { stdin = true }, function(r) + signal = r.signal + end) -- run forever + + cmd:kill('sigint') + + -- wait for the process not to exist + local done = vim.wait(2000, function() + return signal ~= nil + end) + + assert(done, 'process did not exit') + + -- Check the process is no longer running + vim.fn.systemlist({'ps', 'p', tostring(cmd.pid)}) + assert(vim.v.shell_error == 1, 'dwqdqd '..vim.v.shell_error) + + assert(signal == 2) + ]]) + end) + end) -- cgit From 6d5f12efd286c684de8608c07bb0f76a9d594b5b Mon Sep 17 00:00:00 2001 From: Lewis Russell Date: Mon, 4 Sep 2023 11:30:16 +0100 Subject: fix(vim.system): make timeout work properly Mimic the behaviour of timeout(1) from coreutils. --- test/functional/lua/system_spec.lua | 40 ++++++++++++++++++++++++------------- 1 file changed, 26 insertions(+), 14 deletions(-) (limited to 'test/functional/lua/system_spec.lua') diff --git a/test/functional/lua/system_spec.lua b/test/functional/lua/system_spec.lua index 35b9d5cc37..9321468f84 100644 --- a/test/functional/lua/system_spec.lua +++ b/test/functional/lua/system_spec.lua @@ -5,27 +5,39 @@ local eq = helpers.eq local function system_sync(cmd, opts) return exec_lua([[ - return vim.system(...):wait() + local obj = vim.system(...) + local pid = obj.pid + local res = obj:wait() + + -- Check the process is no longer running + vim.fn.systemlist({'ps', 'p', tostring(pid)}) + assert(vim.v.shell_error == 1, 'process still exists') + + return res ]], cmd, opts) end local function system_async(cmd, opts) - exec_lua([[ + return exec_lua([[ local cmd, opts = ... _G.done = false - vim.system(cmd, opts, function(obj) + local obj = vim.system(cmd, opts, function(obj) _G.done = true _G.ret = obj end) - ]], cmd, opts) - while true do - if exec_lua[[return _G.done]] then - break - end - end + local done = vim.wait(10000, function() + return _G.done + end) + + assert(done, 'process did not exit') - return exec_lua[[return _G.ret]] + -- Check the process is no longer running + vim.fn.systemlist({'ps', 'p', tostring(obj.pid)}) + assert(vim.v.shell_error == 1, 'process still exists') + + return _G.ret + ]], cmd, opts) end describe('vim.system', function() @@ -43,12 +55,12 @@ describe('vim.system', function() eq('hellocat', system({ 'cat' }, { stdin = 'hellocat', text = true }).stdout) end) - it ('supports timeout', function() + it('supports timeout', function() eq({ - code = 0, - signal = 2, + code = 124, + signal = 15, stdout = '', - stderr = "Command timed out: 'sleep 10'" + stderr = '' }, system({ 'sleep', '10' }, { timeout = 1 })) end) end) -- cgit From 80d1333b7317460c562a982ac21f900d9fbd89f6 Mon Sep 17 00:00:00 2001 From: Lewis Russell Date: Mon, 4 Sep 2023 12:03:03 +0100 Subject: refactor(vim.system): factor out on_exit handling --- test/functional/lua/system_spec.lua | 27 +++++++++++++++++---------- 1 file changed, 17 insertions(+), 10 deletions(-) (limited to 'test/functional/lua/system_spec.lua') diff --git a/test/functional/lua/system_spec.lua b/test/functional/lua/system_spec.lua index 9321468f84..a988d3f0d7 100644 --- a/test/functional/lua/system_spec.lua +++ b/test/functional/lua/system_spec.lua @@ -5,13 +5,20 @@ local eq = helpers.eq local function system_sync(cmd, opts) return exec_lua([[ + local cmd, opts = ... local obj = vim.system(...) - local pid = obj.pid + + if opts.timeout then + -- Minor delay before calling wait() so the timeout uv timer can have a headstart over the + -- internal call to vim.wait() in wait(). + vim.wait(10) + end + local res = obj:wait() -- Check the process is no longer running - vim.fn.systemlist({'ps', 'p', tostring(pid)}) - assert(vim.v.shell_error == 1, 'process still exists') + local proc = vim.api.nvim_get_proc(obj.pid) + assert(not proc, 'process still exists') return res ]], cmd, opts) @@ -26,15 +33,15 @@ local function system_async(cmd, opts) _G.ret = obj end) - local done = vim.wait(10000, function() + local ok = vim.wait(10000, function() return _G.done end) - assert(done, 'process did not exit') + assert(ok, 'process did not exit') -- Check the process is no longer running - vim.fn.systemlist({'ps', 'p', tostring(obj.pid)}) - assert(vim.v.shell_error == 1, 'process still exists') + local proc = vim.api.nvim_get_proc(obj.pid) + assert(not proc, 'process still exists') return _G.ret ]], cmd, opts) @@ -61,7 +68,7 @@ describe('vim.system', function() signal = 15, stdout = '', stderr = '' - }, system({ 'sleep', '10' }, { timeout = 1 })) + }, system({ 'sleep', '10' }, { timeout = 1000 })) end) end) end @@ -83,8 +90,8 @@ describe('vim.system', function() assert(done, 'process did not exit') -- Check the process is no longer running - vim.fn.systemlist({'ps', 'p', tostring(cmd.pid)}) - assert(vim.v.shell_error == 1, 'dwqdqd '..vim.v.shell_error) + local proc = vim.api.nvim_get_proc(cmd.pid) + assert(not proc, 'process still exists') assert(signal == 2) ]]) -- cgit