123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914 |
- # 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()
|