-
Notifications
You must be signed in to change notification settings - Fork 1
/
proc_win.lua
290 lines (252 loc) · 6.96 KB
/
proc_win.lua
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
local ffi = require'ffi'
local bit = require'bit'
assert(ffi.os == 'Windows', 'platform not Windows')
local M = {}
local proc = {type = 'process', debug_prefix = 'P'}
--TODO: move relevant ctypes here and get rid of the winapi dependency
--(not worth it unless you are really really bored).
local winapi = require'winapi'
require'winapi.process'
require'winapi.thread'
--see https://docs.microsoft.com/en-us/archive/blogs/twistylittlepassagesallalike/everyone-quotes-command-line-arguments-the-wrong-way
--You will lose precious IQ points if you read that. Lose enough of them
--and you might get a job at Microsoft!
function M.esc_win(s) --escape for putting inside double-quoted string
s = tostring(s)
if not s:find'[\\"]' then
return s
end
s = s:gsub('(\\*)"', function(s) return s:rep(2)..'\\"' end)
s = s:gsub('\\+$', function(s) return s:rep(2) end)
return s
end
function M.quote_path_win(s)
if s:find'%s' then
s = '"'..s..'"'
end
return s
end
function M.quote_arg_win(s)
s = tostring(s)
if not s:find'[ \t\n\v"^&<>|]' then
return s
else
return '"'..M.esc_win(s)..'"'
end
end
function M.env(k, v)
if k then
if v ~= nil then
assert(winapi.SetEnvironmentVariable(k, v))
else
return winapi.GetEnvironmentVariable(k)
end
end
local t = {}
for i,s in ipairs(winapi.GetEnvironmentStrings()) do
local k,v = s:match'^([^=]*)=(.*)'
if k and k ~= '' then --the first two ones are internal and invalid.
t[k] = v
end
end
return t
end
local autokill_job
local error_classes = {
[0x002] = 'not_found', --ERROR_FILE_NOT_FOUND
[0x005] = 'access_denied', --ERROR_ACCESS_DENIED
}
function M.exec(cmd, env, dir, stdin, stdout, stderr, autokill, async, inherit_handles)
if type(cmd) == 'table' then
local t = {}
t[1] = M.quote_path_win(cmd[1])
for i = 2, #cmd do
t[i] = M.quote_arg_win(cmd[i])
end
cmd = table.concat(t, ' ')
end
local inp_rf, inp_wf
local out_rf, out_wf
local err_rf, err_wf
local self = setmetatable({async = async}, {__index = proc})
local function close_all()
if self.stdin then
assert(inp_rf:close())
assert(inp_wf:close())
end
if self.stdout then
assert(out_rf:close())
assert(out_wf:close())
end
if self.stderr then
assert(err_rf:close())
assert(err_wf:close())
end
end
if async then
local sock = require'sock'
self._sleep = sock.sleep
self._clock = sock.clock
else
local time = require'time'
self._sleep = time.sleep
self._clock = time.clock
end
local si
if stdin or stdout or stderr then
if stdin == true then
local fs = require'fs'
inp_rf, inp_wf = fs.pipe{
write_async = async,
read_inheritable = true,
}
if not inp_rf then
close_all()
return nil, inp_wf
end
self.stdin = inp_wf
elseif stdin then
assert(stdin.type == 'pipe')
stdin:set_inheritable(true)
inp_rf = stdin
end
if stdout == true then
local fs = require'fs'
out_rf, out_wf = fs.pipe{
read_async = async,
write_inheritable = true,
}
if not out_rf then
close_all()
return nil, out_wf
end
self.stdout = out_rf
elseif stdout then
assert(stdout.type == 'pipe')
stdout:set_inheritable(true)
out_wf = stdout
end
if stderr == true then
local fs = require'fs'
err_rf, err_wf = fs.pipe{
read_async = async,
write_inheritable = true,
}
if not err_rf then
close_all()
return nil, err_wf
end
self.stderr = err_rf
elseif stderr then
assert(stderr.type == 'pipe')
stderr:set_inheritable(true)
err_wf = stderr
end
si = winapi.STARTUPINFO()
si.hStdInput = inp_rf and inp_rf.handle or winapi.GetStdHandle(winapi.STD_INPUT_HANDLE)
si.hStdOutput = out_wf and out_wf.handle or winapi.GetStdHandle(winapi.STD_OUTPUT_HANDLE)
si.hStdError = err_wf and err_wf.handle or winapi.GetStdHandle(winapi.STD_ERROR_HANDLE)
si.dwFlags = winapi.STARTF_USESTDHANDLES
--NOTE: there's no way to inherit only the std handles: all handles
--declared inheritable in the parent process will be inherited!
inherit_handles = true
end
if autokill and not autokill_job then
autokill_job = winapi.CreateJobObject()
local jeli = winapi.JOBOBJECT_EXTENDED_LIMIT_INFORMATION()
jeli.BasicLimitInformation.LimitFlags = winapi.JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE
winapi.SetInformationJobObject(autokill_job, winapi.C.JobObjectExtendedLimitInformation, jeli)
end
local is_in_job = autokill and winapi.IsProcessInJob(winapi.GetCurrentProcess(), nil)
if _G.note then
note('proc', 'exec', '%s', cmd)
end
local pi, err, errcode = winapi.CreateProcess(
cmd, env, dir, si, inherit_handles,
autokill
and bit.bor(
winapi.CREATE_SUSPENDED, --so we can add it to a job before it starts
is_in_job and winapi.CREATE_BREAKAWAY_FROM_JOB or 0
)
or nil)
if not pi then
close_all()
return nil, error_classes[errcode] or err
end
if autokill then
winapi.AssignProcessToJobObject(autokill_job, pi.hProcess)
winapi.ResumeThread(pi.hThread)
end
--Let the child process have the only handles to their pipe ends,
--otherwise when the child process exits, the pipes will stay open on
--account of us (the parent process) holding a handle to them.
if inp_rf then assert(inp_rf:close()) end
if out_wf then assert(out_wf:close()) end
if err_wf then assert(err_wf:close()) end
self.handle = pi.hProcess
self.main_thread_handle = pi.hThread
self.id = pi.dwProcessId
self.main_thread_id = pi.dwThreadId
return self
end
function proc:forget()
if self.stdin then assert(self.stdin :close()) end
if self.stdout then assert(self.stdout:close()) end
if self.stderr then assert(self.stderr:close()) end
if self.handle then
assert(winapi.CloseHandle(self.handle))
assert(winapi.CloseHandle(self.main_thread_handle))
self.handle = false
self.id = false
self.main_thread_handle = false
self.main_thread_id = false
end
end
--compound the STILL_ACTIVE hack with another hack to signal killed status.
local EXIT_CODE_KILLED = winapi.STILL_ACTIVE + 1
function proc:kill()
if not self.handle then
return nil, 'forgotten'
end
return winapi.TerminateProcess(self.handle, EXIT_CODE_KILLED)
end
function proc:exit_code()
if self._exit_code then
return self._exit_code
elseif self._killed then
return nil, 'killed'
end
if not self.handle then
return nil, 'forgotten'
end
local exitcode = winapi.GetExitCodeProcess(self.handle)
if not exitcode then
return nil, 'active'
end
--save the exit status so we can forget the process.
if exitcode == EXIT_CODE_KILLED then
self._killed = true
else
self._exit_code = exitcode
end
return self:exit_code()
end
function proc:wait(expires)
if not self.handle then
return nil, 'forgotten'
end
while self:status() == 'active' and self._clock() < (expires or 1/0) do
self._sleep(.1)
end
local exit_code, err = self:exit_code()
if exit_code then
return exit_code
else
return nil, err
end
end
function proc:status() --finished | killed | active | forgotten
local code, err = self:exit_code()
return code and 'finished' or err
end
return M