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

600 lines, 369 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_ = log
40
41
42class Jobs(vm._Builtin):
43 """List jobs."""
44
45 def __init__(self, job_list):
46 # type: (process.JobList) -> None
47 self.job_list = job_list
48
49 def Run(self, cmd_val):
50 # type: (cmd_value.Argv) -> int
51
52 attrs, arg_r = flag_util.ParseCmdVal('jobs', cmd_val)
53 arg = arg_types.jobs(attrs.attrs)
54
55 if arg.l:
56 style = process.STYLE_LONG
57 elif arg.p:
58 style = process.STYLE_PID_ONLY
59 else:
60 style = process.STYLE_DEFAULT
61
62 self.job_list.DisplayJobs(style)
63
64 if arg.debug:
65 self.job_list.DebugPrint()
66
67 return 0
68
69
70class Fg(vm._Builtin):
71 """Put a job in the foreground."""
72
73 def __init__(self, job_control, job_list, waiter):
74 # type: (process.JobControl, process.JobList, Waiter) -> None
75 self.job_control = job_control
76 self.job_list = job_list
77 self.waiter = waiter
78
79 def Run(self, cmd_val):
80 # type: (cmd_value.Argv) -> int
81
82 job_spec = '' # get current job by default
83 if len(cmd_val.argv) > 1:
84 job_spec = cmd_val.argv[1]
85
86 job = self.job_list.GetJobWithSpec(job_spec)
87 if job is None:
88 print_stderr('fg: No job to put in the foreground')
89 return 1
90
91 pgid = job.ProcessGroupId()
92 assert pgid != process.INVALID_PGID, \
93 'Processes put in the background should have a PGID'
94
95 # TODO: Print job ID rather than the PID
96 print_stderr('fg: PID %d Continued' % pgid)
97 # Put the job's process group back into the foreground. GiveTerminal() must
98 # be called before sending SIGCONT or else the process might immediately get
99 # suspsended again if it tries to read/write on the terminal.
100 self.job_control.MaybeGiveTerminal(pgid)
101 job.SetForeground()
102 # needed for Wait() loop to work
103 job.state = job_state_e.Running
104 posix.killpg(pgid, SIGCONT)
105
106 status = -1
107 wait_st = job.JobWait(self.waiter)
108 UP_wait_st = wait_st
109 with tagswitch(wait_st) as case:
110 if case(wait_status_e.Proc):
111 wait_st = cast(wait_status.Proc, UP_wait_st)
112 status = wait_st.code
113
114 elif case(wait_status_e.Pipeline):
115 wait_st = cast(wait_status.Pipeline, UP_wait_st)
116 # TODO: handle PIPESTATUS? Is this right?
117 status = wait_st.codes[-1]
118
119 elif case(wait_status_e.Cancelled):
120 wait_st = cast(wait_status.Cancelled, UP_wait_st)
121 status = 128 + wait_st.sig_num
122
123 else:
124 raise AssertionError()
125
126 return status
127
128
129class Bg(vm._Builtin):
130 """Put a job in the background."""
131
132 def __init__(self, job_list):
133 # type: (process.JobList) -> None
134 self.job_list = job_list
135
136 def Run(self, cmd_val):
137 # type: (cmd_value.Argv) -> int
138
139 # How does this differ from 'fg'? It doesn't wait and it sets controlling
140 # terminal?
141
142 raise error.Usage("isn't implemented", loc.Missing)
143
144
145class Fork(vm._Builtin):
146
147 def __init__(self, shell_ex):
148 # type: (vm._Executor) -> None
149 self.shell_ex = shell_ex
150
151 def Run(self, cmd_val):
152 # type: (cmd_value.Argv) -> int
153 _, arg_r = flag_util.ParseCmdVal('fork',
154 cmd_val,
155 accept_typed_args=True)
156
157 arg, location = arg_r.Peek2()
158 if arg is not None:
159 e_usage('got unexpected argument %r' % arg, location)
160
161 cmd = typed_args.OptionalBlock(cmd_val)
162 if cmd is None:
163 e_usage('expected a block', loc.Missing)
164
165 return self.shell_ex.RunBackgroundJob(cmd)
166
167
168class ForkWait(vm._Builtin):
169
170 def __init__(self, shell_ex):
171 # type: (vm._Executor) -> None
172 self.shell_ex = shell_ex
173
174 def Run(self, cmd_val):
175 # type: (cmd_value.Argv) -> int
176 _, arg_r = flag_util.ParseCmdVal('forkwait',
177 cmd_val,
178 accept_typed_args=True)
179 arg, location = arg_r.Peek2()
180 if arg is not None:
181 e_usage('got unexpected argument %r' % arg, location)
182
183 cmd = typed_args.OptionalBlock(cmd_val)
184 if cmd is None:
185 e_usage('expected a block', loc.Missing)
186
187 return self.shell_ex.RunSubshell(cmd)
188
189
190class Exec(vm._Builtin):
191
192 def __init__(self, mem, ext_prog, fd_state, search_path, errfmt):
193 # type: (Mem, ExternalProgram, FdState, SearchPath, ui.ErrorFormatter) -> None
194 self.mem = mem
195 self.ext_prog = ext_prog
196 self.fd_state = fd_state
197 self.search_path = search_path
198 self.errfmt = errfmt
199
200 def Run(self, cmd_val):
201 # type: (cmd_value.Argv) -> int
202 _, arg_r = flag_util.ParseCmdVal('exec', cmd_val)
203
204 # Apply redirects in this shell. # NOTE: Redirects were processed earlier.
205 if arg_r.AtEnd():
206 self.fd_state.MakePermanent()
207 return 0
208
209 environ = self.mem.GetExported()
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, 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, 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 return 1
387
388 posix.umask(new_mask)
389 return 0
390
391 e_usage('umask: unexpected arguments', loc.Missing)
392
393
394def _LimitString(lim, factor):
395 # type: (mops.BigInt, int) -> str
396 if mops.Equal(lim, mops.FromC(RLIM_INFINITY)):
397 return 'unlimited'
398 else:
399 i = mops.Div(lim, mops.IntWiden(factor))
400 return mops.ToStr(i)
401
402
403class Ulimit(vm._Builtin):
404
405 def __init__(self):
406 # type: () -> None
407 """Dummy constructor for mycpp."""
408
409 self._table = None # type: List[Tuple[str, int, int, str]]
410
411 def _Table(self):
412 # type: () -> List[Tuple[str, int, int, str]]
413
414 # POSIX 2018
415 #
416 # https://pubs.opengroup.org/onlinepubs/9699919799/functions/getrlimit.html
417 if self._table is None:
418 # This table matches _ULIMIT_RESOURCES in frontend/flag_def.py
419
420 # flag, RLIMIT_X, factor, description
421 self._table = [
422 # Following POSIX and most shells except bash, -f is in
423 # blocks of 512 bytes
424 ('-c', RLIMIT_CORE, 512, 'core dump size'),
425 ('-d', RLIMIT_DATA, 1024, 'data segment size'),
426 ('-f', RLIMIT_FSIZE, 512, 'file size'),
427 ('-n', RLIMIT_NOFILE, 1, 'file descriptors'),
428 ('-s', RLIMIT_STACK, 1024, 'stack size'),
429 ('-t', RLIMIT_CPU, 1, 'CPU seconds'),
430 ('-v', RLIMIT_AS, 1024, 'address space size'),
431 ]
432
433 return self._table
434
435 def _FindFactor(self, what):
436 # type: (int) -> int
437 for _, w, factor, _ in self._Table():
438 if w == what:
439 return factor
440 raise AssertionError()
441
442 def Run(self, cmd_val):
443 # type: (cmd_value.Argv) -> int
444
445 attrs, arg_r = flag_util.ParseCmdVal('ulimit', cmd_val)
446 arg = arg_types.ulimit(attrs.attrs)
447
448 what = 0
449 num_what_flags = 0
450
451 if arg.c:
452 what = RLIMIT_CORE
453 num_what_flags += 1
454
455 if arg.d:
456 what = RLIMIT_DATA
457 num_what_flags += 1
458
459 if arg.f:
460 what = RLIMIT_FSIZE
461 num_what_flags += 1
462
463 if arg.n:
464 what = RLIMIT_NOFILE
465 num_what_flags += 1
466
467 if arg.s:
468 what = RLIMIT_STACK
469 num_what_flags += 1
470
471 if arg.t:
472 what = RLIMIT_CPU
473 num_what_flags += 1
474
475 if arg.v:
476 what = RLIMIT_AS
477 num_what_flags += 1
478
479 if num_what_flags > 1:
480 raise error.Usage(
481 'can only handle one resource at a time; got too many flags',
482 cmd_val.arg_locs[0])
483
484 # Print all
485 show_all = arg.a or arg.all
486 if show_all:
487 if num_what_flags > 0:
488 raise error.Usage("doesn't accept resource flags with -a",
489 cmd_val.arg_locs[0])
490
491 extra, extra_loc = arg_r.Peek2()
492 if extra is not None:
493 raise error.Usage('got extra arg with -a', extra_loc)
494
495 # Worst case 20 == len(str(2**64))
496 fmt = '%5s %15s %15s %7s %s'
497 print(fmt % ('FLAG', 'SOFT', 'HARD', 'FACTOR', 'DESC'))
498 for flag, what, factor, desc in self._Table():
499 soft, hard = pyos.GetRLimit(what)
500
501 soft2 = _LimitString(soft, factor)
502 hard2 = _LimitString(hard, factor)
503 print(fmt % (flag, soft2, hard2, str(factor), desc))
504
505 return 0
506
507 if num_what_flags == 0:
508 what = RLIMIT_FSIZE # -f is the default
509
510 s, s_loc = arg_r.Peek2()
511
512 if s is None:
513 factor = self._FindFactor(what)
514 soft, hard = pyos.GetRLimit(what)
515 if arg.H:
516 print(_LimitString(hard, factor))
517 else:
518 print(_LimitString(soft, factor))
519 return 0
520
521 # Set the given resource
522 if s == 'unlimited':
523 # In C, RLIM_INFINITY is rlim_t
524 limit = mops.FromC(RLIM_INFINITY)
525 else:
526 try:
527 big_int = mops.FromStr(s)
528 except ValueError as e:
529 raise error.Usage(
530 "expected a number or 'unlimited', got %r" % s, s_loc)
531
532 if mops.Greater(mops.IntWiden(0), big_int):
533 raise error.Usage(
534 "doesn't accept negative numbers, got %r" % s, s_loc)
535
536 factor = self._FindFactor(what)
537
538 fac = mops.IntWiden(factor)
539 limit = mops.Mul(big_int, fac)
540
541 # Overflow check like bash does
542 # TODO: This should be replaced with a different overflow check
543 # when we have arbitrary precision integers
544 if not mops.Equal(mops.Div(limit, fac), big_int):
545 #log('div %s', mops.ToStr(mops.Div(limit, fac)))
546 raise error.Usage(
547 'detected integer overflow: %s' % mops.ToStr(big_int),
548 s_loc)
549
550 arg_r.Next()
551 extra2, extra_loc2 = arg_r.Peek2()
552 if extra2 is not None:
553 raise error.Usage('got extra arg', extra_loc2)
554
555 # Now set the resource
556 soft, hard = pyos.GetRLimit(what)
557
558 # For error message
559 old_soft = soft
560 old_hard = hard
561
562 # Bash behavior: manipulate both, unless a flag is parsed. This
563 # differs from zsh!
564 if not arg.S and not arg.H:
565 soft = limit
566 hard = limit
567 if arg.S:
568 soft = limit
569 if arg.H:
570 hard = limit
571
572 if mylib.PYTHON:
573 try:
574 pyos.SetRLimit(what, soft, hard)
575 except OverflowError: # only happens in CPython
576 raise error.Usage('detected overflow', s_loc)
577 except (ValueError, resource.error) as e:
578 # Annoying: Python binding changes IOError -> ValueError
579
580 print_stderr('oils: ulimit error: %s' % e)
581
582 # Extra info we could expose in C++ too
583 print_stderr('soft=%s hard=%s -> soft=%s hard=%s' % (
584 _LimitString(old_soft, factor),
585 _LimitString(old_hard, factor),
586 _LimitString(soft, factor),
587 _LimitString(hard, factor),
588 ))
589 return 1
590 else:
591 try:
592 pyos.SetRLimit(what, soft, hard)
593 except (IOError, OSError) as e:
594 print_stderr('oils: ulimit error: %s' % pyutil.strerror(e))
595 return 1
596
597 return 0
598
599
600# vim: sw=4