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

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