ambuild2/ipc/windows.py
author David Anderson <danderson@mozilla.com>
Wed Nov 13 16:23:04 2013 -0800 (2013-11-13)
changeset 184 b8e9518bd03b
parent 173 34ed6779b20a
permissions -rw-r--r--
Add Cygwin support.
     1 # vim: set ts=8 sts=2 sw=2 tw=99 et:
     2 #
     3 # This file is part of AMBuild.
     4 # 
     5 # AMBuild is free software: you can redistribute it and/or modify
     6 # it under the terms of the GNU General Public License as published by
     7 # the Free Software Foundation, either version 3 of the License, or
     8 # (at your option) any later version.
     9 # 
    10 # AMBuild is distributed in the hope that it will be useful,
    11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
    12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
    13 # GNU General Public License for more details.
    14 # 
    15 # You should have received a copy of the GNU General Public License
    16 # along with AMBuild. If not, see <http://www.gnu.org/licenses/>.
    17 import struct
    18 import ctypes
    19 import os, sys
    20 import traceback
    21 from collections import deque
    22 from . import process
    23 from . import winapi
    24 from . process import Channel, Error, Special
    25 from ambuild2 import util
    26 
    27 def child_main():
    28   if 'LOG' in os.environ:
    29     import logging
    30     logging.basicConfig(level=logging.INFO)
    31 
    32   for index, arg in enumerate(sys.argv):
    33     if arg == '--name':
    34       name = sys.argv[index + 1]
    35     if arg == '--pipe':
    36       pipe = sys.argv[index + 1]
    37 
    38   # Cygwin seems to lose the initial \ in translation or something.
    39   if pipe.startswith('\\.\\'):
    40     pipe = '\\\\.\\' + pipe
    41 
    42   proxy = NamedPipeProxy(name, pipe)
    43   channel = NamedPipe.from_proxy(proxy)
    44 
    45   from . import impl
    46   channel.send(Special.Connected)
    47   impl.child_main(channel)
    48 
    49 class NamedPipeProxy(object):
    50   def __init__(self, name, path):
    51     self.name = name
    52     self.path = path
    53 
    54   def close(self):
    55     pass
    56 
    57 kPrefixLength = 4
    58 kChannelsKey = 'channels'
    59 
    60 class NamedPipe(Channel):
    61   def __init__(self, name, handle, path):
    62     super(NamedPipe, self).__init__(name)
    63     self.handle = handle
    64     self.path = path
    65     self.read_op = winapi.Overlapped()
    66     self.write_op = None
    67 
    68     # False if nothing is posted to the IOCP
    69     self.waiting_input = False
    70 
    71     # False if we're not yet connected to the other side.
    72     self.connected = False
    73 
    74     # Used for receiving data in chunks.
    75     self.input_buffer = bytearray(4096)
    76     self.input_obj = (ctypes.c_byte * 4096).from_buffer(self.input_buffer)
    77     self.input_overflow = bytearray()
    78 
    79   def close(self):
    80     if self.waiting_input:
    81       winapi.CancelIo(self.handle)
    82     if self.write_op:
    83       winapi.CloseHandle(self.write_op.event())
    84     winapi.CloseHandle(self.handle)
    85 
    86   def send_impl(self, obj, channels=()):
    87     if channels and len(channels) and type(obj) is not dict:
    88       raise Exception('must have dictionary message to send channels')
    89 
    90     if channels:
    91       assert kChannelsKey not in obj
    92 
    93       # Unlike posix we can cheat here and just send the paths for the pipes.
    94       proxies = []
    95       for channel in channels:
    96         assert type(channel) is NamedPipeProxy
    97         proxies.append(channel)
    98       obj[kChannelsKey] = proxies
    99 
   100     # Sends in AMBuild IPC are synchronous, so we must always wait for IO to
   101     # complete. 
   102     if not self.write_op:
   103       self.write_op = winapi.Overlapped()
   104       self.write_op.hEvent = winapi.CreateEvent(manual=True, initial=False)
   105       self.write_op.quiet()
   106     else:
   107       # Reset the event just in case...? Though in theory it should be reset
   108       # automatically by WriteFile() if it immediately succeeds.
   109       winapi.ResetEvent(self.write_op.hEvent)
   110 
   111     # Prepare the data to send. Pad with 4 bytes since bytearray() needs to be
   112     # at least 8 due to a ctypes bug, and we're going to prepend 4 bytes.
   113     data = util.pickle.dumps(obj)
   114     data_bytes = bytearray(data)
   115     while len(data_bytes) < 8 - kPrefixLength:
   116       buf.append(0)
   117 
   118     prefix = struct.pack('i', len(data))
   119     data = bytearray(prefix) + data
   120 
   121     buffer = (ctypes.c_byte * len(data)).from_buffer(data)
   122     nwritten = ctypes.c_int()
   123 
   124     result = winapi.fnWriteFile(
   125       self.handle,
   126       ctypes.cast(buffer, ctypes.c_void_p),
   127       len(data),
   128       ctypes.byref(nwritten),
   129       ctypes.cast(ctypes.pointer(self.write_op), ctypes.c_void_p)
   130     )
   131     if not result:
   132       if winapi.GetLastError() != winapi.ERROR_IO_PENDING:
   133         raise winapi.WinError()
   134 
   135       if not winapi.WaitForSingleObject(self.write_op.hEvent, winapi.INFINITE):
   136         raise Exception('unexpected return value from WaitForSingleObject()')
   137 
   138     # Done.
   139     return
   140 
   141   def complete_connection(self, nbytes):
   142     if not self.waiting_input:
   143       # We could already be connected, if we're on the child or if the parent
   144       # has used connect_sync().
   145       if not self.connected:
   146         assert nbytes == 0
   147         if not self.connect_async():
   148           # Connection hasn't occurred yet, so just keep waiting.
   149           return False
   150 
   151     # If we got here, we're definitely connected.
   152     if not self.connected:
   153       assert nbytes == 0
   154       self.connected = True
   155       self.waiting_input = False
   156 
   157     # If we're already waiting for input, but we got 0 bytes, just ignore
   158     # this event (it's probably faked from our own code).
   159     if self.waiting_input and not nbytes:
   160       return False
   161 
   162     self.waiting_input = False
   163     return True
   164 
   165   def post_read(self):
   166     bytes_read = ctypes.c_int()
   167     result = winapi.fnReadFile(
   168       self.handle,
   169       ctypes.cast(self.input_obj, ctypes.c_void_p),
   170       len(self.input_buffer),
   171       ctypes.byref(bytes_read),
   172       ctypes.cast(ctypes.pointer(self.read_op), ctypes.c_void_p)
   173     )
   174 
   175     if not result:
   176       if winapi.GetLastError() == winapi.ERROR_IO_PENDING:
   177         self.waiting_input = True
   178         return None
   179       raise winapi.WinError()
   180 
   181     # Even if the ReadFile() succeeds, and we have data immediately
   182     # available, Windows still posts to the IOCP. This makes very little
   183     # sense to me, but the IOCP API is awful in general. We just return
   184     # immediately if this is the case, to avoid reading the data twice.
   185     self.waiting_input = True
   186 
   187   # Respond to a GetQueuedCompletionStatus().
   188   def complete_incoming_io(self, nbytes):
   189     if not self.complete_connection(nbytes):
   190       return []
   191 
   192     if not nbytes:
   193       self.post_read()
   194       return []
   195 
   196     messages = []
   197 
   198     message = self.receive_bytes(nbytes)
   199     if message:
   200       messages.append(message)
   201       while True:
   202         message = self.seek_remaining_message()
   203         if not message:
   204           break
   205         messages.append(message)
   206 
   207     if not self.waiting_input:
   208       self.post_read()
   209 
   210     # We should always be awaiting input again, otherwise, we'll never receive
   211     # more events from the IO Completion Port.
   212     assert self.waiting_input
   213     return messages
   214 
   215   # Shorthand for complete_incoming_io(0)
   216   def initiate_io(self):
   217     if not self.complete_connection(0):
   218       return
   219     self.post_read()
   220 
   221   # Process newly received bytes.
   222   def receive_bytes(self, nbytes):
   223     total_length = len(self.input_overflow) + nbytes
   224 
   225     # Do we have enough for a message prefix?
   226     if total_length < 4:
   227       self.input_overflow += self.input_buffer
   228       return None
   229 
   230     # Get the message prefix.
   231     msg_prefix = self.input_overflow[0:kPrefixLength]
   232     if len(msg_prefix) < kPrefixLength:
   233       start = len(msg_prefix)
   234       end = kPrefixLength - start
   235       msg_prefix = self.input_buffer[start:end]
   236 
   237     # See if we have enough bytes to cover the message.
   238     msg_size, = struct.unpack('i', bytes(msg_prefix))
   239     if total_length - kPrefixLength < msg_size:
   240       self.input_overflow += self.input_buffer
   241       return None
   242 
   243     # This is a horrible amount of copying... maybe consider using a deque
   244     # of bytes or something if it's a performance problem.
   245     if not len(self.input_overflow):
   246       message = util.pickle.loads(
   247           bytes(self.input_buffer[kPrefixLength : kPrefixLength + msg_size])
   248       )
   249       self.input_overflow += self.input_buffer[kPrefixLength + msg_size : nbytes]
   250     else:
   251       # Steal remaining bytes from the input buffer, if needed.
   252       if len(self.input_overflow) < kPrefixLength + msg_size:
   253         msg_remaining = kPrefixLength + msg_size - len(self.input_overflow)
   254         self.input_overflow += self.input_buffer[:msg_remaining]
   255       else:
   256         msg_remaining = 0
   257 
   258       message = util.pickle.loads(
   259         bytes(self.input_overflow[kPrefixLength : kPrefixLength + msg_size])
   260       )
   261 
   262       # Adjust the input buffer.
   263       del self.input_overflow[0 : kPrefixLength + msg_size]
   264       self.input_overflow += self.input_buffer[msg_remaining : nbytes]
   265 
   266     return message
   267 
   268   # Process any remaining messages.
   269   def seek_remaining_message(self):
   270     if len(self.input_overflow) < kPrefixLength:
   271       return None
   272 
   273     msg_size, = struct.unpack('i', bytes(self.input_overflow[0:kPrefixLength]))
   274     if len(self.input_overflow) < kPrefixLength + msg_size:
   275       return None
   276 
   277     message = util.pickle.loads(
   278       bytes(self.input_overflow[kPrefixLength : kPrefixLength + msg_size])
   279     )
   280     del self.input_overflow[0 : kPrefixLength + msg_size]
   281 
   282     return message
   283 
   284   def connect_async(self):
   285     result = winapi.fnConnectNamedPipe(self.handle, ctypes.byref(self.read_op))
   286     assert not result # MSDN says this is impossible with async pipes
   287 
   288     gle = winapi.GetLastError()
   289     if gle == winapi.ERROR_PIPE_CONNECTED:
   290       assert not self.waiting_input
   291       return True
   292 
   293     if gle != winapi.ERROR_IO_PENDING:
   294       raise winapi.WinError()
   295 
   296     return False
   297 
   298   def connect_sync(self, proc_handle):
   299     event = winapi.CreateEvent(manual=True, initial=False)
   300 
   301     # Suppress completion status posting, since this overlapped is temporary.
   302     overlapped = winapi.Overlapped(event)
   303     overlapped.quiet()
   304 
   305     try:
   306       result = winapi.fnConnectNamedPipe(self.handle, ctypes.byref(overlapped))
   307       assert not result # MSDN says this is impossible with async pipes
   308 
   309       gle = winapi.GetLastError()
   310       if gle != winapi.ERROR_PIPE_CONNECTED:
   311         if gle != winapi.ERROR_IO_PENDING:
   312           raise winapi.WinError()
   313 
   314 
   315         # We need to wait for either process death or for pipe connection.
   316         result = winapi.WaitForMultipleObjects(
   317           handles=[overlapped.hEvent, proc_handle],
   318           wait_all=False,
   319           wait=winapi.INFINITE
   320         )
   321 
   322         if result != winapi.WAIT_OBJECT_0:
   323           if result == winapi.WAIT_OBJECT_0 + 1:
   324             raise Exception('child process died before connecting to pipe')
   325           raise Exception('unexpected WaitForMultipleObjects return code: {0}'.format(result))
   326     finally:
   327       winapi.CloseHandle(event)
   328 
   329     # Fake this to true so we don't try to connect asynchronously.
   330     self.connected = True
   331     self.initiate_io()
   332 
   333   @classmethod
   334   def from_proxy(cls, proxy):
   335     handle = winapi.OpenPipe(proxy.path)
   336     pipe = cls(proxy.name, handle, proxy.path)
   337     pipe.connected = True
   338     return pipe
   339 
   340   @classmethod
   341   def connect(cls, channel, name):
   342     pipe = cls.from_proxy(channel)
   343     pipe.name = name
   344     pipe.send(Special.Connected)
   345     return pipe
   346 
   347   @classmethod
   348   def new(cls, name):
   349     pipe, path = winapi.CreateNamedPipe()
   350     parent = cls(name, pipe, path)
   351     child = NamedPipeProxy(name, path)
   352     return parent, child
   353 
   354 class MessagePump(process.MessagePump):
   355   def __init__(self):
   356     super(MessagePump, self).__init__()
   357     self.port = winapi.CreateIoCompletionPort()
   358     self.listeners = {}
   359     self.channels = {}
   360     self.next_key = 0
   361     self.pending = deque()
   362 
   363   def close(self):
   364     super(LinuxMessagePump, self).close()
   365     winapi.CloseHandle(self.port)
   366 
   367   def shouldProcessEvents(self):
   368     if not super(MessagePump, self).shouldProcessEvents():
   369       return False
   370     return len(self.channels) or len(self.pending)
   371 
   372   def createChannel(self, name):
   373     return NamedPipe.new(name)
   374 
   375   def registerChannel(self, channel, listener):
   376     assert channel.handle.value not in self.channels
   377 
   378     key = self.next_key
   379     self.next_key += 1
   380 
   381     winapi.RegisterIoCompletion(self.port, channel.handle, key)
   382     self.listeners[key] = channel, listener
   383     self.channels[channel.handle.value] = key
   384     return key
   385 
   386   def addChannel(self, channel, listener):
   387     key = self.registerChannel(channel, listener)
   388 
   389     # On Windows, we must initiate a recv() that would block in order to
   390     # receive io completion events. Since that could return actual data,
   391     # we enqueue these back into the message loop.
   392     channel.initiate_io()
   393 
   394   def dropChannel(self, channel):
   395     key = self.channels[channel.handle.value]
   396     del self.channels[channel.handle.value]
   397     del self.listeners[key]
   398 
   399   def processMessage(self, message, channel, listener):
   400     if message == Special.Closing:
   401       self.handle_channel_error(channel, listener, Error.NormalShutdown)
   402       return
   403 
   404     try:
   405       listener.receiveMessage(channel, message)
   406     except Exception as exn:
   407       traceback.print_exc()
   408       self.handle_channel_error(channel, listener, Error.User)
   409 
   410   def processEvents(self):
   411     if not self.pending:
   412       if not self.poll_io_port():
   413         return
   414 
   415     if not self.pending:
   416       return
   417 
   418     key, message = self.pending.popleft()
   419     if key not in self.listeners:
   420       return
   421 
   422     channel, listener = self.listeners[key]
   423     self.processMessage(message, channel, listener)
   424 
   425   def poll_io_port(self):
   426     result, nbytes, key, overlapped = winapi.GetQueuedCompletionStatus(self.port, winapi.INFINITE)
   427 
   428     # There is no way to remove a file from a completion port, so just as a
   429     # precaution, we check this.
   430     if key.value not in self.listeners:
   431       return False
   432 
   433     address = ctypes.addressof(overlapped)
   434     channel, listener = self.listeners[key.value]
   435 
   436     # We only care about read events, so if we're receiving some kind of white
   437     # hot lie, assert.
   438     if ctypes.addressof(channel.read_op) != address:
   439       raise Exception('unexpected IO completion on {0}'.format(listener.name))
   440 
   441     if not result:
   442       self.handle_channel_error(channel, listener, Error.EOF)
   443       return False
   444 
   445     messages = None
   446     try:
   447       messages = channel.complete_incoming_io(nbytes=nbytes.value)
   448     except WindowsError as exn:
   449       if exn.winerror != winapi.ERROR_BROKEN_PIPE:
   450         traceback.print_exc()
   451     except Exception as exn:
   452       traceback.print_exc()
   453 
   454     if messages is None:
   455       self.handle_channel_error(channel, listener, Error.EOF)
   456       return
   457 
   458     for message in messages:
   459       self.pending.append((key.value, message))
   460 
   461     # Finished processing this status change.
   462     return True
   463 
   464   def handle_channel_error(self, channel, listener, error):
   465     self.dropChannel(channel)
   466     listener.receiveError(channel, error)
   467 
   468 class ProcessHost(process.ProcessHost):
   469   def __init__(self, id, proc, channel):
   470     super(ProcessHost, self).__init__(id, proc, channel)
   471 
   472 class ProcessManager(process.ProcessManager):
   473   def __init__(self, pump):
   474     super(ProcessManager, self).__init__(pump)
   475 
   476   def create_process_and_pipe(self, id, listener):
   477     # Create pipes.
   478     parent, child = NamedPipe.new(listener.name)
   479 
   480     # Spawn the process.
   481     proc = winapi.Process.spawn(child)
   482 
   483     # Watch for changes on the parent channel. We don't use addChannel(), since
   484     # that will try to asynchronously connect. We want synchronous connections.
   485     self.pump.registerChannel(parent, listener)
   486 
   487     # Require synchronous connection or process death.
   488     parent.connect_sync(proc.handle)
   489 
   490     return ProcessHost(id, proc, parent)
   491 
   492   def close_process(self, host):
   493     pass