OILS / builtin / process_osh.py View on Github | oilshell.org

381 lines, 224 significant
1#!/usr/bin/env python2
2"""
3builtin_process.py - Builtins that deal with processes or modify process state.
4
5This is sort of the opposite of builtin_pure.py.
6"""
7from __future__ import print_function
8
9from signal import SIGCONT
10
11from _devbuild.gen import arg_types
12from _devbuild.gen.syntax_asdl import loc
13from _devbuild.gen.runtime_asdl import cmd_value, job_state_e, wait_status, wait_status_e
14from core import dev
15from core import error
16from core.error import e_usage, e_die_status
17from core import process # W1_OK, W1_ECHILD
18from core import vm
19from mycpp.mylib import log, tagswitch, print_stderr
20from frontend import flag_util
21from frontend import typed_args
22
23import posix_ as posix
24
25from typing import TYPE_CHECKING, List, Optional, cast
26if TYPE_CHECKING:
27 from core.process import Waiter, ExternalProgram, FdState
28 from core.state import Mem, SearchPath
29 from core.ui import ErrorFormatter
30
31
32class Jobs(vm._Builtin):
33 """List jobs."""
34
35 def __init__(self, job_list):
36 # type: (process.JobList) -> None
37 self.job_list = job_list
38
39 def Run(self, cmd_val):
40 # type: (cmd_value.Argv) -> int
41
42 attrs, arg_r = flag_util.ParseCmdVal('jobs', cmd_val)
43 arg = arg_types.jobs(attrs.attrs)
44
45 if arg.l:
46 style = process.STYLE_LONG
47 elif arg.p:
48 style = process.STYLE_PID_ONLY
49 else:
50 style = process.STYLE_DEFAULT
51
52 self.job_list.DisplayJobs(style)
53
54 if arg.debug:
55 self.job_list.DebugPrint()
56
57 return 0
58
59
60class Fg(vm._Builtin):
61 """Put a job in the foreground."""
62
63 def __init__(self, job_control, job_list, waiter):
64 # type: (process.JobControl, process.JobList, Waiter) -> None
65 self.job_control = job_control
66 self.job_list = job_list
67 self.waiter = waiter
68
69 def Run(self, cmd_val):
70 # type: (cmd_value.Argv) -> int
71
72 job_spec = '' # get current job by default
73 if len(cmd_val.argv) > 1:
74 job_spec = cmd_val.argv[1]
75
76 job = self.job_list.GetJobWithSpec(job_spec)
77 if job is None:
78 log('No job to put in the foreground')
79 return 1
80
81 pgid = job.ProcessGroupId()
82 assert pgid != process.INVALID_PGID, \
83 'Processes put in the background should have a PGID'
84
85 # TODO: Print job ID rather than the PID
86 log('Continue PID %d', pgid)
87 # Put the job's process group back into the foreground. GiveTerminal() must
88 # be called before sending SIGCONT or else the process might immediately get
89 # suspsended again if it tries to read/write on the terminal.
90 self.job_control.MaybeGiveTerminal(pgid)
91 job.SetForeground()
92 # needed for Wait() loop to work
93 job.state = job_state_e.Running
94 posix.killpg(pgid, SIGCONT)
95
96 status = -1
97 wait_st = job.JobWait(self.waiter)
98 UP_wait_st = wait_st
99 with tagswitch(wait_st) as case:
100 if case(wait_status_e.Proc):
101 wait_st = cast(wait_status.Proc, UP_wait_st)
102 status = wait_st.code
103
104 elif case(wait_status_e.Pipeline):
105 wait_st = cast(wait_status.Pipeline, UP_wait_st)
106 # TODO: handle PIPESTATUS? Is this right?
107 status = wait_st.codes[-1]
108
109 elif case(wait_status_e.Cancelled):
110 wait_st = cast(wait_status.Cancelled, UP_wait_st)
111 status = 128 + wait_st.sig_num
112
113 else:
114 raise AssertionError()
115
116 return status
117
118
119class Bg(vm._Builtin):
120 """Put a job in the background."""
121
122 def __init__(self, job_list):
123 # type: (process.JobList) -> None
124 self.job_list = job_list
125
126 def Run(self, cmd_val):
127 # type: (cmd_value.Argv) -> int
128
129 # How does this differ from 'fg'? It doesn't wait and it sets controlling
130 # terminal?
131
132 raise error.Usage("isn't implemented", loc.Missing)
133
134
135class Fork(vm._Builtin):
136
137 def __init__(self, shell_ex):
138 # type: (vm._Executor) -> None
139 self.shell_ex = shell_ex
140
141 def Run(self, cmd_val):
142 # type: (cmd_value.Argv) -> int
143 _, arg_r = flag_util.ParseCmdVal('fork',
144 cmd_val,
145 accept_typed_args=True)
146
147 arg, location = arg_r.Peek2()
148 if arg is not None:
149 e_usage('got unexpected argument %r' % arg, location)
150
151 cmd = typed_args.OptionalBlock(cmd_val)
152 if cmd is None:
153 e_usage('expected a block', loc.Missing)
154
155 return self.shell_ex.RunBackgroundJob(cmd)
156
157
158class ForkWait(vm._Builtin):
159
160 def __init__(self, shell_ex):
161 # type: (vm._Executor) -> None
162 self.shell_ex = shell_ex
163
164 def Run(self, cmd_val):
165 # type: (cmd_value.Argv) -> int
166 _, arg_r = flag_util.ParseCmdVal('forkwait',
167 cmd_val,
168 accept_typed_args=True)
169 arg, location = arg_r.Peek2()
170 if arg is not None:
171 e_usage('got unexpected argument %r' % arg, location)
172
173 cmd = typed_args.OptionalBlock(cmd_val)
174 if cmd is None:
175 e_usage('expected a block', loc.Missing)
176
177 return self.shell_ex.RunSubshell(cmd)
178
179
180class Exec(vm._Builtin):
181
182 def __init__(self, mem, ext_prog, fd_state, search_path, errfmt):
183 # type: (Mem, ExternalProgram, FdState, SearchPath, ErrorFormatter) -> None
184 self.mem = mem
185 self.ext_prog = ext_prog
186 self.fd_state = fd_state
187 self.search_path = search_path
188 self.errfmt = errfmt
189
190 def Run(self, cmd_val):
191 # type: (cmd_value.Argv) -> int
192 _, arg_r = flag_util.ParseCmdVal('exec', cmd_val)
193
194 # Apply redirects in this shell. # NOTE: Redirects were processed earlier.
195 if arg_r.AtEnd():
196 self.fd_state.MakePermanent()
197 return 0
198
199 environ = self.mem.GetExported()
200 i = arg_r.i
201 cmd = cmd_val.argv[i]
202 argv0_path = self.search_path.CachedLookup(cmd)
203 if argv0_path is None:
204 e_die_status(127, 'exec: %r not found' % cmd, cmd_val.arg_locs[1])
205
206 # shift off 'exec', and remove typed args because they don't apply
207 c2 = cmd_value.Argv(cmd_val.argv[i:], cmd_val.arg_locs[i:], None, None,
208 None, None)
209
210 self.ext_prog.Exec(argv0_path, c2, environ) # NEVER RETURNS
211 # makes mypy and C++ compiler happy
212 raise AssertionError('unreachable')
213
214
215class Wait(vm._Builtin):
216 """
217 wait: wait [-n] [id ...]
218 Wait for job completion and return exit status.
219
220 Waits for each process identified by an ID, which may be a process ID or a
221 job specification, and reports its termination status. If ID is not
222 given, waits for all currently active child processes, and the return
223 status is zero. If ID is a a job specification, waits for all processes
224 in that job's pipeline.
225
226 If the -n option is supplied, waits for the next job to terminate and
227 returns its exit status.
228
229 Exit Status:
230 Returns the status of the last ID; fails if ID is invalid or an invalid
231 option is given.
232 """
233
234 def __init__(self, waiter, job_list, mem, tracer, errfmt):
235 # type: (Waiter, process.JobList, Mem, dev.Tracer, ErrorFormatter) -> None
236 self.waiter = waiter
237 self.job_list = job_list
238 self.mem = mem
239 self.tracer = tracer
240 self.errfmt = errfmt
241
242 def Run(self, cmd_val):
243 # type: (cmd_value.Argv) -> int
244 with dev.ctx_Tracer(self.tracer, 'wait', cmd_val.argv):
245 return self._Run(cmd_val)
246
247 def _Run(self, cmd_val):
248 # type: (cmd_value.Argv) -> int
249 attrs, arg_r = flag_util.ParseCmdVal('wait', cmd_val)
250 arg = arg_types.wait(attrs.attrs)
251
252 job_ids, arg_locs = arg_r.Rest2()
253
254 if arg.n:
255 # Loop until there is one fewer process running, there's nothing to wait
256 # for, or there's a signal
257 n = self.job_list.NumRunning()
258 if n == 0:
259 status = 127
260 else:
261 target = n - 1
262 status = 0
263 while self.job_list.NumRunning() > target:
264 result = self.waiter.WaitForOne()
265 if result == process.W1_OK:
266 status = self.waiter.last_status
267 elif result == process.W1_ECHILD:
268 # nothing to wait for, or interrupted
269 status = 127
270 break
271 elif result >= 0: # signal
272 status = 128 + result
273 break
274
275 return status
276
277 if len(job_ids) == 0:
278 #log('*** wait')
279
280 # BUG: If there is a STOPPED process, this will hang forever, because we
281 # don't get ECHILD. Not sure it matters since you can now Ctrl-C it.
282 # But how to fix this?
283
284 status = 0
285 while self.job_list.NumRunning() != 0:
286 result = self.waiter.WaitForOne()
287 if result == process.W1_ECHILD:
288 # nothing to wait for, or interrupted. status is 0
289 break
290 elif result >= 0: # signal
291 status = 128 + result
292 break
293
294 return status
295
296 # Get list of jobs. Then we need to check if they are ALL stopped.
297 # Returns the exit code of the last one on the COMMAND LINE, not the exit
298 # code of last one to FINISH.
299 jobs = [] # type: List[process.Job]
300 for i, job_id in enumerate(job_ids):
301 location = arg_locs[i]
302
303 job = None # type: Optional[process.Job]
304 if job_id == '' or job_id.startswith('%'):
305 job = self.job_list.GetJobWithSpec(job_id)
306
307 if job is None:
308 # Does it look like a PID?
309 try:
310 pid = int(job_id)
311 except ValueError:
312 raise error.Usage(
313 'expected PID or jobspec, got %r' % job_id, location)
314
315 job = self.job_list.ProcessFromPid(pid)
316
317 if job is None:
318 self.errfmt.Print_("%s isn't a child of this shell" % job_id,
319 blame_loc=location)
320 return 127
321
322 jobs.append(job)
323
324 status = 1 # error
325 for job in jobs:
326 wait_st = job.JobWait(self.waiter)
327 UP_wait_st = wait_st
328 with tagswitch(wait_st) as case:
329 if case(wait_status_e.Proc):
330 wait_st = cast(wait_status.Proc, UP_wait_st)
331 status = wait_st.code
332
333 elif case(wait_status_e.Pipeline):
334 wait_st = cast(wait_status.Pipeline, UP_wait_st)
335 # TODO: handle PIPESTATUS? Is this right?
336 status = wait_st.codes[-1]
337
338 elif case(wait_status_e.Cancelled):
339 wait_st = cast(wait_status.Cancelled, UP_wait_st)
340 status = 128 + wait_st.sig_num
341
342 else:
343 raise AssertionError()
344
345 return status
346
347
348class Umask(vm._Builtin):
349
350 def __init__(self):
351 # type: () -> None
352 """Dummy constructor for mycpp."""
353 pass
354
355 def Run(self, cmd_val):
356 # type: (cmd_value.Argv) -> int
357
358 argv = cmd_val.argv[1:]
359 if len(argv) == 0:
360 # umask() has a dumb API: you can't get it without modifying it first!
361 # NOTE: dash disables interrupts around the two umask() calls, but that
362 # shouldn't be a concern for us. Signal handlers won't call umask().
363 mask = posix.umask(0)
364 posix.umask(mask) #
365 print('0%03o' % mask) # octal format
366 return 0
367
368 if len(argv) == 1:
369 a = argv[0]
370 try:
371 new_mask = int(a, 8)
372 except ValueError:
373 # NOTE: This also happens when we have '8' or '9' in the input.
374 print_stderr(
375 "osh warning: umask with symbolic input isn't implemented")
376 return 1
377
378 posix.umask(new_mask)
379 return 0
380
381 e_usage('umask: unexpected arguments', loc.Missing)