Google App Engine SDK 1.9.46 のWindows x64版は罠

# 経緯
解凍して、配置して、`goapp serve`したらFailed.

<a href="https://cloud.google.com/appengine/docs/go/download" target="_blank">Download the App Engine SDK for Go  |  App Engine standard environment for Go  |  Google Cloud Platform</a>

#パッチ

エラーをみると、`set_skip_files_re`関数が無いヨ。といわれている。たしかにWin32FileWatcherクラスだけ実装されていなかった。以下は付け焼き刃的パッチなので、不完全だとおもいます。app.yaml でskip_files パラメータを使用している環境の方はアップデートを待つほうがいいとおもいます。

<a href="https://cloud.google.com/appengine/docs/python/config/appref#skip_files" target="_blank">app.yaml Reference  |  App Engine standard environment for Python  |  Google Cloud Platform</a>

Version: 1.9.46 - 2016-11-03 Size: 67.7 MB

```diff:go_appengine\google\appengine\tools\devappserver2\win32_file_watcher.py
class Win32FileWatcher(object):
  """Monitors a directory tree for changes using win32 API."""
  SUPPORTS_MULTIPLE_DIRECTORIES = False

  def __init__(self, directory):
    """Initializer for Win32FileWatcher.

    Args:
      directory: A string representing the path to a directory that should
          be monitored for changes i.e. files and directories added, renamed,
          deleted or changed.
    """
    self._directory = os.path.abspath(directory)
    self._directory_handle = None
    self._change_set = set()
    self._lock = threading.Lock()  # protects self._change_set
    self._stop = threading.Event()
    self._change_event = threading.Event()
    self._thread = None
+   self._skip_files_re = None

+ def set_skip_files_re(self, skip_files_re):
+   """Allows the file watcher to respect skip_files in app.yaml.
+
+   Args:
+     skip_files_re: The skip_files field of current ModuleConfiguration,
+         defined in app.yaml.
+   """
+   self._skip_files_re = skip_files_re

  def start(self):
    """Start watching the directory for changes."""
    self._directory_handle = ctypes.windll.kernel32.CreateFileW(
        ctypes.c_wchar_p(self._directory),
        ctypes.c_ulong(_FILE_LIST_DIRECTORY),
        ctypes.c_ulong(_FILE_SHARE_READ |
                       _FILE_SHARE_WRITE),
        None,
        ctypes.c_ulong(_OPEN_EXISTING),
        # required to monitor changes.
        ctypes.c_ulong(_FILE_FLAG_BACKUP_SEMANTICS),
        None)
    if self._directory_handle == _INVALID_HANDLE_VALUE:
      raise ctypes.WinError()
    self._thread = threading.Thread(
        target=self._monitor, name='Win32 File Watcher')
    self._thread.start()

  def quit(self):
    """Stop watching the directory for changes."""
    self._stop.set()
    # Note: this will unlock the blocking ReadDirectoryChangesW call.
    ctypes.windll.kernel32.CancelIoEx(self._directory_handle, None)
    self._thread.join()
    ctypes.windll.kernel32.CloseHandle(self._directory_handle)

  def changes(self, timeout_ms=0):
    """Returns the paths changed in the watched directory since the last call.

    start() must be called before this method.

    Args:
      timeout_ms: the maximum number of milliseconds you allow this function to
                  wait for a filesystem change.

    Returns:
      Returns an iterable of changed directories/files.
    """
    if timeout_ms != 0:
      self._change_event.wait(timeout_ms / 1000.0)

    with self._lock:
      result = self._change_set
      self._change_set = set()
      self._change_event.clear()

    return result

  def _monitor(self):
    buff = ctypes.create_string_buffer(_BUFF_SIZE)
    while not self._stop.isSet():
      size_returned = ctypes.c_ulong(0)
      result = ctypes.windll.kernel32.ReadDirectoryChangesW(
          self._directory_handle,
          buff,
          ctypes.c_ulong(_BUFF_SIZE),
          True,  # recursive.
          ctypes.c_ulong(_FILE_NOTIFY_CHANGE_ANY),
          ctypes.byref(size_returned),
          None,
          None)  # this is a blocking call.
      if result == 0 and ctypes.GetLastError() == _ERROR_NOTIFY_ENUM_DIR:
        logging.warning('Buffer overflow while monitoring for file changes.')
        # we need to notify that something changed anyway
        with self._lock:
          self._change_set |= {'Unknown file'}
      if result != 0 and size_returned.value != 0:
        additional_changes = _parse_buffer(buff)
        with self._lock:
          self._change_set |= additional_changes
          self._change_event.set()

```