OILS / core / pyos.py View on Github | oilshell.org

448 lines, 197 significant
1"""
2pyos.py -- Wrappers for the operating system.
3
4Like py{error,util}.py, it won't be translated to C++.
5"""
6from __future__ import print_function
7
8from errno import EINTR
9import pwd
10import resource
11import signal
12import select
13import sys
14import termios # for read -n
15import time
16
17from mycpp import mops
18from mycpp import mylib
19from mycpp.mylib import log
20
21import posix_ as posix
22from posix_ import WUNTRACED
23
24from typing import Optional, Tuple, List, Dict, cast, Any, TYPE_CHECKING
25if TYPE_CHECKING:
26 from core import error
27
28_ = log
29
30EOF_SENTINEL = 256 # bigger than any byte
31NEWLINE_CH = 10 # ord('\n')
32
33
34def FlushStdout():
35 # type: () -> Optional[error.IOError_OSError]
36 """Flush CPython buffers.
37
38 Return error because we call this in a C++ destructor, and those can't
39 throw exceptions.
40 """
41 err = None # type: Optional[error.IOError_OSError]
42 try:
43 sys.stdout.flush()
44 except (IOError, OSError) as e:
45 err = e
46 return err
47
48
49def WaitPid(waitpid_options):
50 # type: (int) -> Tuple[int, int]
51 """
52 Return value:
53 pid is 0 if WNOHANG passed, and nothing has changed state
54 status: value that can be parsed with WIFEXITED() etc.
55 """
56 try:
57 # Notes:
58 # - The arg -1 makes it like wait(), which waits for any process.
59 # - WUNTRACED is necessary to get stopped jobs. What about WCONTINUED?
60 # - We don't retry on EINTR, because the 'wait' builtin should be
61 # interruptible.
62 # - waitpid_options can be WNOHANG
63 pid, status = posix.waitpid(-1, WUNTRACED | waitpid_options)
64 except OSError as e:
65 if e.errno == EINTR and gSignalSafe.PollUntrappedSigInt():
66 raise KeyboardInterrupt()
67 return -1, e.errno
68
69 return pid, status
70
71
72class ReadError(Exception):
73 """Wraps errno returned by read().
74
75 Used by 'read' and 'mapfile' builtins.
76 """
77
78 def __init__(self, err_num):
79 # type: (int) -> None
80 self.err_num = err_num
81
82
83def Read(fd, n, chunks):
84 # type: (int, int, List[str]) -> Tuple[int, int]
85 """C-style wrapper around Python's posix.read() that uses return values
86 instead of exceptions for errors.
87
88 We will implement this directly in C++ and not use exceptions at all.
89
90 It reads n bytes from the given file descriptor and appends it to chunks.
91
92 Returns:
93 (-1, errno) on failure
94 (number of bytes read, 0) on success. Where 0 bytes read indicates EOF.
95 """
96 try:
97 chunk = posix.read(fd, n)
98 except OSError as e:
99 if e.errno == EINTR and gSignalSafe.PollUntrappedSigInt():
100 raise KeyboardInterrupt()
101 return -1, e.errno
102 else:
103 length = len(chunk)
104 if length:
105 chunks.append(chunk)
106 return length, 0
107
108
109def ReadByte(fd):
110 # type: (int) -> Tuple[int, int]
111 """Low-level interface that returns values rather than raising exceptions.
112
113 Used by _ReadUntilDelim() and _ReadLineSlowly().
114
115 Returns:
116 failure: (-1, errno) on failure
117 success: (ch integer value or EOF_SENTINEL, 0)
118 """
119 try:
120 b = posix.read(fd, 1)
121 except OSError as e:
122 if e.errno == EINTR and gSignalSafe.PollUntrappedSigInt():
123 raise KeyboardInterrupt()
124 return -1, e.errno
125 else:
126 if len(b):
127 return ord(b), 0
128 else:
129 return EOF_SENTINEL, 0
130
131
132def Environ():
133 # type: () -> Dict[str, str]
134 return posix.environ
135
136
137def Chdir(dest_dir):
138 # type: (str) -> int
139 """Returns 0 for success and nonzero errno for error."""
140 try:
141 posix.chdir(dest_dir)
142 except OSError as e:
143 return e.errno
144 return 0
145
146
147def GetMyHomeDir():
148 # type: () -> Optional[str]
149 """Get the user's home directory from the /etc/pyos.
150
151 Used by $HOME initialization in osh/state.py. Tilde expansion and
152 readline initialization use mem.GetValue('HOME').
153 """
154 uid = posix.getuid()
155 try:
156 e = pwd.getpwuid(uid)
157 except KeyError:
158 return None
159
160 return e.pw_dir
161
162
163def GetHomeDir(user_name):
164 # type: (str) -> Optional[str]
165 """For ~otheruser/src.
166
167 TODO: Should this be cached?
168 """
169 # http://linux.die.net/man/3/getpwnam
170 try:
171 e = pwd.getpwnam(user_name)
172 except KeyError:
173 return None
174
175 return e.pw_dir
176
177
178class PasswdEntry(object):
179
180 def __init__(self, pw_name, uid, gid):
181 # type: (str, int, int) -> None
182 self.pw_name = pw_name
183 self.pw_uid = uid
184 self.pw_gid = gid
185
186
187def GetAllUsers():
188 # type: () -> List[PasswdEntry]
189 users = [
190 PasswdEntry(u.pw_name, u.pw_uid, u.pw_gid) for u in pwd.getpwall()
191 ]
192 return users
193
194
195def GetUserName(uid):
196 # type: (int) -> str
197 try:
198 e = pwd.getpwuid(uid)
199 except KeyError:
200 return "<ERROR: Couldn't determine user name for uid %d>" % uid
201 else:
202 return e.pw_name
203
204
205def GetRLimit(res):
206 # type: (int) -> Tuple[mops.BigInt, mops.BigInt]
207 """
208 Raises IOError
209 """
210 soft, hard = resource.getrlimit(res)
211 return (mops.IntWiden(soft), mops.IntWiden(hard))
212
213
214def SetRLimit(res, soft, hard):
215 # type: (int, mops.BigInt, mops.BigInt) -> None
216 """
217 Raises IOError
218 """
219 resource.setrlimit(res, (soft.i, hard.i))
220
221
222def Time():
223 # type: () -> Tuple[float, float, float]
224 t = time.time() # calls gettimeofday() under the hood
225 u = resource.getrusage(resource.RUSAGE_SELF)
226 return t, u.ru_utime, u.ru_stime
227
228
229def PrintTimes():
230 # type: () -> None
231 utime, stime, cutime, cstime, elapsed = posix.times()
232 print("%dm%.3fs %dm%.3fs" %
233 (utime / 60, utime % 60, stime / 60, stime % 60))
234 print("%dm%.3fs %dm%.3fs" %
235 (cutime / 60, cutime % 60, cstime / 60, cstime % 60))
236
237
238# So builtin_misc.py doesn't depend on termios, which makes C++ translation
239# easier
240TERM_ICANON = termios.ICANON
241TERM_ECHO = termios.ECHO
242
243
244def PushTermAttrs(fd, mask):
245 # type: (int, int) -> Tuple[int, Any]
246 """Returns opaque type (void* in C++) to be reused in the PopTermAttrs()"""
247 # https://docs.python.org/2/library/termios.html
248 term_attrs = termios.tcgetattr(fd)
249
250 # Flip the bits in one field, e.g. ICANON to disable canonical (buffered)
251 # mode.
252 orig_local_modes = cast(int, term_attrs[3])
253 term_attrs[3] = orig_local_modes & mask
254
255 termios.tcsetattr(fd, termios.TCSANOW, term_attrs)
256 return orig_local_modes, term_attrs
257
258
259def PopTermAttrs(fd, orig_local_modes, term_attrs):
260 # type: (int, int, Any) -> None
261
262 term_attrs[3] = orig_local_modes
263 try:
264 termios.tcsetattr(fd, termios.TCSANOW, term_attrs)
265 except termios.error as e:
266 # Superficial fix for issue #1001. I'm not sure why we get errno.EIO,
267 # but we can't really handle it here. In C++ I guess we ignore the
268 # error.
269 pass
270
271
272def OsType():
273 # type: () -> str
274 """Compute $OSTYPE variable."""
275 return posix.uname()[0].lower()
276
277
278def InputAvailable(fd):
279 # type: (int) -> bool
280 # similar to lib/sh/input_avail.c in bash
281 # read, write, except
282 r, w, exc = select.select([fd], [], [fd], 0)
283 return len(r) != 0
284
285
286UNTRAPPED_SIGWINCH = -1
287
288
289class SignalSafe(object):
290 """State that is shared between the main thread and signal handlers.
291
292 See C++ implementation in cpp/core.h
293 """
294
295 def __init__(self):
296 # type: () -> None
297 self.pending_signals = [] # type: List[int]
298 self.last_sig_num = 0 # type: int
299 self.sigint_trapped = False
300 self.received_sigint = False
301 self.received_sigwinch = False
302 self.sigwinch_code = UNTRAPPED_SIGWINCH
303
304 def UpdateFromSignalHandler(self, sig_num, unused_frame):
305 # type: (int, Any) -> None
306 """Receive the given signal, and update shared state.
307
308 This method is registered as a Python signal handler.
309 """
310 self.pending_signals.append(sig_num)
311
312 if sig_num == signal.SIGINT:
313 self.received_sigint = True
314
315 if sig_num == signal.SIGWINCH:
316 self.received_sigwinch = True
317 sig_num = self.sigwinch_code # mutate param
318
319 self.last_sig_num = sig_num
320
321 def LastSignal(self):
322 # type: () -> int
323 """Return the number of the last signal received."""
324 return self.last_sig_num
325
326 def PollSigInt(self):
327 # type: () -> bool
328 """Has SIGINT received since the last time PollSigInt() was called?"""
329 result = self.received_sigint
330 self.received_sigint = False
331 return result
332
333 def PollUntrappedSigInt(self):
334 # type: () -> bool
335 """Has SIGINT received since the last time PollSigInt() was called?"""
336 received = self.PollSigInt()
337 return received and not self.sigint_trapped
338
339 if 0:
340 def SigIntTrapped(self):
341 # type: () -> bool
342 return self.sigint_trapped
343
344 def SetSigIntTrapped(self, b):
345 # type: (bool) -> None
346 """Set a flag to tell us whether sigint is trapped by the user."""
347 self.sigint_trapped = b
348
349 def SetSigWinchCode(self, code):
350 # type: (int) -> None
351 """Depending on whether or not SIGWINCH is trapped by a user, it is
352 expected to report a different code to `wait`.
353
354 SetSigWinchCode() lets us set which code is reported.
355 """
356 self.sigwinch_code = code
357
358 def PollSigWinch(self):
359 # type: () -> bool
360 """Has SIGWINCH been received since the last time PollSigWinch() was
361 called?"""
362 result = self.received_sigwinch
363 self.received_sigwinch = False
364 return result
365
366 def TakePendingSignals(self):
367 # type: () -> List[int]
368 """Transfer ownership of queue of pending signals to caller."""
369
370 # A note on signal-safety here. The main loop might be calling this function
371 # at the same time a signal is firing and appending to
372 # `self.pending_signals`. We can forgoe using a lock here
373 # (which would be problematic for the signal handler) because mutual
374 # exclusivity should be maintained by the atomic nature of pointer
375 # assignment (i.e. word-sized writes) on most modern platforms.
376 # The replacement run list is allocated before the swap, so it can be
377 # interrupted at any point without consequence.
378 # This means the signal handler always has exclusive access to
379 # `self.pending_signals`. In the worst case the signal handler might write to
380 # `new_queue` and the corresponding trap handler won't get executed
381 # until the main loop calls this function again.
382 # NOTE: It's important to distinguish between signal-safety an
383 # thread-safety here. Signals run in the same process context as the main
384 # loop, while concurrent threads do not and would have to worry about
385 # cache-coherence and instruction reordering.
386 new_queue = [] # type: List[int]
387 ret = self.pending_signals
388 self.pending_signals = new_queue
389 return ret
390
391 def ReuseEmptyList(self, empty_list):
392 # type: (List[int]) -> None
393 """This optimization only happens in C++."""
394 pass
395
396
397gSignalSafe = None # type: SignalSafe
398
399gOrigSigIntHandler = None # type: Any
400
401
402def InitSignalSafe():
403 # type: () -> SignalSafe
404 """Set global instance so the signal handler can access it."""
405 global gSignalSafe
406 gSignalSafe = SignalSafe()
407
408 # See
409 # - demo/cpython/keyboard_interrupt.py
410 # - pyos::InitSignalSafe()
411
412 # In C++, we do
413 # RegisterSignalInterest(signal.SIGINT)
414
415 global gOrigSigIntHandler
416 gOrigSigIntHandler = signal.signal(signal.SIGINT,
417 gSignalSafe.UpdateFromSignalHandler)
418
419 return gSignalSafe
420
421
422def sigaction(sig_num, handler):
423 # type: (int, Any) -> None
424 """
425 Handle a signal with SIG_DFL or SIG_IGN, not our own signal handler.
426 """
427
428 # SIGINT and SIGWINCH must be registered through SignalSafe
429 assert sig_num != signal.SIGINT
430 assert sig_num != signal.SIGWINCH
431 signal.signal(sig_num, handler)
432
433
434def RegisterSignalInterest(sig_num):
435 # type: (int) -> None
436 """Have the kernel notify the main loop about the given signal."""
437 #log('RegisterSignalInterest %d', sig_num)
438
439 assert gSignalSafe is not None
440 signal.signal(sig_num, gSignalSafe.UpdateFromSignalHandler)
441
442
443def MakeDirCacheKey(path):
444 # type: (str) -> Tuple[str, int]
445 """Returns a pair (path with last modified time) that can be used to cache
446 directory accesses."""
447 st = posix.stat(path)
448 return (path, int(st.st_mtime))