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

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