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

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