# Python Tools for Visual Studio
# Copyright(c) Microsoft Corporation
# All rights reserved.
# 
# Licensed under the Apache License, Version 2.0 (the License); you may not use
# this file except in compliance with the License. You may obtain a copy of the
# License at http://www.apache.org/licenses/LICENSE-2.0
# 
# THIS CODE IS PROVIDED ON AN  *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS
# OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY
# IMPLIED WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE,
# MERCHANTABLITY OR NON-INFRINGEMENT.
# 
# See the Apache Version 2.0 License for specific language governing
# permissions and limitations under the License.
from __future__ import absolute_import, print_function, with_statement

__author__ = "Microsoft Corporation <ptvshelp@microsoft.com>"
__version__ = "3.0.0"

import ctypes
import datetime
import os
import re
import struct
import sys
import traceback
from xml.dom import minidom

try:
    from cStringIO import StringIO
    BytesIO = StringIO
except ImportError:
    from io import StringIO, BytesIO
try:
    from thread import start_new_thread
except ImportError:
    from _thread import start_new_thread

if sys.version_info[0] == 3:
    def to_str(value):
        return value.decode(sys.getfilesystemencoding())
else:
    def to_str(value):
        return value.encode(sys.getfilesystemencoding())


# http://www.fastcgi.com/devkit/doc/fcgi-spec.html#S3

FCGI_VERSION_1 = 1
FCGI_HEADER_LEN = 8

FCGI_BEGIN_REQUEST       = 1
FCGI_ABORT_REQUEST       = 2
FCGI_END_REQUEST         = 3
FCGI_PARAMS              = 4
FCGI_STDIN               = 5
FCGI_STDOUT              = 6
FCGI_STDERR              = 7
FCGI_DATA                = 8
FCGI_GET_VALUES          = 9
FCGI_GET_VALUES_RESULT  = 10
FCGI_UNKNOWN_TYPE       = 11
FCGI_MAXTYPE = FCGI_UNKNOWN_TYPE

FCGI_NULL_REQUEST_ID    = 0

FCGI_KEEP_CONN = 1

FCGI_RESPONDER  = 1
FCGI_AUTHORIZER = 2
FCGI_FILTER     = 3

FCGI_REQUEST_COMPLETE = 0
FCGI_CANT_MPX_CONN    = 1
FCGI_OVERLOADED       = 2
FCGI_UNKNOWN_ROLE     = 3

FCGI_MAX_CONNS  = "FCGI_MAX_CONNS"
FCGI_MAX_REQS   = "FCGI_MAX_REQS"
FCGI_MPXS_CONNS = "FCGI_MPXS_CONNS"

class FastCgiRecord(object):
    """Represents a FastCgiRecord.  Encapulates the type, role, flags.  Holds
    onto the params which we will receive and update later."""
    def __init__(self, type, req_id, role, flags):
        self.type = type
        self.req_id = req_id
        self.role = role
        self.flags = flags
        self.params = {}
        
    def __repr__(self):
        return '<FastCgiRecord(%d, %d, %d, %d)>' % (self.type, 
                                                    self.req_id, 
                                                    self.role, 
                                                    self.flags)

#typedef struct {
#   unsigned char version;
#   unsigned char type;
#   unsigned char requestIdB1;
#   unsigned char requestIdB0;
#   unsigned char contentLengthB1;
#   unsigned char contentLengthB0;
#   unsigned char paddingLength;
#   unsigned char reserved;
#   unsigned char contentData[contentLength];
#   unsigned char paddingData[paddingLength];
#} FCGI_Record;

class _ExitException(Exception):
    pass

if sys.version_info[0] >= 3:
    # indexing into byte strings gives us an int, so
    # ord is unnecessary on Python 3
    def ord(x):
        return x
    def chr(x):
        return bytes((x, ))

    def wsgi_decode(x):
        return x.decode('iso-8859-1')
    def wsgi_encode(x):
        return x.encode('iso-8859-1')

    def fs_encode(x):
        return x

    def exception_with_traceback(exc_value, exc_tb):
        return exc_value.with_traceback(exc_tb)

    zero_bytes = bytes
