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

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