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

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