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