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

617 lines, 387 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__(
189 self,
190 mem, # type: state.Mem
191 ext_prog, # type: ExternalProgram
192 fd_state, # type: FdState
193 search_path, # type: executor.SearchPath
194 errfmt, # type: ui.ErrorFormatter
195 ):
196 # type: (...) -> None
197 self.mem = mem
198 self.ext_prog = ext_prog
199 self.fd_state = fd_state
200 self.search_path = search_path
201 self.errfmt = errfmt
202
203 def Run(self, cmd_val):
204 # type: (cmd_value.Argv) -> int
205 _, arg_r = flag_util.ParseCmdVal('exec', cmd_val)
206
207 # Apply redirects in this shell. # NOTE: Redirects were processed earlier.
208 if arg_r.AtEnd():
209 self.fd_state.MakePermanent()
210 return 0
211
212 environ = self.mem.GetEnv()
213 if 0:
214 log('E %r', environ)
215 log('E %r', environ)
216 log('ZZ %r', environ.get('ZZ'))
217 i = arg_r.i
218 cmd = cmd_val.argv[i]
219 argv0_path = self.search_path.CachedLookup(cmd)
220 if argv0_path is None:
221 e_die_status(127, 'exec: %r not found' % cmd, cmd_val.arg_locs[1])
222
223 # shift off 'exec', and remove typed args because they don't apply
224 c2 = cmd_value.Argv(cmd_val.argv[i:], cmd_val.arg_locs[i:],
225 cmd_val.is_last_cmd, cmd_val.self_obj, None)
226
227 self.ext_prog.Exec(argv0_path, c2, environ) # NEVER RETURNS
228 # makes mypy and C++ compiler happy
229 raise AssertionError('unreachable')
230
231
232class Wait(vm._Builtin):
233 """
234 wait: wait [-n] [id ...]
235 Wait for job completion and return exit status.
236
237 Waits for each process identified by an ID, which may be a process ID or a
238 job specification, and reports its termination status. If ID is not
239 given, waits for all currently active child processes, and the return
240 status is zero. If ID is a a job specification, waits for all processes
241 in that job's pipeline.
242
243 If the -n option is supplied, waits for the next job to terminate and
244 returns its exit status.
245
246 Exit Status:
247 Returns the status of the last ID; fails if ID is invalid or an invalid
248 option is given.
249 """
250
251 def __init__(
252 self,
253 waiter, # type: Waiter
254 job_list, #type: process.JobList
255 mem, # type: state.Mem
256 tracer, # type: dev.Tracer
257 errfmt, # type: ui.ErrorFormatter
258 ):
259 # type: (...) -> None
260 self.waiter = waiter
261 self.job_list = job_list
262 self.mem = mem
263 self.tracer = tracer
264 self.errfmt = errfmt
265
266 def Run(self, cmd_val):
267 # type: (cmd_value.Argv) -> int
268 with dev.ctx_Tracer(self.tracer, 'wait', cmd_val.argv):
269 return self._Run(cmd_val)
270
271 def _Run(self, cmd_val):
272 # type: (cmd_value.Argv) -> int
273 attrs, arg_r = flag_util.ParseCmdVal('wait', cmd_val)
274 arg = arg_types.wait(attrs.attrs)
275
276 job_ids, arg_locs = arg_r.Rest2()
277
278 if arg.n:
279 # Loop until there is one fewer process running, there's nothing to wait
280 # for, or there's a signal
281 n = self.job_list.NumRunning()
282 if n == 0:
283 status = 127
284 else:
285 target = n - 1
286 status = 0
287 while self.job_list.NumRunning() > target:
288 result = self.waiter.WaitForOne()
289 if result == process.W1_OK:
290 status = self.waiter.last_status
291 elif result == process.W1_ECHILD:
292 # nothing to wait for, or interrupted
293 status = 127
294 break
295 elif result >= 0: # signal
296 status = 128 + result
297 break
298
299 return status
300
301 if len(job_ids) == 0:
302 #log('*** wait')
303
304 # BUG: If there is a STOPPED process, this will hang forever, because we
305 # don't get ECHILD. Not sure it matters since you can now Ctrl-C it.
306 # But how to fix this?
307
308 status = 0
309 while self.job_list.NumRunning() != 0:
310 result = self.waiter.WaitForOne()
311 if result == process.W1_ECHILD:
312 # nothing to wait for, or interrupted. status is 0
313 break
314 elif result >= 0: # signal
315 status = 128 + result
316 break
317
318 return status
319
320 # Get list of jobs. Then we need to check if they are ALL stopped.
321 # Returns the exit code of the last one on the COMMAND LINE, not the exit
322 # code of last one to FINISH.
323 jobs = [] # type: List[process.Job]
324 for i, job_id in enumerate(job_ids):
325 location = arg_locs[i]
326
327 job = None # type: Optional[process.Job]
328 if job_id == '' or job_id.startswith('%'):
329 job = self.job_list.GetJobWithSpec(job_id)
330
331 if job is None:
332 # Does it look like a PID?
333 try:
334 pid = int(job_id)
335 except ValueError:
336 raise error.Usage(
337 'expected PID or jobspec, got %r' % job_id, location)
338
339 job = self.job_list.ProcessFromPid(pid)
340
341 if job is None:
342 self.errfmt.Print_("%s isn't a child of this shell" % job_id,
343 blame_loc=location)
344 return 127
345
346 jobs.append(job)
347
348 status = 1 # error
349 for job in jobs:
350 wait_st = job.JobWait(self.waiter)
351 UP_wait_st = wait_st
352 with tagswitch(wait_st) as case:
353 if case(wait_status_e.Proc):
354 wait_st = cast(wait_status.Proc, UP_wait_st)
355 status = wait_st.code
356
357 elif case(wait_status_e.Pipeline):
358 wait_st = cast(wait_status.Pipeline, UP_wait_st)
359 # TODO: handle PIPESTATUS? Is this right?
360 status = wait_st.codes[-1]
361
362 elif case(wait_status_e.Cancelled):
363 wait_st = cast(wait_status.Cancelled, UP_wait_st)
364 status = 128 + wait_st.sig_num
365
366 else:
367 raise AssertionError()
368
369 return status
370
371
372class Umask(vm._Builtin):
373
374 def __init__(self):
375 # type: () -> None
376 """Dummy constructor for mycpp."""
377 pass
378
379 def Run(self, cmd_val):
380 # type: (cmd_value.Argv) -> int
381
382 argv = cmd_val.argv[1:]
383 if len(argv) == 0:
384 # umask() has a dumb API: you can't get it without modifying it first!
385 # NOTE: dash disables interrupts around the two umask() calls, but that
386 # shouldn't be a concern for us. Signal handlers won't call umask().
387 mask = posix.umask(0)
388 posix.umask(mask) #
389 print('0%03o' % mask) # octal format
390 return 0
391
392 if len(argv) == 1:
393 a = argv[0]
394 try:
395 new_mask = int(a, 8)
396 except ValueError:
397 # NOTE: This also happens when we have '8' or '9' in the input.
398 print_stderr(
399 "oils warning: umask with symbolic input isn't implemented"
400 )
401 return 1
402
403 posix.umask(new_mask)
404 return 0
405
406 e_usage('umask: unexpected arguments', loc.Missing)
407
408
409def _LimitString(lim, factor):
410 # type: (mops.BigInt, int) -> str
411 if mops.Equal(lim, mops.FromC(RLIM_INFINITY)):
412 return 'unlimited'
413 else:
414 i = mops.Div(lim, mops.IntWiden(factor))
415 return mops.ToStr(i)
416
417
418class Ulimit(vm._Builtin):
419
420 def __init__(self):
421 # type: () -> None
422 """Dummy constructor for mycpp."""
423
424 self._table = None # type: List[Tuple[str, int, int, str]]
425
426 def _Table(self):
427 # type: () -> List[Tuple[str, int, int, str]]
428
429 # POSIX 2018
430 #
431 # https://pubs.opengroup.org/onlinepubs/9699919799/functions/getrlimit.html
432 if self._table is None:
433 # This table matches _ULIMIT_RESOURCES in frontend/flag_def.py
434
435 # flag, RLIMIT_X, factor, description
436 self._table = [
437 # Following POSIX and most shells except bash, -f is in
438 # blocks of 512 bytes
439 ('-c', RLIMIT_CORE, 512, 'core dump size'),
440 ('-d', RLIMIT_DATA, 1024, 'data segment size'),
441 ('-f', RLIMIT_FSIZE, 512, 'file size'),
442 ('-n', RLIMIT_NOFILE, 1, 'file descriptors'),
443 ('-s', RLIMIT_STACK, 1024, 'stack size'),
444 ('-t', RLIMIT_CPU, 1, 'CPU seconds'),
445 ('-v', RLIMIT_AS, 1024, 'address space size'),
446 ]
447
448 return self._table
449
450 def _FindFactor(self, what):
451 # type: (int) -> int
452 for _, w, factor, _ in self._Table():
453 if w == what:
454 return factor
455 raise AssertionError()
456
457 def Run(self, cmd_val):
458 # type: (cmd_value.Argv) -> int
459
460 attrs, arg_r = flag_util.ParseCmdVal('ulimit', cmd_val)
461 arg = arg_types.ulimit(attrs.attrs)
462
463 what = 0
464 num_what_flags = 0
465
466 if arg.c:
467 what = RLIMIT_CORE
468 num_what_flags += 1
469
470 if arg.d:
471 what = RLIMIT_DATA
472 num_what_flags += 1
473
474 if arg.f:
475 what = RLIMIT_FSIZE
476 num_what_flags += 1
477
478 if arg.n:
479 what = RLIMIT_NOFILE
480 num_what_flags += 1
481
482 if arg.s:
483 what = RLIMIT_STACK
484 num_what_flags += 1
485
486 if arg.t:
487 what = RLIMIT_CPU
488 num_what_flags += 1
489
490 if arg.v:
491 what = RLIMIT_AS
492 num_what_flags += 1
493
494 if num_what_flags > 1:
495 raise error.Usage(
496 'can only handle one resource at a time; got too many flags',
497 cmd_val.arg_locs[0])
498
499 # Print all
500 show_all = arg.a or arg.all
501 if show_all:
502 if num_what_flags > 0:
503 raise error.Usage("doesn't accept resource flags with -a",
504 cmd_val.arg_locs[0])
505
506 extra, extra_loc = arg_r.Peek2()
507 if extra is not None:
508 raise error.Usage('got extra arg with -a', extra_loc)
509
510 # Worst case 20 == len(str(2**64))
511 fmt = '%5s %15s %15s %7s %s'
512 print(fmt % ('FLAG', 'SOFT', 'HARD', 'FACTOR', 'DESC'))
513 for flag, what, factor, desc in self._Table():
514 soft, hard = pyos.GetRLimit(what)
515
516 soft2 = _LimitString(soft, factor)
517 hard2 = _LimitString(hard, factor)
518 print(fmt % (flag, soft2, hard2, str(factor), desc))
519
520 return 0
521
522 if num_what_flags == 0:
523 what = RLIMIT_FSIZE # -f is the default
524
525 s, s_loc = arg_r.Peek2()
526
527 if s is None:
528 factor = self._FindFactor(what)
529 soft, hard = pyos.GetRLimit(what)
530 if arg.H:
531 print(_LimitString(hard, factor))
532 else:
533 print(_LimitString(soft, factor))
534 return 0
535
536 # Set the given resource
537 if s == 'unlimited':
538 # In C, RLIM_INFINITY is rlim_t
539 limit = mops.FromC(RLIM_INFINITY)
540 else:
541 if match.LooksLikeInteger(s):
542 ok, big_int = mops.FromStr2(s)
543 if not ok:
544 raise error.Usage('Integer too big: %s' % s, s_loc)
545 else:
546 raise error.Usage(
547 "expected a number or 'unlimited', got %r" % s, s_loc)
548
549 if mops.Greater(mops.IntWiden(0), big_int):
550 raise error.Usage(
551 "doesn't accept negative numbers, got %r" % s, s_loc)
552
553 factor = self._FindFactor(what)
554
555 fac = mops.IntWiden(factor)
556 limit = mops.Mul(big_int, fac)
557
558 # Overflow check like bash does
559 # TODO: This should be replaced with a different overflow check
560 # when we have arbitrary precision integers
561 if not mops.Equal(mops.Div(limit, fac), big_int):
562 #log('div %s', mops.ToStr(mops.Div(limit, fac)))
563 raise error.Usage(
564 'detected integer overflow: %s' % mops.ToStr(big_int),
565 s_loc)
566
567 arg_r.Next()
568 extra2, extra_loc2 = arg_r.Peek2()
569 if extra2 is not None:
570 raise error.Usage('got extra arg', extra_loc2)
571
572 # Now set the resource
573 soft, hard = pyos.GetRLimit(what)
574
575 # For error message
576 old_soft = soft
577 old_hard = hard
578
579 # Bash behavior: manipulate both, unless a flag is parsed. This
580 # differs from zsh!
581 if not arg.S and not arg.H:
582 soft = limit
583 hard = limit
584 if arg.S:
585 soft = limit
586 if arg.H:
587 hard = limit
588
589 if mylib.PYTHON:
590 try:
591 pyos.SetRLimit(what, soft, hard)
592 except OverflowError: # only happens in CPython
593 raise error.Usage('detected overflow', s_loc)
594 except (ValueError, resource.error) as e:
595 # Annoying: Python binding changes IOError -> ValueError
596
597 print_stderr('oils: ulimit error: %s' % e)
598
599 # Extra info we could expose in C++ too
600 print_stderr('soft=%s hard=%s -> soft=%s hard=%s' % (
601 _LimitString(old_soft, factor),
602 _LimitString(old_hard, factor),
603 _LimitString(soft, factor),
604 _LimitString(hard, factor),
605 ))
606 return 1
607 else:
608 try:
609 pyos.SetRLimit(what, soft, hard)
610 except (IOError, OSError) as e:
611 print_stderr('oils: ulimit error: %s' % pyutil.strerror(e))
612 return 1
613
614 return 0
615
616
617# vim: sw=4