OILS / builtin / process_osh.py View on Github | oils.pub

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