else:
    # Replace the builtin open with one that supports an encoding parameter
    from codecs import open

    def wsgi_decode(x):
        return x
    def wsgi_encode(x):
        return x

    def fs_encode(x):
        return x if isinstance(x, str) else x.encode(sys.getfilesystemencoding())

    def exception_with_traceback(exc_value, exc_tb):
        # x.with_traceback() is not supported on 2.x
        return exc_value

    bytes = str

    def zero_bytes(length):
        return '\x00' * length

def read_fastcgi_record(stream):
    """reads the main fast cgi record"""
    data = stream.read(8)     # read record
    if not data:
        # no more data, our other process must have died...
        raise _ExitException()

    fcgi_ver, reqtype, req_id, content_size, padding_len, _ = struct.unpack('>BBHHBB', data)

    content = stream.read(content_size)  # read content
    stream.read(padding_len)

    if fcgi_ver != FCGI_VERSION_1:
        raise Exception('Unknown fastcgi version %s' % fcgi_ver)

    processor = REQUEST_PROCESSORS.get(reqtype)
    if processor is not None:
        return processor(stream, req_id, content)

    # unknown type requested, send response
    log('Unknown request type %s' % reqtype)
    send_response(stream, req_id, FCGI_UNKNOWN_TYPE, chr(reqtype) + zero_bytes(7))
    return None


def read_fastcgi_begin_request(stream, req_id, content):
    """reads the begin request body and updates our _REQUESTS table to include
    the new request"""
    #    typedef struct {
    #        unsigned char roleB1;
    #        unsigned char roleB0;
    #        unsigned char flags;
    #        unsigned char reserved[5];
    #    } FCGI_BeginRequestBody;

    # TODO: Ignore request if it exists
    res = FastCgiRecord(
        FCGI_BEGIN_REQUEST,
        req_id,
        (ord(content[0]) << 8) | ord(content[1]),   # role
        ord(content[2]),  # flags
    )
    _REQUESTS[req_id] = res

def read_encoded_int(content, offset):
    i = struct.unpack_from('>B', content, offset)[0]

    if i < 0x80:
        return offset + 1, i
    
    return offset + 4, struct.unpack_from('>I', content, offset)[0] & ~0x80000000


def read_fastcgi_keyvalue_pairs(content, offset):
    """Reads a FastCGI key/value pair stream"""

    offset, name_len = read_encoded_int(content, offset)
    offset, value_len = read_encoded_int(content, offset)

    name = content[offset:(offset + name_len)]
    offset += name_len
    
    value = content[offset:(offset + value_len)]
    offset += value_len

    return offset, name, value


def get_encoded_int(i):
    """Writes the length of a single name for a key or value in a key/value
    stream"""
    if i <= 0x7f:
        return struct.pack('>B', i)
    elif i < 0x80000000:
        return struct.pack('>I', i | 0x80000000)
    else:
        raise ValueError('cannot encode value %s (%x) because it is too large' % (i, i))


def write_fastcgi_keyvalue_pairs(pairs):
    """Creates a FastCGI key/value stream and returns it as a byte string"""
    parts = []
    for raw_key, raw_value in pairs.items():
        key = wsgi_encode(raw_key)
        value = wsgi_encode(raw_value)
        
        parts.append(get_encoded_int(len(key)))
        parts.append(get_encoded_int(len(value)))
        parts.append(key)
        parts.append(value)

    return bytes().join(parts)

# Keys in this set will be stored in the record without modification but with a
# 'wsgi.' prefix. The original key will have the decoded version.
# (Following mod_wsgi from http://wsgi.readthedocs.org/en/latest/python3.html)
RAW_VALUE_NAMES = {
    'SCRIPT_NAME' : 'wsgi.script_name',
    'PATH_INFO' : 'wsgi.path_info',
    'QUERY_STRING' : 'wsgi.query_string',
    'HTTP_X_ORIGINAL_URL' : 'wfastcgi.http_x_original_url',
}

