You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

914 lines
32 KiB

5 months ago
  1. # Python Tools for Visual Studio
  2. # Copyright(c) Microsoft Corporation
  3. # All rights reserved.
  4. #
  5. # Licensed under the Apache License, Version 2.0 (the License); you may not use
  6. # this file except in compliance with the License. You may obtain a copy of the
  7. # License at http://www.apache.org/licenses/LICENSE-2.0
  8. #
  9. # THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS
  10. # OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY
  11. # IMPLIED WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE,
  12. # MERCHANTABLITY OR NON-INFRINGEMENT.
  13. #
  14. # See the Apache Version 2.0 License for specific language governing
  15. # permissions and limitations under the License.
  16. from __future__ import absolute_import, print_function, with_statement
  17. __author__ = "Microsoft Corporation <ptvshelp@microsoft.com>"
  18. __version__ = "3.0.0"
  19. import ctypes
  20. import datetime
  21. import os
  22. import re
  23. import struct
  24. import sys
  25. import traceback
  26. from xml.dom import minidom
  27. try:
  28. from cStringIO import StringIO
  29. BytesIO = StringIO
  30. except ImportError:
  31. from io import StringIO, BytesIO
  32. try:
  33. from thread import start_new_thread
  34. except ImportError:
  35. from _thread import start_new_thread
  36. if sys.version_info[0] == 3:
  37. def to_str(value):
  38. return value.decode(sys.getfilesystemencoding())
  39. else:
  40. def to_str(value):
  41. return value.encode(sys.getfilesystemencoding())
  42. # http://www.fastcgi.com/devkit/doc/fcgi-spec.html#S3
  43. FCGI_VERSION_1 = 1
  44. FCGI_HEADER_LEN = 8
  45. FCGI_BEGIN_REQUEST = 1
  46. FCGI_ABORT_REQUEST = 2
  47. FCGI_END_REQUEST = 3
  48. FCGI_PARAMS = 4
  49. FCGI_STDIN = 5
  50. FCGI_STDOUT = 6
  51. FCGI_STDERR = 7
  52. FCGI_DATA = 8
  53. FCGI_GET_VALUES = 9
  54. FCGI_GET_VALUES_RESULT = 10
  55. FCGI_UNKNOWN_TYPE = 11
  56. FCGI_MAXTYPE = FCGI_UNKNOWN_TYPE
  57. FCGI_NULL_REQUEST_ID = 0
  58. FCGI_KEEP_CONN = 1
  59. FCGI_RESPONDER = 1
  60. FCGI_AUTHORIZER = 2
  61. FCGI_FILTER = 3
  62. FCGI_REQUEST_COMPLETE = 0
  63. FCGI_CANT_MPX_CONN = 1
  64. FCGI_OVERLOADED = 2
  65. FCGI_UNKNOWN_ROLE = 3
  66. FCGI_MAX_CONNS = "FCGI_MAX_CONNS"
  67. FCGI_MAX_REQS = "FCGI_MAX_REQS"
  68. FCGI_MPXS_CONNS = "FCGI_MPXS_CONNS"
  69. class FastCgiRecord(object):
  70. """Represents a FastCgiRecord. Encapulates the type, role, flags. Holds
  71. onto the params which we will receive and update later."""
  72. def __init__(self, type, req_id, role, flags):
  73. self.type = type
  74. self.req_id = req_id
  75. self.role = role
  76. self.flags = flags
  77. self.params = {}
  78. def __repr__(self):
  79. return '<FastCgiRecord(%d, %d, %d, %d)>' % (self.type,
  80. self.req_id,
  81. self.role,
  82. self.flags)
  83. #typedef struct {
  84. # unsigned char version;
  85. # unsigned char type;
  86. # unsigned char requestIdB1;
  87. # unsigned char requestIdB0;
  88. # unsigned char contentLengthB1;
  89. # unsigned char contentLengthB0;
  90. # unsigned char paddingLength;
  91. # unsigned char reserved;
  92. # unsigned char contentData[contentLength];
  93. # unsigned char paddingData[paddingLength];
  94. #} FCGI_Record;
  95. class _ExitException(Exception):
  96. pass
  97. if sys.version_info[0] >= 3:
  98. # indexing into byte strings gives us an int, so
  99. # ord is unnecessary on Python 3
  100. def ord(x):
  101. return x
  102. def chr(x):
  103. return bytes((x, ))
  104. def wsgi_decode(x):
  105. return x.decode('iso-8859-1')
  106. def wsgi_encode(x):
  107. return x.encode('iso-8859-1')
  108. def fs_encode(x):
  109. return x
  110. def exception_with_traceback(exc_value, exc_tb):
  111. return exc_value.with_traceback(exc_tb)
  112. zero_bytes = bytes
  113. else:
  114. # Replace the builtin open with one that supports an encoding parameter
  115. from codecs import open
  116. def wsgi_decode(x):
  117. return x
  118. def wsgi_encode(x):
  119. return x
  120. def fs_encode(x):
  121. return x if isinstance(x, str) else x.encode(sys.getfilesystemencoding())
  122. def exception_with_traceback(exc_value, exc_tb):
  123. # x.with_traceback() is not supported on 2.x
  124. return exc_value
  125. bytes = str
  126. def zero_bytes(length):
  127. return '\x00' * length
  128. def read_fastcgi_record(stream):
  129. """reads the main fast cgi record"""
  130. data = stream.read(8) # read record
  131. if not data:
  132. # no more data, our other process must have died...
  133. raise _ExitException()
  134. fcgi_ver, reqtype, req_id, content_size, padding_len, _ = struct.unpack('>BBHHBB', data)
  135. content = stream.read(content_size) # read content
  136. stream.read(padding_len)
  137. if fcgi_ver != FCGI_VERSION_1:
  138. raise Exception('Unknown fastcgi version %s' % fcgi_ver)
  139. processor = REQUEST_PROCESSORS.get(reqtype)
  140. if processor is not None:
  141. return processor(stream, req_id, content)
  142. # unknown type requested, send response
  143. log('Unknown request type %s' % reqtype)
  144. send_response(stream, req_id, FCGI_UNKNOWN_TYPE, chr(reqtype) + zero_bytes(7))
  145. return None
  146. def read_fastcgi_begin_request(stream, req_id, content):
  147. """reads the begin request body and updates our _REQUESTS table to include
  148. the new request"""
  149. # typedef struct {
  150. # unsigned char roleB1;
  151. # unsigned char roleB0;
  152. # unsigned char flags;
  153. # unsigned char reserved[5];
  154. # } FCGI_BeginRequestBody;
  155. # TODO: Ignore request if it exists
  156. res = FastCgiRecord(
  157. FCGI_BEGIN_REQUEST,
  158. req_id,
  159. (ord(content[0]) << 8) | ord(content[1]), # role
  160. ord(content[2]), # flags
  161. )
  162. _REQUESTS[req_id] = res
  163. def read_encoded_int(content, offset):
  164. i = struct.unpack_from('>B', content, offset)[0]
  165. if i < 0x80:
  166. return offset + 1, i
  167. return offset + 4, struct.unpack_from('>I', content, offset)[0] & ~0x80000000
  168. def read_fastcgi_keyvalue_pairs(content, offset):
  169. """Reads a FastCGI key/value pair stream"""
  170. offset, name_len = read_encoded_int(content, offset)
  171. offset, value_len = read_encoded_int(content, offset)
  172. name = content[offset:(offset + name_len)]
  173. offset += name_len
  174. value = content[offset:(offset + value_len)]
  175. offset += value_len
  176. return offset, name, value
  177. def get_encoded_int(i):
  178. """Writes the length of a single name for a key or value in a key/value
  179. stream"""
  180. if i <= 0x7f:
  181. return struct.pack('>B', i)
  182. elif i < 0x80000000:
  183. return struct.pack('>I', i | 0x80000000)
  184. else:
  185. raise ValueError('cannot encode value %s (%x) because it is too large' % (i, i))
  186. def write_fastcgi_keyvalue_pairs(pairs):
  187. """Creates a FastCGI key/value stream and returns it as a byte string"""
  188. parts = []
  189. for raw_key, raw_value in pairs.items():
  190. key = wsgi_encode(raw_key)
  191. value = wsgi_encode(raw_value)
  192. parts.append(get_encoded_int(len(key)))
  193. parts.append(get_encoded_int(len(value)))
  194. parts.append(key)
  195. parts.append(value)
  196. return bytes().join(parts)
  197. # Keys in this set will be stored in the record without modification but with a
  198. # 'wsgi.' prefix. The original key will have the decoded version.
  199. # (Following mod_wsgi from http://wsgi.readthedocs.org/en/latest/python3.html)
  200. RAW_VALUE_NAMES = {
  201. 'SCRIPT_NAME' : 'wsgi.script_name',
  202. 'PATH_INFO' : 'wsgi.path_info',
  203. 'QUERY_STRING' : 'wsgi.query_string',
  204. 'HTTP_X_ORIGINAL_URL' : 'wfastcgi.http_x_original_url',
  205. }
  206. def read_fastcgi_params(stream, req_id, content):
  207. if not content:
  208. return None
  209. offset = 0
  210. res = _REQUESTS[req_id].params
  211. while offset < len(content):
  212. offset, name, value = read_fastcgi_keyvalue_pairs(content, offset)
  213. name = wsgi_decode(name)
  214. raw_name = RAW_VALUE_NAMES.get(name)
  215. if raw_name:
  216. res[raw_name] = value
  217. res[name] = wsgi_decode(value)
  218. def read_fastcgi_input(stream, req_id, content):
  219. """reads FastCGI std-in and stores it in wsgi.input passed in the
  220. wsgi environment array"""
  221. res = _REQUESTS[req_id].params
  222. if 'wsgi.input' not in res:
  223. res['wsgi.input'] = content
  224. else:
  225. res['wsgi.input'] += content
  226. if not content:
  227. # we've hit the end of the input stream, time to process input...
  228. return _REQUESTS[req_id]
  229. def read_fastcgi_data(stream, req_id, content):
  230. """reads FastCGI data stream and publishes it as wsgi.data"""
  231. res = _REQUESTS[req_id].params
  232. if 'wsgi.data' not in res:
  233. res['wsgi.data'] = content
  234. else:
  235. res['wsgi.data'] += content
  236. def read_fastcgi_abort_request(stream, req_id, content):
  237. """reads the wsgi abort request, which we ignore, we'll send the
  238. finish execution request anyway..."""
  239. pass
  240. def read_fastcgi_get_values(stream, req_id, content):
  241. """reads the fastcgi request to get parameter values, and immediately
  242. responds"""
  243. offset = 0
  244. request = {}
  245. while offset < len(content):
  246. offset, name, value = read_fastcgi_keyvalue_pairs(content, offset)
  247. request[name] = value
  248. response = {}
  249. if FCGI_MAX_CONNS in request:
  250. response[FCGI_MAX_CONNS] = '1'
  251. if FCGI_MAX_REQS in request:
  252. response[FCGI_MAX_REQS] = '1'
  253. if FCGI_MPXS_CONNS in request:
  254. response[FCGI_MPXS_CONNS] = '0'
  255. send_response(
  256. stream,
  257. req_id,
  258. FCGI_GET_VALUES_RESULT,
  259. write_fastcgi_keyvalue_pairs(response)
  260. )
  261. # Our request processors for different FastCGI protocol requests. Only those
  262. # requests that we receive are defined here.
  263. REQUEST_PROCESSORS = {
  264. FCGI_BEGIN_REQUEST : read_fastcgi_begin_request,
  265. FCGI_ABORT_REQUEST : read_fastcgi_abort_request,
  266. FCGI_PARAMS : read_fastcgi_params,
  267. FCGI_STDIN : read_fastcgi_input,
  268. FCGI_DATA : read_fastcgi_data,
  269. FCGI_GET_VALUES : read_fastcgi_get_values
  270. }
  271. APPINSIGHT_CLIENT = None
  272. def log(txt):
  273. """Logs messages to a log file if WSGI_LOG env var is defined."""
  274. if APPINSIGHT_CLIENT:
  275. try:
  276. APPINSIGHT_CLIENT.track_event(txt)
  277. except:
  278. pass
  279. log_file = os.environ.get('WSGI_LOG')
  280. if log_file:
  281. with open(log_file, 'a+', encoding='utf-8') as f:
  282. txt = txt.replace('\r\n', '\n')
  283. f.write('%s: %s%s' % (datetime.datetime.now(), txt, '' if txt.endswith('\n') else '\n'))
  284. def maybe_log(txt):
  285. """Logs messages to a log file if WSGI_LOG env var is defined, and does not
  286. raise exceptions if logging fails."""
  287. try:
  288. log(txt)
  289. except:
  290. pass
  291. def send_response(stream, req_id, resp_type, content, streaming=True):
  292. """sends a response w/ the given id, type, and content to the server.
  293. If the content is streaming then an empty record is sent at the end to
  294. terminate the stream"""
  295. if not isinstance(content, bytes):
  296. raise TypeError("content must be encoded before sending: %r" % content)
  297. offset = 0
  298. while True:
  299. len_remaining = max(min(len(content) - offset, 0xFFFF), 0)
  300. data = struct.pack(
  301. '>BBHHBB',
  302. FCGI_VERSION_1, # version
  303. resp_type, # type
  304. req_id, # requestIdB1:B0
  305. len_remaining, # contentLengthB1:B0
  306. 0, # paddingLength
  307. 0, # reserved
  308. ) + content[offset:(offset + len_remaining)]
  309. offset += len_remaining
  310. os.write(stream.fileno(), data)
  311. if len_remaining == 0 or not streaming:
  312. break
  313. stream.flush()
  314. def get_environment(dir):
  315. web_config = os.path.join(dir, 'Web.config')
  316. if not os.path.exists(web_config):
  317. return {}
  318. d = {}
  319. doc = minidom.parse(web_config)
  320. config = doc.getElementsByTagName('configuration')
  321. for configSection in config:
  322. appSettings = configSection.getElementsByTagName('appSettings')
  323. for appSettingsSection in appSettings:
  324. values = appSettingsSection.getElementsByTagName('add')
  325. for curAdd in values:
  326. key = curAdd.getAttribute('key')
  327. value = curAdd.getAttribute('value')
  328. if key and value is not None:
  329. d[key.strip()] = value
  330. return d
  331. ReadDirectoryChangesW = ctypes.windll.kernel32.ReadDirectoryChangesW
  332. ReadDirectoryChangesW.restype = ctypes.c_uint32
  333. ReadDirectoryChangesW.argtypes = [
  334. ctypes.c_void_p, # HANDLE hDirectory
  335. ctypes.c_void_p, # LPVOID lpBuffer
  336. ctypes.c_uint32, # DWORD nBufferLength
  337. ctypes.c_uint32, # BOOL bWatchSubtree
  338. ctypes.c_uint32, # DWORD dwNotifyFilter
  339. ctypes.POINTER(ctypes.c_uint32), # LPDWORD lpBytesReturned
  340. ctypes.c_void_p, # LPOVERLAPPED lpOverlapped
  341. ctypes.c_void_p # LPOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine
  342. ]
  343. try:
  344. from _winapi import (CreateFile, CloseHandle, GetLastError, ExitProcess,
  345. WaitForSingleObject, INFINITE, OPEN_EXISTING)
  346. except ImportError:
  347. CreateFile = ctypes.windll.kernel32.CreateFileW
  348. CreateFile.restype = ctypes.c_void_p
  349. CreateFile.argtypes = [
  350. ctypes.c_wchar_p, # lpFilename
  351. ctypes.c_uint32, # dwDesiredAccess
  352. ctypes.c_uint32, # dwShareMode
  353. ctypes.c_void_p, # LPSECURITY_ATTRIBUTES,
  354. ctypes.c_uint32, # dwCreationDisposition,
  355. ctypes.c_uint32, # dwFlagsAndAttributes,
  356. ctypes.c_void_p # hTemplateFile
  357. ]
  358. CloseHandle = ctypes.windll.kernel32.CloseHandle
  359. CloseHandle.argtypes = [ctypes.c_void_p]
  360. GetLastError = ctypes.windll.kernel32.GetLastError
  361. GetLastError.restype = ctypes.c_uint32
  362. ExitProcess = ctypes.windll.kernel32.ExitProcess
  363. ExitProcess.restype = ctypes.c_void_p
  364. ExitProcess.argtypes = [ctypes.c_uint32]
  365. WaitForSingleObject = ctypes.windll.kernel32.WaitForSingleObject
  366. WaitForSingleObject.argtypes = [ctypes.c_void_p, ctypes.c_uint32]
  367. WaitForSingleObject.restype = ctypes.c_uint32
  368. OPEN_EXISTING = 3
  369. INFINITE = -1
  370. FILE_LIST_DIRECTORY = 1
  371. FILE_SHARE_READ = 0x00000001
  372. FILE_SHARE_WRITE = 0x00000002
  373. FILE_SHARE_DELETE = 0x00000004
  374. FILE_FLAG_BACKUP_SEMANTICS = 0x02000000
  375. MAX_PATH = 260
  376. FILE_NOTIFY_CHANGE_LAST_WRITE = 0x10
  377. ERROR_NOTIFY_ENUM_DIR = 1022
  378. INVALID_HANDLE_VALUE = 0xFFFFFFFF
  379. class FILE_NOTIFY_INFORMATION(ctypes.Structure):
  380. _fields_ = [('NextEntryOffset', ctypes.c_uint32),
  381. ('Action', ctypes.c_uint32),
  382. ('FileNameLength', ctypes.c_uint32),
  383. ('Filename', ctypes.c_wchar)]
  384. _ON_EXIT_TASKS = None
  385. def run_exit_tasks():
  386. global _ON_EXIT_TASKS
  387. maybe_log("Running on_exit tasks")
  388. while _ON_EXIT_TASKS:
  389. tasks, _ON_EXIT_TASKS = _ON_EXIT_TASKS, []
  390. for t in tasks:
  391. try:
  392. t()
  393. except Exception:
  394. maybe_log("Error in exit task: " + traceback.format_exc())
  395. def on_exit(task):
  396. global _ON_EXIT_TASKS
  397. if _ON_EXIT_TASKS is None:
  398. _ON_EXIT_TASKS = tasks = []
  399. try:
  400. evt = int(os.getenv('_FCGI_SHUTDOWN_EVENT_'))
  401. except (TypeError, ValueError):
  402. maybe_log("Could not wait on event %s" % os.getenv('_FCGI_SHUTDOWN_EVENT_'))
  403. else:
  404. def _wait_for_exit():
  405. WaitForSingleObject(evt, INFINITE)
  406. run_exit_tasks()
  407. ExitProcess(0)
  408. start_new_thread(_wait_for_exit, ())
  409. _ON_EXIT_TASKS.append(task)
  410. def start_file_watcher(path, restart_regex):
  411. if restart_regex is None:
  412. restart_regex = ".*((\\.py)|(\\.config))$"
  413. elif not restart_regex:
  414. # restart regex set to empty string, no restart behavior
  415. return
  416. def enum_changes(path):
  417. """Returns a generator that blocks until a change occurs, then yields
  418. the filename of the changed file.
  419. Yields an empty string and stops if the buffer overruns, indicating that
  420. too many files were changed."""
  421. buffer = ctypes.create_string_buffer(32 * 1024)
  422. bytes_ret = ctypes.c_uint32()
  423. try:
  424. the_dir = CreateFile(
  425. path,
  426. FILE_LIST_DIRECTORY,
  427. FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
  428. 0,
  429. OPEN_EXISTING,
  430. FILE_FLAG_BACKUP_SEMANTICS,
  431. 0,
  432. )
  433. except OSError:
  434. maybe_log("Unable to create watcher")
  435. return
  436. if not the_dir or the_dir == INVALID_HANDLE_VALUE:
  437. maybe_log("Unable to create watcher")
  438. return
  439. while True:
  440. ret_code = ReadDirectoryChangesW(
  441. the_dir,
  442. buffer,
  443. ctypes.sizeof(buffer),
  444. True,
  445. FILE_NOTIFY_CHANGE_LAST_WRITE,
  446. ctypes.byref(bytes_ret),
  447. None,
  448. None,
  449. )
  450. if ret_code:
  451. cur_pointer = ctypes.addressof(buffer)
  452. while True:
  453. fni = ctypes.cast(cur_pointer, ctypes.POINTER(FILE_NOTIFY_INFORMATION))
  454. # FileName is not null-terminated, so specifying length is mandatory.
  455. filename = ctypes.wstring_at(cur_pointer + 12, fni.contents.FileNameLength // 2)
  456. yield filename
  457. if fni.contents.NextEntryOffset == 0:
  458. break
  459. cur_pointer = cur_pointer + fni.contents.NextEntryOffset
  460. elif GetLastError() == ERROR_NOTIFY_ENUM_DIR:
  461. CloseHandle(the_dir)
  462. yield ''
  463. return
  464. else:
  465. CloseHandle(the_dir)
  466. return
  467. log('wfastcgi.py will restart when files in %s are changed: %s' % (path, restart_regex))
  468. def watcher(path, restart):
  469. for filename in enum_changes(path):
  470. if not filename:
  471. log('wfastcgi.py exiting because the buffer was full')
  472. run_exit_tasks()
  473. ExitProcess(0)
  474. elif restart.match(filename):
  475. log('wfastcgi.py exiting because %s has changed, matching %s' % (filename, restart_regex))
  476. # we call ExitProcess directly to quickly shutdown the whole process
  477. # because sys.exit(0) won't have an effect on the main thread.
  478. run_exit_tasks()
  479. ExitProcess(0)
  480. restart = re.compile(restart_regex)
  481. start_new_thread(watcher, (path, restart))
  482. def get_wsgi_handler(handler_name):
  483. if not handler_name:
  484. raise Exception('WSGI_HANDLER env var must be set')
  485. if not isinstance(handler_name, str):
  486. handler_name = to_str(handler_name)
  487. module_name, _, callable_name = handler_name.rpartition('.')
  488. should_call = callable_name.endswith('()')
  489. callable_name = callable_name[:-2] if should_call else callable_name
  490. name_list = [(callable_name, should_call)]
  491. handler = None
  492. last_tb = ''
  493. while module_name:
  494. try:
  495. handler = __import__(module_name, fromlist=[name_list[0][0]])
  496. last_tb = ''
  497. for name, should_call in name_list:
  498. handler = getattr(handler, name)
  499. if should_call:
  500. handler = handler()
  501. break
  502. except ImportError:
  503. module_name, _, callable_name = module_name.rpartition('.')
  504. should_call = callable_name.endswith('()')
  505. callable_name = callable_name[:-2] if should_call else callable_name
  506. name_list.insert(0, (callable_name, should_call))
  507. handler = None
  508. last_tb = ': ' + traceback.format_exc()
  509. if handler is None:
  510. raise ValueError('"%s" could not be imported%s' % (handler_name, last_tb))
  511. return handler
  512. def read_wsgi_handler(physical_path):
  513. global APPINSIGHT_CLIENT
  514. env = get_environment(physical_path)
  515. os.environ.update(env)
  516. for path in (v for k, v in env.items() if k.lower() == 'pythonpath'):
  517. # Expand environment variables manually.
  518. expanded_path = re.sub(
  519. '%(\\w+?)%',
  520. lambda m: os.getenv(m.group(1), ''),
  521. path
  522. )
  523. sys.path.extend(fs_encode(p) for p in expanded_path.split(';') if p)
  524. handler = get_wsgi_handler(os.getenv("WSGI_HANDLER"))
  525. instr_key = os.getenv("APPINSIGHTS_INSTRUMENTATIONKEY")
  526. if instr_key:
  527. try:
  528. # Attempt the import after updating sys.path - sites must
  529. # include applicationinsights themselves.
  530. from applicationinsights.requests import WSGIApplication
  531. except ImportError:
  532. maybe_log("Failed to import applicationinsights: " + traceback.format_exc())
  533. else:
  534. handler = WSGIApplication(instr_key, handler)
  535. APPINSIGHT_CLIENT = handler.client
  536. # Ensure we will flush any remaining events when we exit
  537. on_exit(handler.client.flush)
  538. return env, handler
  539. class handle_response(object):
  540. """A context manager for handling the response. This will ensure that
  541. exceptions in the handler are correctly reported, and the FastCGI request is
  542. properly terminated.
  543. """
  544. def __init__(self, stream, record, get_output, get_errors):
  545. self.stream = stream
  546. self.record = record
  547. self._get_output = get_output
  548. self._get_errors = get_errors
  549. self.error_message = ''
  550. self.fatal_errors = False
  551. self.physical_path = ''
  552. self.header_bytes = None
  553. self.sent_headers = False
  554. def __enter__(self):
  555. record = self.record
  556. record.params['wsgi.input'] = BytesIO(record.params['wsgi.input'])
  557. record.params['wsgi.version'] = (1, 0)
  558. record.params['wsgi.url_scheme'] = 'https' if record.params.get('HTTPS', '').lower() == 'on' else 'http'
  559. record.params['wsgi.multiprocess'] = True
  560. record.params['wsgi.multithread'] = False
  561. record.params['wsgi.run_once'] = False
  562. self.physical_path = record.params.get('APPL_PHYSICAL_PATH', os.path.dirname(__file__))
  563. if 'HTTP_X_ORIGINAL_URL' in record.params:
  564. # We've been re-written for shared FastCGI hosting, so send the
  565. # original URL as PATH_INFO.
  566. record.params['PATH_INFO'] = record.params['HTTP_X_ORIGINAL_URL']
  567. record.params['wsgi.path_info'] = record.params['wfastcgi.http_x_original_url']
  568. # PATH_INFO is not supposed to include the query parameters, so remove them
  569. record.params['PATH_INFO'] = record.params['PATH_INFO'].partition('?')[0]
  570. record.params['wsgi.path_info'] = record.params['wsgi.path_info'].partition(wsgi_encode('?'))[0]
  571. return self
  572. def __exit__(self, exc_type, exc_value, exc_tb):
  573. # Send any error message on FCGI_STDERR.
  574. if exc_type and exc_type is not _ExitException:
  575. error_msg = "%s:\n\n%s\n\nStdOut: %s\n\nStdErr: %s" % (
  576. self.error_message or 'Error occurred',
  577. ''.join(traceback.format_exception(exc_type, exc_value, exc_tb)),
  578. self._get_output(),
  579. self._get_errors(),
  580. )
  581. if not self.header_bytes or not self.sent_headers:
  582. self.header_bytes = wsgi_encode('Status: 500 Internal Server Error\r\n')
  583. self.send(FCGI_STDERR, wsgi_encode(error_msg))
  584. # Best effort at writing to the log. It's more important to
  585. # finish the response or the user will only see a generic 500
  586. # error.
  587. maybe_log(error_msg)
  588. # End the request. This has to run in both success and failure cases.
  589. self.send(FCGI_END_REQUEST, zero_bytes(8), streaming=False)
  590. # Remove the request from our global dict
  591. del _REQUESTS[self.record.req_id]
  592. # Suppress all exceptions unless requested
  593. return not self.fatal_errors
  594. @staticmethod
  595. def _decode_header(key, value):
  596. if not isinstance(key, str):
  597. key = wsgi_decode(key)
  598. if not isinstance(value, str):
  599. value = wsgi_decode(value)
  600. return key, value
  601. def start(self, status, headers, exc_info=None):
  602. """Starts sending the response. The response is ended when the context
  603. manager exits."""
  604. if exc_info:
  605. try:
  606. if self.sent_headers:
  607. # We have to re-raise if we've already started sending data.
  608. raise exception_with_traceback(exc_info[1], exc_info[2])
  609. finally:
  610. exc_info = None
  611. elif self.header_bytes:
  612. raise Exception('start_response has already been called')
  613. if not isinstance(status, str):
  614. status = wsgi_decode(status)
  615. header_text = 'Status: %s\r\n' % status
  616. if headers:
  617. header_text += ''.join('%s: %s\r\n' % handle_response._decode_header(*i) for i in headers)
  618. self.header_bytes = wsgi_encode(header_text + '\r\n')
  619. return lambda content: self.send(FCGI_STDOUT, content)
  620. def send(self, resp_type, content, streaming=True):
  621. '''Sends part of the response.'''
  622. if not self.sent_headers:
  623. if not self.header_bytes:
  624. raise Exception("start_response has not yet been called")
  625. self.sent_headers = True
  626. send_response(self.stream, self.record.req_id, FCGI_STDOUT, self.header_bytes)
  627. self.header_bytes = None
  628. return send_response(self.stream, self.record.req_id, resp_type, content, streaming)
  629. _REQUESTS = {}
  630. def main():
  631. initialized = False
  632. log('wfastcgi.py %s started' % __version__)
  633. log('Python version: %s' % sys.version)
  634. try:
  635. fcgi_stream = sys.stdin.detach() if sys.version_info[0] >= 3 else sys.stdin
  636. try:
  637. import msvcrt
  638. msvcrt.setmode(fcgi_stream.fileno(), os.O_BINARY)
  639. except ImportError:
  640. pass
  641. while True:
  642. record = read_fastcgi_record(fcgi_stream)
  643. if not record:
  644. continue
  645. errors = sys.stderr = sys.__stderr__ = record.params['wsgi.errors'] = StringIO()
  646. output = sys.stdout = sys.__stdout__ = StringIO()
  647. with handle_response(fcgi_stream, record, output.getvalue, errors.getvalue) as response:
  648. if not initialized:
  649. log('wfastcgi.py %s initializing' % __version__)
  650. os.chdir(response.physical_path)
  651. sys.path[0] = '.'
  652. # Initialization errors should be treated as fatal.
  653. response.fatal_errors = True
  654. response.error_message = 'Error occurred while reading WSGI handler'
  655. env, handler = read_wsgi_handler(response.physical_path)
  656. response.error_message = 'Error occurred starting file watcher'
  657. start_file_watcher(response.physical_path, env.get('WSGI_RESTART_FILE_REGEX'))
  658. # Enable debugging if possible. Default to local-only, but
  659. # allow a web.config to override where we listen
  660. ptvsd_secret = env.get('WSGI_PTVSD_SECRET')
  661. if ptvsd_secret:
  662. ptvsd_address = (env.get('WSGI_PTVSD_ADDRESS') or 'localhost:5678').split(':', 2)
  663. try:
  664. ptvsd_port = int(ptvsd_address[1])
  665. except LookupError:
  666. ptvsd_port = 5678
  667. except ValueError:
  668. log('"%s" is not a valid port number for debugging' % ptvsd_address[1])
  669. ptvsd_port = 0
  670. if ptvsd_address[0] and ptvsd_port:
  671. try:
  672. import ptvsd
  673. except ImportError:
  674. log('unable to import ptvsd to enable debugging')
  675. else:
  676. addr = ptvsd_address[0], ptvsd_port
  677. ptvsd.enable_attach(secret=ptvsd_secret, address=addr)
  678. log('debugging enabled on %s:%s' % addr)
  679. response.error_message = ''
  680. response.fatal_errors = False
  681. log('wfastcgi.py %s initialized' % __version__)
  682. initialized = True
  683. os.environ.update(env)
  684. # SCRIPT_NAME + PATH_INFO is supposed to be the full path
  685. # (http://www.python.org/dev/peps/pep-0333/) but by default
  686. # (http://msdn.microsoft.com/en-us/library/ms525840(v=vs.90).aspx)
  687. # IIS is sending us the full URL in PATH_INFO, so we need to
  688. # clear the script name here
  689. if 'AllowPathInfoForScriptMappings' not in os.environ:
  690. record.params['SCRIPT_NAME'] = ''
  691. record.params['wsgi.script_name'] = wsgi_encode('')
  692. # correct SCRIPT_NAME and PATH_INFO if we are told what our SCRIPT_NAME should be
  693. if 'SCRIPT_NAME' in os.environ and record.params['PATH_INFO'].lower().startswith(os.environ['SCRIPT_NAME'].lower()):
  694. record.params['SCRIPT_NAME'] = os.environ['SCRIPT_NAME']
  695. record.params['PATH_INFO'] = record.params['PATH_INFO'][len(record.params['SCRIPT_NAME']):]
  696. record.params['wsgi.script_name'] = wsgi_encode(record.params['SCRIPT_NAME'])
  697. record.params['wsgi.path_info'] = wsgi_encode(record.params['PATH_INFO'])
  698. # Send each part of the response to FCGI_STDOUT.
  699. # Exceptions raised in the handler will be logged by the context
  700. # manager and we will then wait for the next record.
  701. result = handler(record.params, response.start)
  702. try:
  703. for part in result:
  704. if part:
  705. response.send(FCGI_STDOUT, part)
  706. finally:
  707. if hasattr(result, 'close'):
  708. result.close()
  709. except _ExitException:
  710. pass
  711. except Exception:
  712. maybe_log('Unhandled exception in wfastcgi.py: ' + traceback.format_exc())
  713. except BaseException:
  714. maybe_log('Unhandled exception in wfastcgi.py: ' + traceback.format_exc())
  715. raise
  716. finally:
  717. run_exit_tasks()
  718. maybe_log('wfastcgi.py %s closed' % __version__)
  719. def _run_appcmd(args):
  720. from subprocess import check_call, CalledProcessError
  721. if len(sys.argv) > 1 and os.path.isfile(sys.argv[1]):
  722. appcmd = sys.argv[1:]
  723. else:
  724. appcmd = [os.path.join(os.getenv('SystemRoot'), 'system32', 'inetsrv', 'appcmd.exe')]
  725. if not os.path.isfile(appcmd[0]):
  726. print('IIS configuration tool appcmd.exe was not found at', appcmd, file=sys.stderr)
  727. return -1
  728. args = appcmd + args
  729. try:
  730. return check_call(args)
  731. except CalledProcessError as ex:
  732. print('''An error occurred running the command:
  733. %r
  734. Ensure your user has sufficient privileges and try again.''' % args, file=sys.stderr)
  735. return ex.returncode
  736. def enable():
  737. executable = '"' + sys.executable + '"' if ' ' in sys.executable else sys.executable
  738. quoted_file = '"' + __file__ + '"' if ' ' in __file__ else __file__
  739. res = _run_appcmd([
  740. "set", "config", "/section:system.webServer/fastCGI",
  741. "/+[fullPath='" + executable + "', arguments='" + quoted_file + "', signalBeforeTerminateSeconds='30']"
  742. ])
  743. if res == 0:
  744. print('"%s|%s" can now be used as a FastCGI script processor' % (executable, quoted_file))
  745. return res
  746. def disable():
  747. executable = '"' + sys.executable + '"' if ' ' in sys.executable else sys.executable
  748. quoted_file = '"' + __file__ + '"' if ' ' in __file__ else __file__
  749. res = _run_appcmd([
  750. "set", "config", "/section:system.webServer/fastCGI",
  751. "/-[fullPath='" + executable + "', arguments='" + quoted_file + "', signalBeforeTerminateSeconds='30']"
  752. ])
  753. if res == 0:
  754. print('"%s|%s" is no longer registered for use with FastCGI' % (executable, quoted_file))
  755. return res
  756. if __name__ == '__main__':
  757. main()