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

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