def read_fastcgi_params(stream, req_id, content):
    if not content:
        return None

    offset = 0
    res = _REQUESTS[req_id].params
    while offset < len(content):
        offset, name, value = read_fastcgi_keyvalue_pairs(content, offset)
        name = wsgi_decode(name)
        raw_name = RAW_VALUE_NAMES.get(name)
        if raw_name:
            res[raw_name] = value
        res[name] = wsgi_decode(value)


def read_fastcgi_input(stream, req_id, content):
    """reads FastCGI std-in and stores it in wsgi.input passed in the
    wsgi environment array"""
    res = _REQUESTS[req_id].params
    if 'wsgi.input' not in res:
        res['wsgi.input'] = content
    else:
        res['wsgi.input'] += content

    if not content:
        # we've hit the end of the input stream, time to process input...
        return _REQUESTS[req_id]


def read_fastcgi_data(stream, req_id, content):
    """reads FastCGI data stream and publishes it as wsgi.data"""
    res = _REQUESTS[req_id].params
    if 'wsgi.data' not in res:
        res['wsgi.data'] = content
    else:
        res['wsgi.data'] += content


def read_fastcgi_abort_request(stream, req_id, content):
    """reads the wsgi abort request, which we ignore, we'll send the
    finish execution request anyway..."""
    pass


def read_fastcgi_get_values(stream, req_id, content):
    """reads the fastcgi request to get parameter values, and immediately 
    responds"""
    offset = 0
    request = {}
    while offset < len(content):
        offset, name, value = read_fastcgi_keyvalue_pairs(content, offset)
        request[name] = value

    response = {}
    if FCGI_MAX_CONNS in request:
        response[FCGI_MAX_CONNS] = '1'

    if FCGI_MAX_REQS in request:
        response[FCGI_MAX_REQS] = '1'

    if FCGI_MPXS_CONNS in request:
        response[FCGI_MPXS_CONNS] = '0'

    send_response(
        stream,
        req_id,
        FCGI_GET_VALUES_RESULT,
        write_fastcgi_keyvalue_pairs(response)
    )


# Our request processors for different FastCGI protocol requests. Only those
# requests that we receive are defined here.
REQUEST_PROCESSORS = {
    FCGI_BEGIN_REQUEST : read_fastcgi_begin_request,
    FCGI_ABORT_REQUEST : read_fastcgi_abort_request,
    FCGI_PARAMS : read_fastcgi_params,
    FCGI_STDIN : read_fastcgi_input,
    FCGI_DATA : read_fastcgi_data,
    FCGI_GET_VALUES : read_fastcgi_get_values
}

APPINSIGHT_CLIENT = None

def log(txt):
    """Logs messages to a log file if WSGI_LOG env var is defined."""
    if APPINSIGHT_CLIENT:
        try:
            APPINSIGHT_CLIENT.track_event(txt)
        except:
            pass
    
    log_file = os.environ.get('WSGI_LOG')
    if log_file:
        with open(log_file, 'a+', encoding='utf-8') as f:
            txt = txt.replace('\r\n', '\n')
            f.write('%s: %s%s' % (datetime.datetime.now(), txt, '' if txt.endswith('\n') else '\n'))

def maybe_log(txt):
    """Logs messages to a log file if WSGI_LOG env var is defined, and does not
    raise exceptions if logging fails."""
    try:
        log(txt)
    except:
        pass

def send_response(stream, req_id, resp_type, content, streaming=True):
    """sends a response w/ the given id, type, and content to the server.
    If the content is streaming then an empty record is sent at the end to 
    terminate the stream"""
    if not isinstance(content, bytes):
        raise TypeError("content must be encoded before sending: %r" % content)
    
    offset = 0
    while True:
        len_remaining = max(min(len(content) - offset, 0xFFFF), 0)

        data = struct.pack(
            '>BBHHBB',
            FCGI_VERSION_1,     # version
            resp_type,          # type
            req_id,             # requestIdB1:B0
            len_remaining,      # contentLengthB1:B0
            0,                  # paddingLength
            0,                  # reserved
        ) + content[offset:(offset + len_remaining)]

        offset += len_remaining

        os.write(stream.fileno(), data)
        if len_remaining == 0 or not streaming:
            break
    stream.flush()

