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