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

598 lines, 368 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
9import resource
10from resource import (RLIM_INFINITY, RLIMIT_CORE, RLIMIT_CPU, RLIMIT_DATA,
11 RLIMIT_FSIZE, RLIMIT_NOFILE, RLIMIT_STACK, RLIMIT_AS)
12from signal import SIGCONT
13
14from _devbuild.gen import arg_types
15from _devbuild.gen.syntax_asdl import loc
16from _devbuild.gen.runtime_asdl import (cmd_value, job_state_e, wait_status,
17 wait_status_e)
18from core import dev
19from core import error
20from core.error import e_usage, e_die_status
21from core import process # W1_OK, W1_ECHILD
22from core import pyos
23from core import pyutil
24from core import vm
25from frontend import flag_util
26from frontend import typed_args
27from mycpp import mops
28from mycpp import mylib
29from mycpp.mylib import log, tagswitch, print_stderr
30
31import posix_ as posix
32
33from typing import TYPE_CHECKING, List, Tuple, Optional, cast
34if TYPE_CHECKING:
35 from core.process import Waiter, ExternalProgram, FdState
36 from core.state import Mem, SearchPath
37 from display import ui
38
39
40class Jobs(vm._Builtin):
41 """List jobs."""
42
43 def __init__(self, job_list):
44 # type: (process.JobList) -> None
45 self.job_list = job_list
46
47 def Run(self, cmd_val):
48 # type: (cmd_value.Argv) -> int
49
50 attrs, arg_r = flag_util.ParseCmdVal('jobs', cmd_val)
51 arg = arg_types.jobs(attrs.attrs)
52
53 if arg.l:
54 style = process.STYLE_LONG
55 elif arg.p:
56 style = process.STYLE_PID_ONLY
57 else:
58 style = process.STYLE_DEFAULT
59
60 self.job_list.DisplayJobs(style)
61
62 if arg.debug:
63 self.job_list.DebugPrint()
64
65 return 0
66
67
68class Fg(vm._Builtin):
69 """Put a job in the foreground."""
70
71 def __init__(self, job_control, job_list, waiter):
72 # type: (process.JobControl, process.JobList, Waiter) -> None
73 self.job_control = job_control
74 self.job_list = job_list
75 self.waiter = waiter
76
77 def Run(self, cmd_val):
78 # type: (cmd_value.Argv) -> int
79
80 job_spec = '' # get current job by default
81 if len(cmd_val.argv) > 1:
82 job_spec = cmd_val.argv[1]
83
84 job = self.job_list.GetJobWithSpec(job_spec)
85 if job is None:
86 log('No job to put in the foreground')
87 return 1
88
89 pgid = job.ProcessGroupId()
90 assert pgid != process.INVALID_PGID, \
91 'Processes put in the background should have a PGID'
92
93 # TODO: Print job ID rather than the PID
94 log('Continue PID %d', pgid)
95 # Put the job's process group back into the foreground. GiveTerminal() must
96 # be called before sending SIGCONT or else the process might immediately get
97 # suspsended again if it tries to read/write on the terminal.
98 self.job_control.MaybeGiveTerminal(pgid)
99 job.SetForeground()
100 # needed for Wait() loop to work
101 job.state = job_state_e.Running
102 posix.killpg(pgid, SIGCONT)
103
104 status = -1
105 wait_st = job.JobWait(self.waiter)
106 UP_wait_st = wait_st
107 with tagswitch(wait_st) as case:
108 if case(wait_status_e.Proc):
109 wait_st = cast(wait_status.Proc, UP_wait_st)
110 status = wait_st.code
111
112 elif case(wait_status_e.Pipeline):
113 wait_st = cast(wait_status.Pipeline, UP_wait_st)
114 # TODO: handle PIPESTATUS? Is this right?
115 status = wait_st.codes[-1]
116
117 elif case(wait_status_e.Cancelled):
118 wait_st = cast(wait_status.Cancelled, UP_wait_st)
119 status = 128 + wait_st.sig_num
120
121 else:
122 raise AssertionError()
123
124 return status
125
126
127class Bg(vm._Builtin):
128 """Put a job in the background."""
129
130 def __init__(self, job_list):
131 # type: (process.JobList) -> None
132 self.job_list = job_list
133
134 def Run(self, cmd_val):
135 # type: (cmd_value.Argv) -> int
136
137 # How does this differ from 'fg'? It doesn't wait and it sets controlling
138 # terminal?
139
140 raise error.Usage("isn't implemented", loc.Missing)
141
142
143class Fork(vm._Builtin):
144
145 def __init__(self, shell_ex):
146 # type: (vm._Executor) -> None
147 self.shell_ex = shell_ex
148
149 def Run(self, cmd_val):
150 # type: (cmd_value.Argv) -> int
151 _, arg_r = flag_util.ParseCmdVal('fork',
152 cmd_val,
153 accept_typed_args=True)
154
155 arg, location = arg_r.Peek2()
156 if arg is not None:
157 e_usage('got unexpected argument %r' % arg, location)
158
159 cmd = typed_args.OptionalBlock(cmd_val)
160 if cmd is None:
161 e_usage('expected a block', loc.Missing)
162
163 return self.shell_ex.RunBackgroundJob(cmd)
164
165
166class ForkWait(vm._Builtin):
167
168 def __init__(self, shell_ex):
169 # type: (vm._Executor) -> None
170 self.shell_ex = shell_ex
171
172 def Run(self, cmd_val):
173 # type: (cmd_value.Argv) -> int
174 _, arg_r = flag_util.ParseCmdVal('forkwait',
175 cmd_val,
176 accept_typed_args=True)
177 arg, location = arg_r.Peek2()
178 if arg is not None:
179 e_usage('got unexpected argument %r' % arg, location)
180
181 cmd = typed_args.OptionalBlock(cmd_val)
182 if cmd is None:
183 e_usage('expected a block', loc.Missing)
184
185 return self.shell_ex.RunSubshell(cmd)
186
187
188class Exec(vm._Builtin):
189
190 def __init__(self, mem, ext_prog, fd_state, search_path, errfmt):
191 # type: (Mem, ExternalProgram, FdState, SearchPath, ui.ErrorFormatter) -> None
192 self.mem = mem
193 self.ext_prog = ext_prog
194 self.fd_state = fd_state
195 self.search_path = search_path
196 self.errfmt = errfmt
197
198 def Run(self, cmd_val):
199 # type: (cmd_value.Argv) -> int
200 _, arg_r = flag_util.ParseCmdVal('exec', cmd_val)
201
202 # Apply redirects in this shell. # NOTE: Redirects were processed earlier.
203 if arg_r.AtEnd():
204 self.fd_state.MakePermanent()
205 return 0
206
207 environ = self.mem.GetExported()
208 i = arg_r.i
209 cmd = cmd_val.argv[i]
210 argv0_path = self.search_path.CachedLookup(cmd)
211 if argv0_path is None:
212 e_die_status(127, 'exec: %r not found' % cmd, cmd_val.arg_locs[1])
213
214 # shift off 'exec', and remove typed args because they don't apply
215 c2 = cmd_value.Argv(cmd_val.argv[i:], cmd_val.arg_locs[i:],
216 cmd_val.is_last_cmd, None, None, None, None)
217
218 self.ext_prog.Exec(argv0_path, c2, environ) # NEVER RETURNS
219 # makes mypy and C++ compiler happy
220 raise AssertionError('unreachable')
221
222
223class Wait(vm._Builtin):
224 """
225 wait: wait [-n] [id ...]
226 Wait for job completion and return exit status.
227
228 Waits for each process identified by an ID, which may be a process ID or a
229 job specification, and reports its termination status. If ID is not
230 given, waits for all currently active child processes, and the return
231 status is zero. If ID is a a job specification, waits for all processes
232 in that job's pipeline.
233
234 If the -n option is supplied, waits for the next job to terminate and
235 returns its exit status.
236
237 Exit Status:
238 Returns the status of the last ID; fails if ID is invalid or an invalid
239 option is given.
240 """
241
242 def __init__(self, waiter, job_list, mem, tracer, errfmt):
243 # type: (Waiter, process.JobList, Mem, dev.Tracer, ui.ErrorFormatter) -> None
244 self.waiter = waiter
245 self.job_list = job_list
246 self.mem = mem
247 self.tracer = tracer
248 self.errfmt = errfmt
249
250 def Run(self, cmd_val):
251 # type: (cmd_value.Argv) -> int
252 with dev.ctx_Tracer(self.tracer, 'wait', cmd_val.argv):
253 return self._Run(cmd_val)
254
255 def _Run(self, cmd_val):
256 # type: (cmd_value.Argv) -> int
257 attrs, arg_r = flag_util.ParseCmdVal('wait', cmd_val)
258 arg = arg_types.wait(attrs.attrs)
259
260 job_ids, arg_locs = arg_r.Rest2()
261
262 if arg.n:
263 # Loop until there is one fewer process running, there's nothing to wait
264 # for, or there's a signal
265 n = self.job_list.NumRunning()
266 if n == 0:
267 status = 127
268 else:
269 target = n - 1
270 status = 0
271 while self.job_list.NumRunning() > target:
272 result = self.waiter.WaitForOne()
273 if result == process.W1_OK:
274 status = self.waiter.last_status
275 elif result == process.W1_ECHILD:
276 # nothing to wait for, or interrupted
277 status = 127
278 break
279 elif result >= 0: # signal
280 status = 128 + result
281 break
282
283 return status
284
285 if len(job_ids) == 0:
286 #log('*** wait')
287
288 # BUG: If there is a STOPPED process, this will hang forever, because we
289 # don't get ECHILD. Not sure it matters since you can now Ctrl-C it.
290 # But how to fix this?
291
292 status = 0
293 while self.job_list.NumRunning() != 0:
294 result = self.waiter.WaitForOne()
295 if result == process.W1_ECHILD:
296 # nothing to wait for, or interrupted. status is 0
297 break
298 elif result >= 0: # signal
299 status = 128 + result
300 break
301
302 return status
303
304 # Get list of jobs. Then we need to check if they are ALL stopped.
305 # Returns the exit code of the last one on the COMMAND LINE, not the exit
306 # code of last one to FINISH.
307 jobs = [] # type: List[process.Job]
308 for i, job_id in enumerate(job_ids):
309 location = arg_locs[i]
310
311 job = None # type: Optional[process.Job]
312 if job_id == '' or job_id.startswith('%'):
313 job = self.job_list.GetJobWithSpec(job_id)
314
315 if job is None:
316 # Does it look like a PID?
317 try:
318 pid = int(job_id)
319 except ValueError:
320 raise error.Usage(
321 'expected PID or jobspec, got %r' % job_id, location)
322
323 job = self.job_list.ProcessFromPid(pid)
324
325 if job is None:
326 self.errfmt.Print_("%s isn't a child of this shell" % job_id,
327 blame_loc=location)
328 return 127
329
330 jobs.append(job)
331
332 status = 1 # error
333 for job in jobs:
334 wait_st = job.JobWait(self.waiter)
335 UP_wait_st = wait_st
336 with tagswitch(wait_st) as case:
337 if case(wait_status_e.Proc):
338 wait_st = cast(wait_status.Proc, UP_wait_st)
339 status = wait_st.code
340
341 elif case(wait_status_e.Pipeline):
342 wait_st = cast(wait_status.Pipeline, UP_wait_st)
343 # TODO: handle PIPESTATUS? Is this right?
344 status = wait_st.codes[-1]
345
346 elif case(wait_status_e.Cancelled):
347 wait_st = cast(wait_status.Cancelled, UP_wait_st)
348 status = 128 + wait_st.sig_num
349
350 else:
351 raise AssertionError()
352
353 return status
354
355
356class Umask(vm._Builtin):
357
358 def __init__(self):
359 # type: () -> None
360 """Dummy constructor for mycpp."""
361 pass
362
363 def Run(self, cmd_val):
364 # type: (cmd_value.Argv) -> int
365
366 argv = cmd_val.argv[1:]
367 if len(argv) == 0:
368 # umask() has a dumb API: you can't get it without modifying it first!
369 # NOTE: dash disables interrupts around the two umask() calls, but that
370 # shouldn't be a concern for us. Signal handlers won't call umask().
371 mask = posix.umask(0)
372 posix.umask(mask) #
373 print('0%03o' % mask) # octal format
374 return 0
375
376 if len(argv) == 1:
377 a = argv[0]
378 try:
379 new_mask = int(a, 8)
380 except ValueError:
381 # NOTE: This also happens when we have '8' or '9' in the input.
382 print_stderr(
383 "osh warning: umask with symbolic input isn't implemented")
384 return 1
385
386 posix.umask(new_mask)
387 return 0
388
389 e_usage('umask: unexpected arguments', loc.Missing)
390
391
392def _LimitString(lim, factor):
393 # type: (mops.BigInt, int) -> str
394 if mops.Equal(lim, mops.FromC(RLIM_INFINITY)):
395 return 'unlimited'
396 else:
397 i = mops.Div(lim, mops.IntWiden(factor))
398 return mops.ToStr(i)
399
400
401class Ulimit(vm._Builtin):
402
403 def __init__(self):
404 # type: () -> None
405 """Dummy constructor for mycpp."""
406
407 self._table = None # type: List[Tuple[str, int, int, str]]
408
409 def _Table(self):
410 # type: () -> List[Tuple[str, int, int, str]]
411
412 # POSIX 2018
413 #
414 # https://pubs.opengroup.org/onlinepubs/9699919799/functions/getrlimit.html
415 if self._table is None:
416 # This table matches _ULIMIT_RESOURCES in frontend/flag_def.py
417
418 # flag, RLIMIT_X, factor, description
419 self._table = [
420 # Following POSIX and most shells except bash, -f is in
421 # blocks of 512 bytes
422 ('-c', RLIMIT_CORE, 512, 'core dump size'),
423 ('-d', RLIMIT_DATA, 1024, 'data segment size'),
424 ('-f', RLIMIT_FSIZE, 512, 'file size'),
425 ('-n', RLIMIT_NOFILE, 1, 'file descriptors'),
426 ('-s', RLIMIT_STACK, 1024, 'stack size'),
427 ('-t', RLIMIT_CPU, 1, 'CPU seconds'),
428 ('-v', RLIMIT_AS, 1024, 'address space size'),
429 ]
430
431 return self._table
432
433 def _FindFactor(self, what):
434 # type: (int) -> int
435 for _, w, factor, _ in self._Table():
436 if w == what:
437 return factor
438 raise AssertionError()
439
440 def Run(self, cmd_val):
441 # type: (cmd_value.Argv) -> int
442
443 attrs, arg_r = flag_util.ParseCmdVal('ulimit', cmd_val)
444 arg = arg_types.ulimit(attrs.attrs)
445
446 what = 0
447 num_what_flags = 0
448
449 if arg.c:
450 what = RLIMIT_CORE
451 num_what_flags += 1
452
453 if arg.d:
454 what = RLIMIT_DATA
455 num_what_flags += 1
456
457 if arg.f:
458 what = RLIMIT_FSIZE
459 num_what_flags += 1
460
461 if arg.n:
462 what = RLIMIT_NOFILE
463 num_what_flags += 1
464
465 if arg.s:
466 what = RLIMIT_STACK
467 num_what_flags += 1
468
469 if arg.t:
470 what = RLIMIT_CPU
471 num_what_flags += 1
472
473 if arg.v:
474 what = RLIMIT_AS
475 num_what_flags += 1
476
477 if num_what_flags > 1:
478 raise error.Usage(
479 'can only handle one resource at a time; got too many flags',
480 cmd_val.arg_locs[0])
481
482 # Print all
483 show_all = arg.a or arg.all
484 if show_all:
485 if num_what_flags > 0:
486 raise error.Usage("doesn't accept resource flags with -a",
487 cmd_val.arg_locs[0])
488
489 extra, extra_loc = arg_r.Peek2()
490 if extra is not None:
491 raise error.Usage('got extra arg with -a', extra_loc)
492
493 # Worst case 20 == len(str(2**64))
494 fmt = '%5s %15s %15s %7s %s'
495 print(fmt % ('FLAG', 'SOFT', 'HARD', 'FACTOR', 'DESC'))
496 for flag, what, factor, desc in self._Table():
497 soft, hard = pyos.GetRLimit(what)
498
499 soft2 = _LimitString(soft, factor)
500 hard2 = _LimitString(hard, factor)
501 print(fmt % (flag, soft2, hard2, str(factor), desc))
502
503 return 0
504
505 if num_what_flags == 0:
506 what = RLIMIT_FSIZE # -f is the default
507
508 s, s_loc = arg_r.Peek2()
509
510 if s is None:
511 factor = self._FindFactor(what)
512 soft, hard = pyos.GetRLimit(what)
513 if arg.H:
514 print(_LimitString(hard, factor))
515 else:
516 print(_LimitString(soft, factor))
517 return 0
518
519 # Set the given resource
520 if s == 'unlimited':
521 # In C, RLIM_INFINITY is rlim_t
522 limit = mops.FromC(RLIM_INFINITY)
523 else:
524 try:
525 big_int = mops.FromStr(s)
526 except ValueError as e:
527 raise error.Usage(
528 "expected a number or 'unlimited', got %r" % s, s_loc)
529
530 if mops.Greater(mops.IntWiden(0), big_int):
531 raise error.Usage(
532 "doesn't accept negative numbers, got %r" % s, s_loc)
533
534 factor = self._FindFactor(what)
535
536 fac = mops.IntWiden(factor)
537 limit = mops.Mul(big_int, fac)
538
539 # Overflow check like bash does
540 # TODO: This should be replaced with a different overflow check
541 # when we have arbitrary precision integers
542 if not mops.Equal(mops.Div(limit, fac), big_int):
543 #log('div %s', mops.ToStr(mops.Div(limit, fac)))
544 raise error.Usage(
545 'detected integer overflow: %s' % mops.ToStr(big_int),
546 s_loc)
547
548 arg_r.Next()
549 extra2, extra_loc2 = arg_r.Peek2()
550 if extra2 is not None:
551 raise error.Usage('got extra arg', extra_loc2)
552
553 # Now set the resource
554 soft, hard = pyos.GetRLimit(what)
555
556 # For error message
557 old_soft = soft
558 old_hard = hard
559
560 # Bash behavior: manipulate both, unless a flag is parsed. This
561 # differs from zsh!
562 if not arg.S and not arg.H:
563 soft = limit
564 hard = limit
565 if arg.S:
566 soft = limit
567 if arg.H:
568 hard = limit
569
570 if mylib.PYTHON:
571 try:
572 pyos.SetRLimit(what, soft, hard)
573 except OverflowError: # only happens in CPython
574 raise error.Usage('detected overflow', s_loc)
575 except (ValueError, resource.error) as e:
576 # Annoying: Python binding changes IOError -> ValueError
577
578 print_stderr('ulimit error: %s' % e)
579
580 # Extra info we could expose in C++ too
581 print_stderr('soft=%s hard=%s -> soft=%s hard=%s' % (
582 _LimitString(old_soft, factor),
583 _LimitString(old_hard, factor),
584 _LimitString(soft, factor),
585 _LimitString(hard, factor),
586 ))
587 return 1
588 else:
589 try:
590 pyos.SetRLimit(what, soft, hard)
591 except (IOError, OSError) as e:
592 print_stderr('ulimit error: %s' % pyutil.strerror(e))
593 return 1
594
595 return 0
596
597
598# vim: sw=4