def get_environment(dir):
    web_config = os.path.join(dir, 'Web.config')
    if not os.path.exists(web_config):
        return {}

    d = {}
    doc = minidom.parse(web_config)
    config = doc.getElementsByTagName('configuration')
    for configSection in config:
        appSettings = configSection.getElementsByTagName('appSettings')
        for appSettingsSection in appSettings:
            values = appSettingsSection.getElementsByTagName('add')
            for curAdd in values:
                key = curAdd.getAttribute('key')
                value = curAdd.getAttribute('value')
                if key and value is not None:
                    d[key.strip()] = value
    return d

ReadDirectoryChangesW = ctypes.windll.kernel32.ReadDirectoryChangesW
ReadDirectoryChangesW.restype = ctypes.c_uint32
ReadDirectoryChangesW.argtypes  = [
    ctypes.c_void_p,     # HANDLE hDirectory
    ctypes.c_void_p,     # LPVOID lpBuffer
    ctypes.c_uint32,     # DWORD nBufferLength
    ctypes.c_uint32,     # BOOL bWatchSubtree
    ctypes.c_uint32,     # DWORD dwNotifyFilter
    ctypes.POINTER(ctypes.c_uint32),  # LPDWORD lpBytesReturned
    ctypes.c_void_p,     # LPOVERLAPPED lpOverlapped
    ctypes.c_void_p      # LPOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine
]
try:
    from _winapi import (CreateFile, CloseHandle, GetLastError, ExitProcess,
                         WaitForSingleObject, INFINITE, OPEN_EXISTING)
except ImportError:
    CreateFile = ctypes.windll.kernel32.CreateFileW
    CreateFile.restype = ctypes.c_void_p
    CreateFile.argtypes  = [
        ctypes.c_wchar_p,     # lpFilename
        ctypes.c_uint32,      # dwDesiredAccess
        ctypes.c_uint32,      # dwShareMode
        ctypes.c_void_p,      # LPSECURITY_ATTRIBUTES,
        ctypes.c_uint32,      # dwCreationDisposition,
        ctypes.c_uint32,      # dwFlagsAndAttributes,
        ctypes.c_void_p       # hTemplateFile
    ]

    CloseHandle = ctypes.windll.kernel32.CloseHandle
    CloseHandle.argtypes = [ctypes.c_void_p]

    GetLastError = ctypes.windll.kernel32.GetLastError
    GetLastError.restype = ctypes.c_uint32

    ExitProcess = ctypes.windll.kernel32.ExitProcess
    ExitProcess.restype = ctypes.c_void_p
    ExitProcess.argtypes  = [ctypes.c_uint32]

    WaitForSingleObject = ctypes.windll.kernel32.WaitForSingleObject
    WaitForSingleObject.argtypes = [ctypes.c_void_p, ctypes.c_uint32]
    WaitForSingleObject.restype = ctypes.c_uint32

    OPEN_EXISTING = 3
    INFINITE = -1

FILE_LIST_DIRECTORY = 1
FILE_SHARE_READ = 0x00000001
FILE_SHARE_WRITE = 0x00000002
FILE_SHARE_DELETE = 0x00000004
FILE_FLAG_BACKUP_SEMANTICS = 0x02000000
MAX_PATH = 260
FILE_NOTIFY_CHANGE_LAST_WRITE  = 0x10
ERROR_NOTIFY_ENUM_DIR = 1022
INVALID_HANDLE_VALUE = 0xFFFFFFFF

class FILE_NOTIFY_INFORMATION(ctypes.Structure):
    _fields_ = [('NextEntryOffset', ctypes.c_uint32),
                ('Action', ctypes.c_uint32),
                ('FileNameLength', ctypes.c_uint32),
                ('Filename', ctypes.c_wchar)]

_ON_EXIT_TASKS = None
def run_exit_tasks():
    global _ON_EXIT_TASKS
    maybe_log("Running on_exit tasks")
    while _ON_EXIT_TASKS:
        tasks, _ON_EXIT_TASKS = _ON_EXIT_TASKS, []
        for t in tasks:
            try:
                t()
            except Exception:
                maybe_log("Error in exit task: " + traceback.format_exc())

def on_exit(task):
    global _ON_EXIT_TASKS
    if _ON_EXIT_TASKS is None:
        _ON_EXIT_TASKS = tasks = []
        try:
            evt = int(os.getenv('_FCGI_SHUTDOWN_EVENT_'))
        except (TypeError, ValueError):
            maybe_log("Could not wait on event %s" % os.getenv('_FCGI_SHUTDOWN_EVENT_'))
        else:
            def _wait_for_exit():
                WaitForSingleObject(evt, INFINITE)
                run_exit_tasks()
                ExitProcess(0)

            start_new_thread(_wait_for_exit, ())
    _ON_EXIT_TASKS.append(task)

def start_file_watcher(path, restart_regex):
    if restart_regex is None:
        restart_regex = ".*((\\.py)|(\\.config))$"
    elif not restart_regex:
        # restart regex set to empty string, no restart behavior
        return
    
    def enum_changes(path):
        """Returns a generator that blocks until a change occurs, then yields
        the filename of the changed file.

        Yields an empty string and stops if the buffer overruns, indicating that
        too many files were changed."""

        buffer = ctypes.create_string_buffer(32 * 1024)
        bytes_ret = ctypes.c_uint32()

        try:
            the_dir = CreateFile(
                path, 
                FILE_LIST_DIRECTORY, 
                FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
                0,
                OPEN_EXISTING,
                FILE_FLAG_BACKUP_SEMANTICS,
                0,
            )
        except OSError:
            maybe_log("Unable to create watcher")
            return

        if not the_dir or the_dir == INVALID_HANDLE_VALUE:
            maybe_log("Unable to create watcher")
            return

        while True:
            ret_code = ReadDirectoryChangesW(
                the_dir, 
                buffer, 
                ctypes.sizeof(buffer), 
                True, 
                FILE_NOTIFY_CHANGE_LAST_WRITE,
                ctypes.byref(bytes_ret),
                None,
                None,
            )

            if ret_code:
                cur_pointer = ctypes.addressof(buffer)
                while True:
                    fni = ctypes.cast(cur_pointer, ctypes.POINTER(FILE_NOTIFY_INFORMATION))
                    # FileName is not null-terminated, so specifying length is mandatory.
                    filename = ctypes.wstring_at(cur_pointer + 12, fni.contents.FileNameLength // 2)
                    yield filename
                    if fni.contents.NextEntryOffset == 0:
                        break
                    cur_pointer = cur_pointer + fni.contents.NextEntryOffset
            elif GetLastError() == ERROR_NOTIFY_ENUM_DIR:
                CloseHandle(the_dir)
                yield ''
                return
            else:
                CloseHandle(the_dir)
                return

    log('wfastcgi.py will restart when files in %s are changed: %s' % (path, restart_regex))
    def watcher(path, restart):
        for filename in enum_changes(path):
            if not filename:
                log('wfastcgi.py exiting because the buffer was full')
                run_exit_tasks()
                ExitProcess(0)
            elif restart.match(filename):
                log('wfastcgi.py exiting because %s has changed, matching %s' % (filename, restart_regex))
                # we call ExitProcess directly to quickly shutdown the whole process
                # because sys.exit(0) won't have an effect on the main thread.
                run_exit_tasks()
                ExitProcess(0)

    restart = re.compile(restart_regex)
    start_new_thread(watcher, (path, restart))

def get_wsgi_handler(handler_name):
    if not handler_name:
        raise Exception('WSGI_HANDLER env var must be set')
    
    if not isinstance(handler_name, str):
        handler_name = to_str(handler_name)
    
    module_name, _, callable_name = handler_name.rpartition('.')
    should_call = callable_name.endswith('()')
    callable_name = callable_name[:-2] if should_call else callable_name
    name_list = [(callable_name, should_call)]
    handler = None
    last_tb = ''

    while module_name:
        try:
            handler = __import__(module_name, fromlist=[name_list[0][0]])
            last_tb = ''
            for name, should_call in name_list:
                handler = getattr(handler, name)
                if should_call:
                    handler = handler()
            break
        except ImportError:
            module_name, _, callable_name = module_name.rpartition('.')
            should_call = callable_name.endswith('()')
            callable_name = callable_name[:-2] if should_call else callable_name
            name_list.insert(0, (callable_name, should_call))
            handler = None
            last_tb = ': ' + traceback.format_exc()
    
    if handler is None:
        raise ValueError('"%s" could not be imported%s' % (handler_name, last_tb))
    
    return handler

def read_wsgi_handler(physical_path):
    global APPINSIGHT_CLIENT
    env = get_environment(physical_path)
    os.environ.update(env)
    for path in (v for k, v in env.items() if k.lower() == 'pythonpath'):
        # Expand environment variables manually.
        expanded_path = re.sub(
            '%(\\w+?)%',
            lambda m: os.getenv(m.group(1), ''),
            path
        )
        sys.path.extend(fs_encode(p) for p in expanded_path.split(';') if p)
    
    handler = get_wsgi_handler(os.getenv("WSGI_HANDLER"))
    instr_key = os.getenv("APPINSIGHTS_INSTRUMENTATIONKEY")
    if instr_key:
        try:
            # Attempt the import after updating sys.path - sites must
            # include applicationinsights themselves.
            from applicationinsights.requests import WSGIApplication
        except ImportError:
            maybe_log("Failed to import applicationinsights: " + traceback.format_exc())
        else:
            handler = WSGIApplication(instr_key, handler)
            APPINSIGHT_CLIENT = handler.client
            # Ensure we will flush any remaining events when we exit
            on_exit(handler.client.flush)

    return env, handler

class handle_response(object):
    """A context manager for handling the response. This will ensure that
    exceptions in the handler are correctly reported, and the FastCGI request is
    properly terminated.
    """

    def __init__(self, stream, record, get_output, get_errors):
        self.stream = stream
        self.record = record
        self._get_output = get_output
        self._get_errors = get_errors
        self.error_message = ''
        self.fatal_errors = False
        self.physical_path = ''
        self.header_bytes = None
        self.sent_headers = False

    def __enter__(self):
        record = self.record
        record.params['wsgi.input'] = BytesIO(record.params['wsgi.input'])
        record.params['wsgi.version'] = (1, 0)
        record.params['wsgi.url_scheme'] = 'https' if record.params.get('HTTPS', '').lower() == 'on' else 'http'
        record.params['wsgi.multiprocess'] = True
        record.params['wsgi.multithread'] = False
        record.params['wsgi.run_once'] = False

        self.physical_path = record.params.get('APPL_PHYSICAL_PATH', os.path.dirname(__file__))

        if 'HTTP_X_ORIGINAL_URL' in record.params:
            # We've been re-written for shared FastCGI hosting, so send the
            # original URL as PATH_INFO.
            record.params['PATH_INFO'] = record.params['HTTP_X_ORIGINAL_URL']
            record.params['wsgi.path_info'] = record.params['wfastcgi.http_x_original_url']

        # PATH_INFO is not supposed to include the query parameters, so remove them
        record.params['PATH_INFO'] = record.params['PATH_INFO'].partition('?')[0]
        record.params['wsgi.path_info'] = record.params['wsgi.path_info'].partition(wsgi_encode('?'))[0]

        return self

    def __exit__(self, exc_type, exc_value, exc_tb):
        # Send any error message on FCGI_STDERR.
        if exc_type and exc_type is not _ExitException:
            error_msg = "%s:\n\n%s\n\nStdOut: %s\n\nStdErr: %s" % (
                self.error_message or 'Error occurred',
                ''.join(traceback.format_exception(exc_type, exc_value, exc_tb)),
                self._get_output(),
                self._get_errors(),
            )
            if not self.header_bytes or not self.sent_headers:
                self.header_bytes = wsgi_encode('Status: 500 Internal Server Error\r\n')
            self.send(FCGI_STDERR, wsgi_encode(error_msg))
            # Best effort at writing to the log. It's more important to
            # finish the response or the user will only see a generic 500
            # error.
            maybe_log(error_msg)

        # End the request. This has to run in both success and failure cases.
        self.send(FCGI_END_REQUEST, zero_bytes(8), streaming=False)
        
        # Remove the request from our global dict
        del _REQUESTS[self.record.req_id]
        
        # Suppress all exceptions unless requested
        return not self.fatal_errors

    @staticmethod
    def _decode_header(key, value):
        if not isinstance(key, str):
            key = wsgi_decode(key)
        if not isinstance(value, str):
            value = wsgi_decode(value)
        return key, value

    def start(self, status, headers, exc_info=None):
        """Starts sending the response. The response is ended when the context
        manager exits."""
        if exc_info:
            try:
                if self.sent_headers:
                    # We have to re-raise if we've already started sending data.
                    raise exception_with_traceback(exc_info[1], exc_info[2])
            finally:
                exc_info = None
        elif self.header_bytes:
            raise Exception('start_response has already been called')

        if not isinstance(status, str):
            status = wsgi_decode(status)
        header_text = 'Status: %s\r\n' % status
        if headers:
            header_text += ''.join('%s: %s\r\n' % handle_response._decode_header(*i) for i in headers)
        self.header_bytes = wsgi_encode(header_text + '\r\n')

        return lambda content: self.send(FCGI_STDOUT, content)

    def send(self, resp_type, content, streaming=True):
        '''Sends part of the response.'''
        if not self.sent_headers:
            if not self.header_bytes:
                raise Exception("start_response has not yet been called")

            self.sent_headers = True
            send_response(self.stream, self.record.req_id, FCGI_STDOUT, self.header_bytes)
            self.header_bytes = None

        return send_response(self.stream, self.record.req_id, resp_type, content, streaming)

_REQUESTS = {}

def main():
    initialized = False
    log('wfastcgi.py %s started' % __version__)
    log('Python version: %s' % sys.version)

    try:
        fcgi_stream = sys.stdin.detach() if sys.version_info[0] >= 3 else sys.stdin
        try:
            import msvcrt
            msvcrt.setmode(fcgi_stream.fileno(), os.O_BINARY)
        except ImportError:
            pass

        while True:
            record = read_fastcgi_record(fcgi_stream)
            if not record:
                continue

            errors = sys.stderr = sys.__stderr__ = record.params['wsgi.errors'] = StringIO()
            output = sys.stdout = sys.__stdout__ = StringIO()

            with handle_response(fcgi_stream, record, output.getvalue, errors.getvalue) as response:
                if not initialized:
                    log('wfastcgi.py %s initializing' % __version__)

                    os.chdir(response.physical_path)
                    sys.path[0] = '.'

                    # Initialization errors should be treated as fatal.
                    response.fatal_errors = True
                    response.error_message = 'Error occurred while reading WSGI handler'
                    env, handler = read_wsgi_handler(response.physical_path)

                    response.error_message = 'Error occurred starting file watcher'
                    start_file_watcher(response.physical_path, env.get('WSGI_RESTART_FILE_REGEX'))

                    # Enable debugging if possible. Default to local-only, but
                    # allow a web.config to override where we listen
                    ptvsd_secret = env.get('WSGI_PTVSD_SECRET')
                    if ptvsd_secret:
                        ptvsd_address = (env.get('WSGI_PTVSD_ADDRESS') or 'localhost:5678').split(':', 2)
                        try:
                            ptvsd_port = int(ptvsd_address[1])
                        except LookupError:
                            ptvsd_port = 5678
                        except ValueError:
                            log('"%s" is not a valid port number for debugging' % ptvsd_address[1])
                            ptvsd_port = 0

                        if ptvsd_address[0] and ptvsd_port:
                            try:
                                import ptvsd
                            except ImportError:
                                log('unable to import ptvsd to enable debugging')
                            else:
                                addr = ptvsd_address[0], ptvsd_port
                                ptvsd.enable_attach(secret=ptvsd_secret, address=addr)
                                log('debugging enabled on %s:%s' % addr)

                    response.error_message = ''
                    response.fatal_errors = False

                    log('wfastcgi.py %s initialized' % __version__)
                    initialized = True

                os.environ.update(env)

                # SCRIPT_NAME + PATH_INFO is supposed to be the full path
                # (http://www.python.org/dev/peps/pep-0333/) but by default
                # (http://msdn.microsoft.com/en-us/library/ms525840(v=vs.90).aspx)
                # IIS is sending us the full URL in PATH_INFO, so we need to
                # clear the script name here
                if 'AllowPathInfoForScriptMappings' not in os.environ:
                    record.params['SCRIPT_NAME'] = ''
                    record.params['wsgi.script_name'] = wsgi_encode('')

                # correct SCRIPT_NAME and PATH_INFO if we are told what our SCRIPT_NAME should be
                if 'SCRIPT_NAME' in os.environ and record.params['PATH_INFO'].lower().startswith(os.environ['SCRIPT_NAME'].lower()):
                    record.params['SCRIPT_NAME'] = os.environ['SCRIPT_NAME']
                    record.params['PATH_INFO'] = record.params['PATH_INFO'][len(record.params['SCRIPT_NAME']):]
                    record.params['wsgi.script_name'] = wsgi_encode(record.params['SCRIPT_NAME'])
                    record.params['wsgi.path_info'] = wsgi_encode(record.params['PATH_INFO'])

                # Send each part of the response to FCGI_STDOUT.
                # Exceptions raised in the handler will be logged by the context
                # manager and we will then wait for the next record.

                result = handler(record.params, response.start)
                try:
                    for part in result:
                        if part:
                            response.send(FCGI_STDOUT, part)
                finally:
                    if hasattr(result, 'close'):
                        result.close()
    except _ExitException:
        pass
    except Exception:
        maybe_log('Unhandled exception in wfastcgi.py: ' + traceback.format_exc())
    except BaseException:
        maybe_log('Unhandled exception in wfastcgi.py: ' + traceback.format_exc())
        raise
    finally:
        run_exit_tasks()
        maybe_log('wfastcgi.py %s closed' % __version__)

def _run_appcmd(args):
    from subprocess import check_call, CalledProcessError
    
    if len(sys.argv) > 1 and os.path.isfile(sys.argv[1]):
        appcmd = sys.argv[1:]
    else:
        appcmd = [os.path.join(os.getenv('SystemRoot'), 'system32', 'inetsrv', 'appcmd.exe')]

    if not os.path.isfile(appcmd[0]):
        print('IIS configuration tool appcmd.exe was not found at', appcmd, file=sys.stderr)
        return -1

    args = appcmd + args
    try:
        return check_call(args)
    except CalledProcessError as ex:
        print('''An error occurred running the command:

%r

Ensure your user has sufficient privileges and try again.''' % args, file=sys.stderr)
        return ex.returncode

def enable():
    executable = '"' + sys.executable + '"' if ' ' in sys.executable else sys.executable
    quoted_file = '"' + __file__ + '"' if ' ' in __file__ else __file__
    res = _run_appcmd([
        "set", "config", "/section:system.webServer/fastCGI",
        "/+[fullPath='" + executable + "', arguments='" + quoted_file + "', signalBeforeTerminateSeconds='30']"
    ])

    if res == 0:
        print('"%s|%s" can now be used as a FastCGI script processor' % (executable, quoted_file))
    return res

def disable():
    executable = '"' + sys.executable + '"' if ' ' in sys.executable else sys.executable
    quoted_file = '"' + __file__ + '"' if ' ' in __file__ else __file__    
    res = _run_appcmd([
        "set", "config", "/section:system.webServer/fastCGI",
        "/-[fullPath='" + executable + "', arguments='" + quoted_file + "', signalBeforeTerminateSeconds='30']"
    ])

    if res == 0:
        print('"%s|%s" is no longer registered for use with FastCGI' % (executable, quoted_file))
    return res

if __name__ == '__main__':
    main()
