├── .gitignore ├── LICENSE.txt ├── MANIFEST.in ├── README.rst ├── django_windows_tools ├── __init__.py ├── management │ ├── __init__.py │ └── commands │ │ ├── __init__.py │ │ ├── winfcgi.py │ │ ├── winfcgi_install.py │ │ └── winservice_install.py ├── models.py ├── service.py ├── static │ └── web.config ├── templates │ └── windows_tools │ │ ├── iis │ │ └── web.config │ │ └── service │ │ ├── service.ini │ │ └── service.py ├── tests.py └── views.py ├── docs ├── Makefile ├── conf.py ├── index.rst ├── make.bat └── quickstart.rst ├── requirements.txt └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[co] 2 | 3 | # Packages 4 | *.egg 5 | *.egg-info 6 | dist 7 | build 8 | eggs 9 | parts 10 | bin 11 | var 12 | sdist 13 | develop-eggs 14 | .installed.cfg 15 | 16 | # Installer logs 17 | pip-log.txt 18 | 19 | # Unit test / coverage reports 20 | .coverage 21 | .tox 22 | 23 | #Translations 24 | *.mo 25 | 26 | #Mr Developer 27 | .mr.developer.cfg 28 | 29 | #sphinx 30 | _build 31 | 32 | #pycharm 33 | .idea/ 34 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012 Openance SARL 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions 6 | are met: 7 | 1. Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | 2. Redistributions in binary form must reproduce the above copyright 10 | notice, this list of conditions and the following disclaimer in the 11 | documentation and/or other materials provided with the distribution. 12 | 13 | THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND 14 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 15 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 16 | ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE 17 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 18 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS 19 | OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 20 | HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 21 | LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY 22 | OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 23 | SUCH DAMAGE. -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | recursive-include django_windows_tools/templates * -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Django-windows-tools 2 | ==================== 3 | 4 | .. warning:: 5 | This package is no longer maintained - use it at your own risk. 6 | The package has been tested against versions of django, Celery and IIS that 7 | are outdated today, and may not work with newer versions. 8 | 9 | 10 | `django-windows-tools` is a Django application providing management commands 11 | to host Django projects in Windows environments. 12 | 13 | It provides management commands allowing to: 14 | 15 | - host a Django application behind IIS as a FastCGI application (requires 16 | IIS 7 or newer) 17 | - run Celery and Celery Beat background processes as a Windows Service (does 18 | not work with current Celery versions, see compatibility notes below) 19 | 20 | It requires Django >= 1.4 and pywin32. 21 | 22 | Compatibility notes 23 | ------------------- 24 | 25 | - `django-windows-tools` 0.1.3 is the last version to work with Django <= 1.7 26 | - the ``win_fcgi`` part is known to work with Django versions up to 1.11 and 27 | Python 3.7 (django-windows-tools 0.2 needed), and works with IIS up to 28 | version 10 29 | - installing services with newer versions of Django and Python 3 is not tested 30 | and may not work (see also the open issues) 31 | - newer Celery versions cannot be installed as a service as described (see 32 | #19 for running Celery as a scheduled task instead) 33 | 34 | The following gives a Quick overview of the project. For more information, please 35 | read the `Project documentation `_. 36 | 37 | Installation and Configuration 38 | ############################## 39 | 40 | You install the application with the command: :: 41 | 42 | pip install django-windows-tools 43 | 44 | Enable the ``django_windows_tools`` application to be able to use the management commands. Add the app to 45 | the project's list in ``settings.py``: :: 46 | 47 | INSTALLED_APPS += ( 48 | 'django_windows_tools', 49 | ) 50 | 51 | FastCGI Configuration 52 | ##################### 53 | 54 | Pre-requisites 55 | -------------- 56 | 57 | On the host machine, you need to have : 58 | 59 | - IIS 7 or better installed and running. 60 | - The ``CGI`` module installed. 61 | 62 | To host your Django project under ``IIS`` with the binding ``www.mydjangoapp.com``, 63 | you need first to collect your static files with the command: :: 64 | 65 | D:\sites\mydjangoapp> python manage.py collectstatic 66 | 67 | And then run the following command with Administrator privileges : :: 68 | 69 | D:\sites\mydjangoapp> python manage.py winfcgi_install --binding=http://www.mydjangoapp.com:80 70 | 71 | The command will do the following: 72 | 73 | - Create the FastCGI application to serve your Django application dynamic content. 74 | - Create a site name ``mydjangoapp`` with the ``www.mydjangoapp.com`` binding pointing to the root of your project. 75 | - Install a ``web.config`` file in the root of the project that handles the 76 | redirection of requests to the Django application. 77 | - Create if needed a virtual directory to handle the serving of your static files through ``IIS``. 78 | 79 | To remove the site created with the preceding command, type: :: 80 | 81 | D:\sites\mydjangoapp> python manage.py winfcgi_install --delete 82 | 83 | the ``winfcgi_install`` command provides numerous options. To list them, type: :: 84 | 85 | D:\sites\mydjangoapp> python manage.py help winfcgi_install 86 | 87 | More information on how the configuration is done is provided in 88 | this `Blog post `_. 89 | 90 | Running Celery or other Background commands as a Windows Service 91 | ################################################################ 92 | 93 | With the application installed, on the root of your project, type the following command: :: 94 | 95 | D:\sites\mydjangoapp> python manage.py winservice_install 96 | 97 | It will create two files, ``service.py`` and ``service.ini`` in the 98 | root directory of your project. The first one will help you install, 99 | run and remove the Windows Service. Ther later one contain the list of 100 | the management commands that will be run by the Windows Service. 101 | 102 | Configuration 103 | ------------- 104 | 105 | The ``service.ini`` is a configuration file that looks like the following: :: 106 | 107 | [services] 108 | # Services to be run on all machines 109 | run=celeryd 110 | clean=d:\logs\celery.log 111 | 112 | [BEATSERVER] 113 | # There should be only one machine with the celerybeat service 114 | run=celeryd celerybeat 115 | clean=d:\logs\celerybeat.pid;d:\logs\beat.log;d:\logs\celery.log 116 | 117 | [celeryd] 118 | command=celeryd 119 | parameters=-f d:\logs\celery.log -l info 120 | 121 | [celerybeat] 122 | command=celerybeat 123 | parameters=-f d:\logs\beat.log -l info --pidfile=d:\logs\celerybeat.pid 124 | 125 | [runserver] 126 | # Runs the debug server and listen on port 8000 127 | # This one is just an example to show that any manage command can be used 128 | command=runserver 129 | parameters=--noreload --insecure 0.0.0.0:8000 130 | 131 | [log] 132 | filename=d:\logs\service.log 133 | level=INFO 134 | 135 | The ``services`` section contains : 136 | 137 | - The list of background commands to run in the ``run`` directive. 138 | - The list of files to delete when refreshed or stopped in the ``clean`` directive. 139 | 140 | You can have several ``services`` sections in the same configuration file 141 | for different host servers. The Windows Service will try to find the section which name 142 | matches the name of the current server and will fallback to the ``services`` section if it 143 | does not find it. This allows you to deploy the same configuration file on serveral 144 | machines but only have one machine run the celery beat background process. In the preceding 145 | configuration, only the server named ``BEATSERVER`` will run the ``celerybeat`` command. 146 | The other ones will only run the ``celeryd`` command. 147 | 148 | For each command name specified in the ``run`` directive, there must be a matching configuration 149 | section. The section contains two directives: 150 | 151 | - ``command`` specifies the ``manage.py`` command to run. 152 | - ``parameters`` specifies the parameters to the command. 153 | 154 | In the previous configuration file, the ``celeryd`` configuration will spawn a process 155 | that will run the same command as : :: 156 | 157 | D:\sites\mydjangoapp> python manage.py celeryd -f d:\logs\celery.log -l info 158 | 159 | Lastly, the ``log`` section defines the log level and the the log destination file 160 | for the Windows Service. 161 | 162 | Installation and start 163 | ---------------------- 164 | 165 | The windows service is installed with the following command (run with 166 | Administrator privileges) : :: 167 | 168 | D:\sites\mydjangoapp> python service.py --startup=auto install 169 | 170 | It is started and stopped with the commands: :: 171 | 172 | D:\sites\mydjangoapp> python service.py start 173 | D:\sites\mydjangoapp> python service.py stop 174 | 175 | It can be removed with the following commands: :: 176 | 177 | D:\sites\mydjangoapp> python service.py remove 178 | 179 | The Windows Service monitor changes to the ``service.ini`` configuration 180 | file. In case it is modified, the service does the following: 181 | 182 | - Stop the background processes. 183 | - Reread the configuration file. 184 | - Start the background processes. 185 | 186 | Customization 187 | ------------- 188 | 189 | The ``winservice_install`` management command provides several options 190 | allowing to customize the name of the web service or of the script name. 191 | To obtain information about them, type: :: 192 | 193 | D:\sites\mydjangoapp> python manage.py help winservice_install 194 | -------------------------------------------------------------------------------- /django_windows_tools/__init__.py: -------------------------------------------------------------------------------- 1 | '''Helper Management commands to host Django applications on Windows Servers. 2 | 3 | ''' 4 | 5 | 6 | __version_info__ = { 7 | 'major': 0, 8 | 'minor': 2, 9 | 'micro': 1, 10 | 'releaselevel': 'final', 11 | 'serial': 1 12 | } 13 | 14 | def get_version(): 15 | """ 16 | Return the formatted version information 17 | """ 18 | vers = ["%(major)i.%(minor)i" % __version_info__, ] 19 | 20 | if __version_info__['micro']: 21 | vers.append(".%(micro)i" % __version_info__) 22 | if __version_info__['releaselevel'] != 'final': 23 | vers.append('%(releaselevel)s%(serial)i' % __version_info__) 24 | return ''.join(vers) 25 | 26 | __version__ = get_version() 27 | -------------------------------------------------------------------------------- /django_windows_tools/management/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/antoinemartin/django-windows-tools/86fcc4d0ff3c49f55e574415bda5f3b5018cb6e1/django_windows_tools/management/__init__.py -------------------------------------------------------------------------------- /django_windows_tools/management/commands/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/antoinemartin/django-windows-tools/86fcc4d0ff3c49f55e574415bda5f3b5018cb6e1/django_windows_tools/management/commands/__init__.py -------------------------------------------------------------------------------- /django_windows_tools/management/commands/winfcgi.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | # FastCGI-to-WSGI bridge for files/pipes transport (not socket) 4 | # 5 | # Copyright (c) 2002, 2003, 2005, 2006 Allan Saddi 6 | # Copyright (c) 2011 Ruslan Keba 7 | # Copyright (c) 2012 Antoine Martin 8 | # All rights reserved. 9 | # 10 | # Redistribution and use in source and binary forms, with or without 11 | # modification, are permitted provided that the following conditions 12 | # are met: 13 | # 1. Redistributions of source code must retain the above copyright 14 | # notice, this list of conditions and the following disclaimer. 15 | # 2. Redistributions in binary form must reproduce the above copyright 16 | # notice, this list of conditions and the following disclaimer in the 17 | # documentation and/or other materials provided with the distribution. 18 | # 19 | # THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND 20 | # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 21 | # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 22 | # ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE 23 | # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 24 | # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS 25 | # OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 26 | # HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 27 | # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY 28 | # OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 29 | # SUCH DAMAGE. 30 | # 31 | 32 | __author__ = 'Allan Saddi , Ruslan Keba , Antoine Martin ' 33 | 34 | import msvcrt 35 | import struct 36 | import os 37 | import os.path 38 | import logging 39 | import sys 40 | import datetime 41 | from optparse import OptionParser 42 | 43 | if sys.version_info >= (3,): 44 | long_int = int 45 | bytes_type = bytes 46 | import urllib.parse as url_parse 47 | 48 | def char_to_int(value): 49 | return int(value) 50 | 51 | def int_to_char(value): 52 | return bytes([value]) 53 | 54 | def make_bytes(content): 55 | return bytes(content, FCGI_CONTENT_ENCODING) if type(content) is str else content 56 | else: 57 | long_int = long 58 | bytes_type = str 59 | import urllib as url_parse 60 | 61 | def char_to_int(value): 62 | return ord(value) 63 | 64 | def int_to_char(value): 65 | return chr(value) 66 | 67 | def make_bytes(content): 68 | return content 69 | 70 | from django.core.management.base import BaseCommand 71 | from django.conf import settings 72 | 73 | 74 | # Constants from the spec. 75 | 76 | FCGI_LISTENSOCK_FILENO = 0 77 | FCGI_HEADER_LEN = 8 78 | FCGI_VERSION_1 = 1 79 | FCGI_BEGIN_REQUEST = 1 80 | FCGI_ABORT_REQUEST = 2 81 | FCGI_END_REQUEST = 3 82 | FCGI_PARAMS = 4 83 | FCGI_STDIN = 5 84 | FCGI_STDOUT = 6 85 | FCGI_STDERR = 7 86 | FCGI_DATA = 8 87 | FCGI_GET_VALUES = 9 88 | FCGI_GET_VALUES_RESULT = 10 89 | FCGI_UNKNOWN_TYPE = 11 90 | FCGI_MAXTYPE = FCGI_UNKNOWN_TYPE 91 | FCGI_NULL_REQUEST_ID = 0 92 | FCGI_KEEP_CONN = 1 93 | FCGI_RESPONDER = 1 94 | FCGI_AUTHORIZER = 2 95 | FCGI_FILTER = 3 96 | FCGI_REQUEST_COMPLETE = 0 97 | FCGI_CANT_MPX_CONN = 1 98 | FCGI_OVERLOADED = 2 99 | FCGI_UNKNOWN_ROLE = 3 100 | 101 | FCGI_MAX_CONNS = 'FCGI_MAX_CONNS' 102 | FCGI_MAX_REQS = 'FCGI_MAX_REQS' 103 | FCGI_MPXS_CONNS = 'FCGI_MPXS_CONNS' 104 | 105 | FCGI_Header = '!BBHHBx' 106 | FCGI_BeginRequestBody = '!HB5x' 107 | FCGI_EndRequestBody = '!LB3x' 108 | FCGI_UnknownTypeBody = '!B7x' 109 | 110 | FCGI_EndRequestBody_LEN = struct.calcsize(FCGI_EndRequestBody) 111 | FCGI_UnknownTypeBody_LEN = struct.calcsize(FCGI_UnknownTypeBody) 112 | 113 | FCGI_HEADER_NAMES = ( 114 | 'ERROR TYPE: 0', 115 | 'BEGIN_REQUEST', 116 | 'ABORT_REQUEST', 117 | 'END_REQUEST', 118 | 'PARAMS', 119 | 'STDIN', 120 | 'STDOUT', 121 | 'STDERR', 122 | 'DATA', 123 | 'GET_VALUES', 124 | 'GET_VALUES_RESULT', 125 | 'UNKNOWN_TYPE', 126 | ) 127 | 128 | # configuration not from the spec 129 | 130 | FCGI_PARAMS_ENCODING = "utf-8" 131 | FCGI_CONTENT_ENCODING = FCGI_PARAMS_ENCODING 132 | 133 | FCGI_DEBUG = getattr(settings, 'FCGI_DEBUG', settings.DEBUG) 134 | FCGI_LOG = getattr(settings, 'FCGI_LOG', FCGI_DEBUG) 135 | FCGI_LOG_PATH = getattr(settings, 'FCGI_LOG_PATH', os.path.dirname(os.path.abspath(sys.argv[0]))) 136 | 137 | 138 | class InputStream(object): 139 | """ 140 | File-like object representing FastCGI input streams (FCGI_STDIN and 141 | FCGI_DATA). Supports the minimum methods required by WSGI spec. 142 | """ 143 | 144 | def __init__(self, conn): 145 | self._conn = conn 146 | 147 | # See Server. 148 | self._shrinkThreshold = conn.server.inputStreamShrinkThreshold 149 | 150 | self._buf = b'' 151 | self._bufList = [] 152 | self._pos = 0 # Current read position. 153 | self._avail = 0 # Number of bytes currently available. 154 | 155 | self._eof = False # True when server has sent EOF notification. 156 | 157 | def _shrinkBuffer(self): 158 | """Gets rid of already read data (since we can't rewind).""" 159 | if self._pos >= self._shrinkThreshold: 160 | self._buf = self._buf[self._pos:] 161 | self._avail -= self._pos 162 | self._pos = 0 163 | 164 | assert self._avail >= 0 165 | 166 | def _waitForData(self): 167 | """Waits for more data to become available.""" 168 | self._conn.process_input() 169 | 170 | def read(self, n=-1): 171 | if self._pos == self._avail and self._eof: 172 | return b'' 173 | while True: 174 | if n < 0 or (self._avail - self._pos) < n: 175 | # Not enough data available. 176 | if self._eof: 177 | # And there's no more coming. 178 | newPos = self._avail 179 | break 180 | else: 181 | # Wait for more data. 182 | self._waitForData() 183 | continue 184 | else: 185 | newPos = self._pos + n 186 | break 187 | # Merge buffer list, if necessary. 188 | if self._bufList: 189 | self._buf += b''.join(self._bufList) 190 | self._bufList = [] 191 | r = self._buf[self._pos:newPos] 192 | self._pos = newPos 193 | self._shrinkBuffer() 194 | return r 195 | 196 | def readline(self, length=None): 197 | if self._pos == self._avail and self._eof: 198 | return b'' 199 | while True: 200 | # Unfortunately, we need to merge the buffer list early. 201 | if self._bufList: 202 | self._buf += b''.join(self._bufList) 203 | self._bufList = [] 204 | # Find newline. 205 | i = self._buf.find(b'\n', self._pos) 206 | if i < 0: 207 | # Not found? 208 | if self._eof: 209 | # No more data coming. 210 | newPos = self._avail 211 | break 212 | else: 213 | if length is not None and len(self._buf) >= length + self._pos: 214 | newPos = self._pos + length 215 | break 216 | # Wait for more to come. 217 | self._waitForData() 218 | continue 219 | else: 220 | newPos = i + 1 221 | break 222 | r = self._buf[self._pos:newPos] 223 | self._pos = newPos 224 | self._shrinkBuffer() 225 | return r 226 | 227 | def readlines(self, sizehint=0): 228 | total = 0 229 | lines = [] 230 | line = self.readline() 231 | while line: 232 | lines.append(line) 233 | total += len(line) 234 | if 0 < sizehint <= total: 235 | break 236 | line = self.readline() 237 | return lines 238 | 239 | def __iter__(self): 240 | return self 241 | 242 | def next(self): 243 | r = self.readline() 244 | if not r: 245 | raise StopIteration 246 | return r 247 | 248 | def add_data(self, data): 249 | if not data: 250 | self._eof = True 251 | else: 252 | self._bufList.append(data) 253 | self._avail += len(data) 254 | 255 | 256 | class OutputStream(object): 257 | """ 258 | FastCGI output stream (FCGI_STDOUT/FCGI_STDERR). By default, calls to 259 | write() or writelines() immediately result in Records being sent back 260 | to the server. Buffering should be done in a higher level! 261 | """ 262 | 263 | def __init__(self, conn, req, type, buffered=False): 264 | self._conn = conn 265 | self._req = req 266 | self._type = type 267 | self._buffered = buffered 268 | self._bufList = [] # Used if buffered is True 269 | self.dataWritten = False 270 | self.closed = False 271 | 272 | def _write(self, data): 273 | length = len(data) 274 | while length: 275 | to_write = min(length, self._req.server.maxwrite - FCGI_HEADER_LEN) 276 | 277 | rec = Record(self._type, self._req.requestId) 278 | rec.contentLength = to_write 279 | rec.contentData = data[:to_write] 280 | self._conn.writeRecord(rec) 281 | 282 | data = data[to_write:] 283 | length -= to_write 284 | 285 | def write(self, data): 286 | assert not self.closed 287 | 288 | if not data: 289 | return 290 | 291 | self.dataWritten = True 292 | 293 | if self._buffered: 294 | self._bufList.append(data) 295 | else: 296 | self._write(data) 297 | 298 | def writelines(self, lines): 299 | assert not self.closed 300 | 301 | for line in lines: 302 | self.write(line) 303 | 304 | def flush(self): 305 | # Only need to flush if this OutputStream is actually buffered. 306 | if self._buffered: 307 | data = b''.join(self._bufList) 308 | self._bufList = [] 309 | self._write(data) 310 | 311 | # Though available, the following should NOT be called by WSGI apps. 312 | def close(self): 313 | """Sends end-of-stream notification, if necessary.""" 314 | if not self.closed and self.dataWritten: 315 | self.flush() 316 | rec = Record(self._type, self._req.requestId) 317 | self._conn.writeRecord(rec) 318 | self.closed = True 319 | 320 | 321 | class TeeOutputStream(object): 322 | """ 323 | Simple wrapper around two or more output file-like objects that copies 324 | written data to all streams. 325 | """ 326 | 327 | def __init__(self, streamList): 328 | self._streamList = streamList 329 | 330 | def write(self, data): 331 | for f in self._streamList: 332 | f.write(data) 333 | 334 | def writelines(self, lines): 335 | for line in lines: 336 | self.write(line) 337 | 338 | def flush(self): 339 | for f in self._streamList: 340 | f.flush() 341 | 342 | 343 | class StdoutWrapper(object): 344 | """ 345 | Wrapper for sys.stdout so we know if data has actually been written. 346 | """ 347 | 348 | def __init__(self, stdout): 349 | self._file = stdout 350 | self.dataWritten = False 351 | 352 | def write(self, data): 353 | if data: 354 | self.dataWritten = True 355 | self._file.write(data) 356 | 357 | def writelines(self, lines): 358 | for line in lines: 359 | self.write(line) 360 | 361 | def __getattr__(self, name): 362 | return getattr(self._file, name) 363 | 364 | 365 | def decode_pair(s, pos=0): 366 | """ 367 | Decodes a name/value pair. 368 | 369 | The number of bytes decoded as well as the name/value pair 370 | are returned. 371 | """ 372 | nameLength = char_to_int(s[pos]) 373 | if nameLength & 128: 374 | nameLength = struct.unpack('!L', s[pos:pos + 4])[0] & 0x7fffffff 375 | pos += 4 376 | else: 377 | pos += 1 378 | 379 | valueLength = char_to_int(s[pos]) 380 | if valueLength & 128: 381 | valueLength = struct.unpack('!L', s[pos:pos + 4])[0] & 0x7fffffff 382 | pos += 4 383 | else: 384 | pos += 1 385 | 386 | name = s[pos:pos + nameLength] 387 | pos += nameLength 388 | value = s[pos:pos + valueLength] 389 | pos += valueLength 390 | 391 | # when decoding, the fallback encoding must be one which can encode any binary value 392 | # i.e. it must be a code-page-based encoding with no undefined values - e.g. cp850. 393 | try: 394 | return pos, (name.decode(FCGI_PARAMS_ENCODING), value.decode(FCGI_PARAMS_ENCODING)) 395 | except UnicodeError: 396 | return pos, (name.decode('cp850'), value.decode('cp850')) 397 | 398 | 399 | def encode_pair(name, value): 400 | """ 401 | Encodes a name/value pair. 402 | 403 | The encoded string is returned. 404 | """ 405 | nameLength = len(name) 406 | if nameLength < 128: 407 | s = int_to_char(nameLength) 408 | else: 409 | s = struct.pack('!L', nameLength | long_int('0x80000000')) 410 | 411 | valueLength = len(value) 412 | if valueLength < 128: 413 | s += int_to_char(valueLength) 414 | else: 415 | s += struct.pack('!L', valueLength | long_int('0x80000000')) 416 | 417 | # when encoding, the fallback encoding must be one which can encode any unicode code point 418 | # i.e. it must be a UTF-* encoding. since we're on the web the default choice is UTF-8. 419 | try: 420 | return s + name.encode(FCGI_PARAMS_ENCODING) + value.encode(FCGI_PARAMS_ENCODING) 421 | except UnicodeError: 422 | return s + name.encode('utf-8') + value.encode('utf-8') 423 | 424 | 425 | class Record(object): 426 | """ 427 | A FastCGI Record. 428 | Used for encoding/decoding records. 429 | """ 430 | 431 | def __init__(self, type=FCGI_UNKNOWN_TYPE, requestId=FCGI_NULL_REQUEST_ID): 432 | self.version = FCGI_VERSION_1 433 | self.type = type 434 | self.requestId = requestId 435 | self.contentLength = 0 436 | self.paddingLength = 0 437 | self.contentData = b'' 438 | 439 | def _recvall(stream, length): 440 | """ 441 | Attempts to receive length bytes from a socket, blocking if necessary. 442 | (Socket may be blocking or non-blocking.) 443 | """ 444 | 445 | if FCGI_DEBUG: logging.debug('_recvall (%d)' % (length)) 446 | 447 | dataList = [] 448 | recvLen = 0 449 | 450 | while length: 451 | data = stream.read(length) 452 | if not data: # EOF 453 | break 454 | dataList.append(data) 455 | dataLen = len(data) 456 | recvLen += dataLen 457 | length -= dataLen 458 | 459 | # if FCGI_DEBUG: logging.debug('recived length = %d' % (recvLen)) 460 | 461 | return b''.join(dataList), recvLen 462 | 463 | _recvall = staticmethod(_recvall) 464 | 465 | def read(self, stream): 466 | """Read and decode a Record from a socket.""" 467 | header, length = self._recvall(stream, FCGI_HEADER_LEN) 468 | 469 | if length < FCGI_HEADER_LEN: 470 | raise EOFError 471 | 472 | self.version, self.type, self.requestId, self.contentLength, \ 473 | self.paddingLength = struct.unpack(FCGI_Header, header) 474 | 475 | if FCGI_DEBUG: 476 | hex = '' 477 | for s in header: 478 | hex += '%x|' % (char_to_int(s)) 479 | logging.debug('recv fcgi header: %s %s len: %d' % ( 480 | FCGI_HEADER_NAMES[self.type] if self.type is not None and self.type < FCGI_MAXTYPE else 481 | FCGI_HEADER_NAMES[FCGI_MAXTYPE], 482 | hex, len(header) 483 | )) 484 | 485 | if self.contentLength: 486 | try: 487 | self.contentData, length = self._recvall(stream, self.contentLength) 488 | except: 489 | raise EOFError 490 | 491 | if length < self.contentLength: 492 | raise EOFError 493 | 494 | if self.paddingLength: 495 | try: 496 | self._recvall(stream, self.paddingLength) 497 | except: 498 | raise EOFError 499 | 500 | def _sendall(stream, data): 501 | """ 502 | Writes data to a socket and does not return until all the data is sent. 503 | """ 504 | if FCGI_DEBUG: logging.debug('_sendall: len=%d' % len(data)) 505 | stream.write(data) 506 | 507 | _sendall = staticmethod(_sendall) 508 | 509 | def write(self, stream): 510 | """Encode and write a Record to a socket.""" 511 | if not self.contentLength: 512 | self.paddingLength = 8 513 | else: 514 | self.paddingLength = -self.contentLength & 7 515 | 516 | header = struct.pack(FCGI_Header, self.version, self.type, 517 | self.requestId, self.contentLength, 518 | self.paddingLength) 519 | 520 | if FCGI_DEBUG: 521 | logging.debug( 522 | 'send fcgi header: %s' % 523 | FCGI_HEADER_NAMES[self.type] if self.type is not None and self.type < FCGI_MAXTYPE else 524 | FCGI_HEADER_NAMES[FCGI_MAXTYPE] 525 | ) 526 | 527 | self._sendall(stream, header) 528 | 529 | if self.contentLength: 530 | if FCGI_DEBUG: logging.debug('send CONTENT') 531 | self._sendall(stream, self.contentData) 532 | if self.paddingLength: 533 | if FCGI_DEBUG: logging.debug('send PADDING') 534 | self._sendall(stream, b'\x00' * self.paddingLength) 535 | 536 | 537 | class Request(object): 538 | """ 539 | Represents a single FastCGI request. 540 | 541 | These objects are passed to your handler and is the main interface 542 | between your handler and the fcgi module. The methods should not 543 | be called by your handler. However, server, params, stdin, stdout, 544 | stderr, and data are free for your handler's use. 545 | """ 546 | 547 | def __init__(self, conn, inputStreamClass): 548 | self._conn = conn 549 | 550 | self.server = conn.server 551 | self.params = {} 552 | self.stdin = inputStreamClass(conn) 553 | self.stdout = OutputStream(conn, self, FCGI_STDOUT) 554 | self.stderr = OutputStream(conn, self, FCGI_STDERR) 555 | self.data = inputStreamClass(conn) 556 | 557 | def run(self): 558 | """Runs the handler, flushes the streams, and ends the request.""" 559 | 560 | try: 561 | protocolStatus, appStatus = self.server.handler(self) 562 | except Exception as instance: 563 | logging.exception(instance) # just in case there's another error reporting the exception 564 | # TODO: this appears to cause FCGI timeouts sometimes. is it an exception loop? 565 | self.stderr.flush() 566 | if not self.stdout.dataWritten: 567 | self.server.error(self) 568 | protocolStatus, appStatus = FCGI_REQUEST_COMPLETE, 0 569 | 570 | if FCGI_DEBUG: logging.debug('protocolStatus = %d, appStatus = %d' % (protocolStatus, appStatus)) 571 | 572 | self._flush() 573 | self._end(appStatus, protocolStatus) 574 | 575 | def _end(self, appStatus=long_int('0'), protocolStatus=FCGI_REQUEST_COMPLETE): 576 | self._conn.end_request(self, appStatus, protocolStatus) 577 | 578 | def _flush(self): 579 | self.stdout.flush() 580 | self.stderr.flush() 581 | 582 | 583 | class Connection(object): 584 | """ 585 | A Connection with the web server. 586 | 587 | Each Connection is associated with a single socket (which is 588 | connected to the web server) and is responsible for handling all 589 | the FastCGI message processing for that socket. 590 | """ 591 | 592 | _multiplexed = False 593 | _inputStreamClass = InputStream 594 | 595 | def __init__(self, stdin, stdout, server): 596 | self._stdin = stdin 597 | self._stdout = stdout 598 | self.server = server 599 | 600 | # Active Requests for this Connection, mapped by request ID. 601 | self._requests = {} 602 | 603 | def run(self): 604 | """Begin processing data from the socket.""" 605 | 606 | self._keepGoing = True 607 | while self._keepGoing: 608 | try: 609 | self.process_input() 610 | except KeyboardInterrupt: 611 | break 612 | # except EOFError, inst: 613 | # raise 614 | # if FCGI_DEBUG: logging.error(str(inst)) 615 | # break 616 | 617 | def process_input(self): 618 | """Attempt to read a single Record from the socket and process it.""" 619 | # Currently, any children Request threads notify this Connection 620 | # that it is no longer needed by closing the Connection's socket. 621 | # We need to put a timeout on select, otherwise we might get 622 | # stuck in it indefinitely... (I don't like this solution.) 623 | 624 | if not self._keepGoing: 625 | return 626 | 627 | rec = Record() 628 | rec.read(self._stdin) 629 | 630 | if rec.type == FCGI_GET_VALUES: 631 | self._do_get_values(rec) 632 | elif rec.type == FCGI_BEGIN_REQUEST: 633 | self._do_begin_request(rec) 634 | elif rec.type == FCGI_ABORT_REQUEST: 635 | self._do_abort_request(rec) 636 | elif rec.type == FCGI_PARAMS: 637 | self._do_params(rec) 638 | elif rec.type == FCGI_STDIN: 639 | self._do_stdin(rec) 640 | elif rec.type == FCGI_DATA: 641 | self._do_data(rec) 642 | elif rec.requestId == FCGI_NULL_REQUEST_ID: 643 | self._do_unknown_type(rec) 644 | else: 645 | # Need to complain about this. 646 | pass 647 | 648 | def writeRecord(self, rec): 649 | """ 650 | Write a Record to the socket. 651 | """ 652 | rec.write(self._stdout) 653 | 654 | def end_request(self, req, appStatus=long_int('0'), protocolStatus=FCGI_REQUEST_COMPLETE, remove=True): 655 | """ 656 | End a Request. 657 | 658 | Called by Request objects. An FCGI_END_REQUEST Record is 659 | sent to the web server. If the web server no longer requires 660 | the connection, the socket is closed, thereby ending this 661 | Connection (run() returns). 662 | """ 663 | 664 | # write empty packet to stdin 665 | rec = Record(FCGI_STDOUT, req.requestId) 666 | rec.contentData = '' 667 | rec.contentLength = 0 668 | self.writeRecord(rec) 669 | 670 | # write end request 671 | rec = Record(FCGI_END_REQUEST, req.requestId) 672 | rec.contentData = struct.pack(FCGI_EndRequestBody, appStatus, 673 | protocolStatus) 674 | rec.contentLength = FCGI_EndRequestBody_LEN 675 | self.writeRecord(rec) 676 | 677 | if remove: 678 | if FCGI_DEBUG: logging.debug('end_request: removing request from list') 679 | del self._requests[req.requestId] 680 | 681 | if FCGI_DEBUG: logging.debug('end_request: flags = %d' % req.flags) 682 | 683 | if not (req.flags & FCGI_KEEP_CONN) and not self._requests: 684 | if FCGI_DEBUG: logging.debug('end_request: set _keepGoing = False') 685 | self._keepGoing = False 686 | 687 | def _do_get_values(self, inrec): 688 | """Handle an FCGI_GET_VALUES request from the web server.""" 689 | 690 | outrec = Record(FCGI_GET_VALUES_RESULT) 691 | 692 | pos = 0 693 | while pos < inrec.contentLength: 694 | pos, (name, value) = decode_pair(inrec.contentData, pos) 695 | cap = self.server.capability.get(name) 696 | if cap is not None: 697 | outrec.contentData += encode_pair(name, str(cap)) 698 | 699 | outrec.contentLength = len(outrec.contentData) 700 | self.writeRecord(outrec) 701 | 702 | def _do_begin_request(self, inrec): 703 | """Handle an FCGI_BEGIN_REQUEST from the web server.""" 704 | role, flags = struct.unpack(FCGI_BeginRequestBody, inrec.contentData) 705 | 706 | req = self.server.request_class(self, self._inputStreamClass) 707 | req.requestId, req.role, req.flags = inrec.requestId, role, flags 708 | req.aborted = False 709 | 710 | if not self._multiplexed and self._requests: 711 | # Can't multiplex requests. 712 | self.end_request(req, long_int(0), FCGI_CANT_MPX_CONN, remove=False) 713 | else: 714 | self._requests[inrec.requestId] = req 715 | 716 | def _do_abort_request(self, inrec): 717 | """ 718 | Handle an FCGI_ABORT_REQUEST from the web server. 719 | 720 | We just mark a flag in the associated Request. 721 | """ 722 | req = self._requests.get(inrec.requestId) 723 | if req is not None: 724 | req.aborted = True 725 | 726 | def _start_request(self, req): 727 | """Run the request.""" 728 | # Not multiplexed, so run it inline. 729 | req.run() 730 | 731 | def _do_params(self, inrec): 732 | """ 733 | Handle an FCGI_PARAMS Record. 734 | 735 | If the last FCGI_PARAMS Record is received, start the request. 736 | """ 737 | 738 | req = self._requests.get(inrec.requestId) 739 | if req is not None: 740 | if inrec.contentLength: 741 | pos = 0 742 | while pos < inrec.contentLength: 743 | pos, (name, value) = decode_pair(inrec.contentData, pos) 744 | req.params[name] = value 745 | 746 | def _do_stdin(self, inrec): 747 | """Handle the FCGI_STDIN stream.""" 748 | req = self._requests.get(inrec.requestId) 749 | 750 | if inrec.contentLength: 751 | if req is not None: 752 | req.stdin.add_data(inrec.contentData) 753 | else: 754 | self._start_request(req) 755 | 756 | def _do_data(self, inrec): 757 | """Handle the FCGI_DATA stream.""" 758 | req = self._requests.get(inrec.requestId) 759 | if req is not None: 760 | req.data.add_data(inrec.contentData) 761 | 762 | def _do_unknown_type(self, inrec): 763 | """Handle an unknown request type. Respond accordingly.""" 764 | outrec = Record(FCGI_UNKNOWN_TYPE) 765 | outrec.contentData = struct.pack(FCGI_UnknownTypeBody, inrec.type) 766 | outrec.contentLength = FCGI_UnknownTypeBody_LEN 767 | self.writeRecord(outrec) 768 | 769 | 770 | class FCGIServer(object): 771 | request_class = Request 772 | maxwrite = 8192 773 | inputStreamShrinkThreshold = 102400 - 8192 774 | 775 | def __init__(self, application, environ=None, 776 | multithreaded=False, multiprocess=False, 777 | debug=False, roles=(FCGI_RESPONDER,), 778 | app_root=None): 779 | if environ is None: 780 | environ = {} 781 | 782 | self.application = application 783 | self.environ = environ 784 | self.multithreaded = multithreaded 785 | self.multiprocess = multiprocess 786 | self.debug = debug 787 | self.roles = roles 788 | self._connectionClass = Connection 789 | self.capability = { 790 | # If threads aren't available, these are pretty much correct. 791 | FCGI_MAX_CONNS: 1, 792 | FCGI_MAX_REQS: 1, 793 | FCGI_MPXS_CONNS: 0 794 | } 795 | self.app_root = app_root 796 | 797 | def run(self): 798 | msvcrt.setmode(sys.stdin.fileno(), os.O_BINARY) 799 | stdin = os.fdopen(sys.stdin.fileno(), 'rb', 0) 800 | stdout = os.fdopen(sys.stdout.fileno(), 'wb', 0) 801 | 802 | conn = Connection(stdin, stdout, self) 803 | try: 804 | conn.run() 805 | except Exception as e: 806 | logging.exception(e) 807 | raise 808 | 809 | def handler(self, req): 810 | """Special handler for WSGI.""" 811 | if req.role not in self.roles: 812 | return FCGI_UNKNOWN_ROLE, 0 813 | 814 | # Mostly taken from example CGI gateway. 815 | environ = req.params 816 | environ.update(self.environ) 817 | 818 | environ['wsgi.version'] = (1, 0) 819 | environ['wsgi.input'] = req.stdin 820 | # TODO - sys.stderr appears to be None here?? (on Windows/IIS) 821 | stderr = TeeOutputStream((sys.stderr, req.stderr)) 822 | environ['wsgi.errors'] = stderr 823 | environ['wsgi.multithread'] = False 824 | environ['wsgi.multiprocess'] = False 825 | environ['wsgi.run_once'] = False 826 | 827 | if environ.get('HTTPS', 'off') in ('on', '1'): 828 | environ['wsgi.url_scheme'] = 'https' 829 | else: 830 | environ['wsgi.url_scheme'] = 'http' 831 | 832 | self._sanitizeEnv(environ) 833 | 834 | headers_set = [] 835 | headers_sent = [] 836 | result = None 837 | 838 | def write(data): 839 | assert type(data) is bytes_type, 'write() argument must be bytes' 840 | assert headers_set, 'write() before start_response()' 841 | 842 | if not headers_sent: 843 | status, responseHeaders = headers_sent[:] = headers_set 844 | found = False 845 | for header, value in responseHeaders: 846 | if header.lower() == 'content-length': 847 | found = True 848 | break 849 | if not found and result is not None: 850 | try: 851 | if len(result) == 1: 852 | responseHeaders.append(('Content-Length', 853 | str(len(data)))) 854 | except: 855 | pass 856 | s = 'Status: %s\r\n' % status 857 | for header in responseHeaders: 858 | s += '%s: %s\r\n' % header 859 | s += '\r\n' 860 | req.stdout.write(s.encode(FCGI_CONTENT_ENCODING)) 861 | 862 | req.stdout.write(data) 863 | req.stdout.flush() 864 | 865 | def start_response(status, response_headers, exc_info=None): 866 | if exc_info: 867 | try: 868 | if headers_sent: 869 | # Re-raise if too late 870 | raise exc_info[0](exc_info[1]).with_traceback(exc_info[2]) 871 | finally: 872 | exc_info = None # avoid dangling circular ref 873 | else: 874 | assert not headers_set, 'Headers already set!' 875 | 876 | assert type(status) is str, 'Status must be a string' 877 | assert len(status) >= 4, 'Status must be at least 4 characters' 878 | assert int(status[:3]), 'Status must begin with 3-digit code' 879 | assert status[3] == ' ', 'Status must have a space after code' 880 | assert type(response_headers) is list, 'Headers must be a list' 881 | if FCGI_DEBUG: 882 | logging.debug('response headers:') 883 | for name, val in response_headers: 884 | assert type(name) is str, 'Header name "%s" must be a string' % name 885 | assert type(val) is str, 'Value of header "%s" must be a string' % name 886 | logging.debug('%s: %s' % (name, val)) 887 | 888 | headers_set[:] = [status, response_headers] 889 | return write 890 | 891 | try: 892 | try: 893 | result = self.application(environ, start_response) 894 | try: 895 | for data in result: 896 | if data: 897 | write(make_bytes(data)) 898 | if not headers_sent: 899 | write(b'') # in case body was empty 900 | finally: 901 | # if hasattr(result, 'close'): 902 | # result.close() 903 | pass 904 | # except socket.error, e: 905 | # if e[0] != errno.EPIPE: 906 | # raise # Don't let EPIPE propagate beyond server 907 | except: 908 | raise 909 | finally: 910 | pass 911 | 912 | return FCGI_REQUEST_COMPLETE, 0 913 | 914 | def _sanitizeEnv(self, environ): 915 | """Ensure certain values are present, if required by WSGI.""" 916 | 917 | if FCGI_DEBUG: 918 | logging.debug('raw envs: {0}'.format(environ)) 919 | 920 | # if not environ.has_key('SCRIPT_NAME'): 921 | # environ['SCRIPT_NAME'] = '' 922 | # TODO: fix for django 923 | environ['SCRIPT_NAME'] = '' 924 | 925 | reqUri = None 926 | if 'REQUEST_URI' in environ: 927 | reqUri = environ['REQUEST_URI'].split('?', 1) 928 | 929 | if 'PATH_INFO' not in environ or not environ['PATH_INFO']: 930 | if reqUri is not None: 931 | environ['PATH_INFO'] = reqUri[0] 932 | else: 933 | environ['PATH_INFO'] = '' 934 | 935 | # convert %XX to python unicode 936 | environ['PATH_INFO'] = url_parse.unquote(environ['PATH_INFO']) 937 | 938 | # process app_root 939 | if self.app_root and environ['PATH_INFO'].startswith(self.app_root): 940 | environ['PATH_INFO'] = environ['PATH_INFO'][len(self.app_root):] 941 | 942 | if 'QUERY_STRING' not in environ or not environ['QUERY_STRING']: 943 | if reqUri is not None and len(reqUri) > 1: 944 | environ['QUERY_STRING'] = reqUri[1] 945 | else: 946 | environ['QUERY_STRING'] = '' 947 | 948 | # If any of these are missing, it probably signifies a broken 949 | # server... 950 | for name, default in [('REQUEST_METHOD', 'GET'), 951 | ('SERVER_NAME', 'localhost'), 952 | ('SERVER_PORT', '80'), 953 | ('SERVER_PROTOCOL', 'HTTP/1.0')]: 954 | if name not in environ: 955 | message = '%s: missing FastCGI param %s required by WSGI!\n' % ( 956 | self.__class__.__name__, name) 957 | 958 | environ['wsgi.errors'].write(message.encode(FCGI_CONTENT_ENCODING)) 959 | environ[name] = default 960 | 961 | def error(self, req): 962 | """ 963 | Called by Request if an exception occurs within the handler. May and 964 | should be overridden. 965 | """ 966 | if self.debug: 967 | import cgitb 968 | 969 | req.stdout.write(b'Status: 500 Internal Server Error\r\n' + 970 | b'Content-Type: text/html\r\n\r\n' + 971 | cgitb.html(sys.exc_info()).encode(FCGI_CONTENT_ENCODING)) 972 | else: 973 | errorpage = b""" 974 | 975 | Unhandled Exception 976 | 977 |

Unhandled Exception

978 |

An unhandled exception was thrown by the application.

979 | 980 | """ 981 | req.stdout.write(b'Status: 500 Internal Server Error\r\n' + 982 | b'Content-Type: text/html\r\n\r\n' + 983 | errorpage) 984 | 985 | 986 | def example_application(environ, start_response): 987 | '''example wsgi app which outputs wsgi environment''' 988 | logging.debug('wsgi app started') 989 | data = '' 990 | env_keys = environ.keys() 991 | env_keys.sort() 992 | for e in env_keys: 993 | data += '%s: %s\n' % (e, environ[e]) 994 | data += 'sys.version: ' + sys.version + '\n' 995 | start_response('200 OK', [('Content-Type', 'text/plain'), ('Content-Length', str(len(data)))]) 996 | yield data.encode(FCGI_CONTENT_ENCODING) 997 | 998 | 999 | def run_example_app(): 1000 | if FCGI_DEBUG: logging.info('run_fcgi: STARTED') 1001 | FCGIServer(example_application).run() 1002 | if FCGI_DEBUG: logging.info('run_fcgi: EXITED') 1003 | 1004 | 1005 | def run_django_app(django_settings_module, django_root): 1006 | '''run django app by django_settings_module, 1007 | django_settings_module can be python path or physical path 1008 | ''' 1009 | if os.path.exists(django_settings_module): 1010 | # this is physical path 1011 | app_path, app_settings = os.path.split(django_settings_module) 1012 | 1013 | # add diretory to PYTHONPATH 1014 | app_dir = os.path.dirname(app_path) 1015 | if app_dir not in sys.path: 1016 | sys.path.append(app_dir) 1017 | if FCGI_DEBUG: logging.debug('%s added to PYTHONPATH' % app_dir) 1018 | 1019 | # cut .py extension in module 1020 | if app_settings.endswith('.py'): 1021 | app_settings = app_settings[:-3] 1022 | 1023 | # get python path to settings 1024 | settings_module = '%s.%s' % (os.path.basename(app_path), app_settings) 1025 | else: 1026 | # consider that django_settings_module is valid python path 1027 | settings_module = django_settings_module 1028 | 1029 | os.environ['DJANGO_SETTINGS_MODULE'] = settings_module 1030 | if FCGI_DEBUG: logging.info('DJANGO_SETTINGS_MODULE set to %s' % settings_module) 1031 | 1032 | try: 1033 | from django.core.handlers.wsgi import WSGIHandler 1034 | except ImportError: 1035 | if FCGI_DEBUG: logging.error( 1036 | 'Could not import django.core.handlers.wsgi module. Check that django is installed and in PYTHONPATH.') 1037 | raise 1038 | 1039 | FCGIServer(WSGIHandler(), app_root=django_root).run() 1040 | 1041 | 1042 | class Command(BaseCommand): 1043 | args = '[root_path]' 1044 | help = '''Run as a fcgi server''' 1045 | 1046 | def handle(self, *args, **options): 1047 | django_root = args[0] if args else None 1048 | if FCGI_LOG: 1049 | logging.basicConfig( 1050 | filename=os.path.join(FCGI_LOG_PATH, 'fcgi_%s_%d.log' % ( 1051 | datetime.datetime.now().strftime('%y%m%d_%H%M%S'), os.getpid())), 1052 | filemode='w', 1053 | format='%(asctime)s [%(levelname)-5s] %(message)s', 1054 | level=logging.DEBUG) 1055 | try: 1056 | from django.core.handlers.wsgi import WSGIHandler 1057 | except ImportError: 1058 | if FCGI_DEBUG: logging.error( 1059 | 'Could not import django.core.handlers.wsgi module. Check that django is installed and in PYTHONPATH.') 1060 | raise 1061 | 1062 | FCGIServer(WSGIHandler(), app_root=django_root, debug=settings.DEBUG).run() 1063 | 1064 | 1065 | if __name__ == '__main__': 1066 | 1067 | # compile self 1068 | compiled = os.path.split(__file__)[-1].replace('.py', '.pyc' if FCGI_DEBUG else '.pyo') 1069 | if not os.path.exists(compiled): 1070 | import py_compile 1071 | 1072 | try: 1073 | py_compile.compile(__file__) 1074 | except: 1075 | pass 1076 | 1077 | # enable logging 1078 | if FCGI_DEBUG: 1079 | logging.basicConfig( 1080 | filename=os.path.join(FCGI_LOG_PATH, 1081 | 'fcgi_%s_%d.log' % (datetime.datetime.now().strftime('%y%m%d_%H%M%S'), os.getpid())), 1082 | filemode='w', 1083 | format='%(asctime)s [%(levelname)-5s] %(message)s', 1084 | level=logging.DEBUG) 1085 | 1086 | # If we are inside a subdirectory of a django app, set the default Djan 1087 | default_django_settings_module = None 1088 | parent_settings_file = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'settings.py') 1089 | if os.path.exists(parent_settings_file): 1090 | default_django_settings_module = os.path.abspath(parent_settings_file) 1091 | if FCGI_DEBUG: 1092 | logging.info('default DJANGO_SETTINGS_MODULE set to %s' % default_django_settings_module) 1093 | 1094 | 1095 | # parse options 1096 | usage = "usage: %prog [options]" 1097 | parser = OptionParser(usage) 1098 | parser.add_option("", "--django-settings-module", dest="django_settings_module", 1099 | help="python or physical path to Django settings module") 1100 | parser.add_option("", "--django-root", dest="django_root", 1101 | help="strip this string from the front of any URLs before matching them against your URLconf patterns.") 1102 | parser.set_defaults( 1103 | django_settings_module=os.environ.get('DJANGO_SETTINGS_MODULE', default_django_settings_module), 1104 | django_root=os.environ.get('django.root', None) 1105 | ) 1106 | 1107 | (options, args) = parser.parse_args() 1108 | 1109 | # check django 1110 | if options.django_settings_module: 1111 | run_django_app(options.django_settings_module, options.django_root) 1112 | else: 1113 | # run example app 1114 | run_example_app() 1115 | -------------------------------------------------------------------------------- /django_windows_tools/management/commands/winfcgi_install.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | # FastCGI Windows Server Django installation command 4 | # 5 | # Copyright (c) 2012 Openance SARL 6 | # All rights reserved. 7 | # 8 | # Redistribution and use in source and binary forms, with or without 9 | # modification, are permitted provided that the following conditions 10 | # are met: 11 | # 1. Redistributions of source code must retain the above copyright 12 | # notice, this list of conditions and the following disclaimer. 13 | # 2. Redistributions in binary form must reproduce the above copyright 14 | # notice, this list of conditions and the following disclaimer in the 15 | # documentation and/or other materials provided with the distribution. 16 | # 17 | # THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND 18 | # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 19 | # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 20 | # ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE 21 | # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 22 | # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS 23 | # OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 24 | # HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 25 | # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY 26 | # OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 27 | # SUCH DAMAGE. 28 | # 29 | from __future__ import print_function 30 | 31 | import os 32 | import os.path 33 | import sys 34 | import re 35 | from django.core.management.base import BaseCommand, CommandError 36 | from django.template.loader import get_template 37 | from django.conf import settings 38 | import subprocess 39 | 40 | __author__ = 'Antoine Martin ' 41 | 42 | 43 | def set_file_readable(filename): 44 | import win32api 45 | import win32security 46 | import ntsecuritycon as con 47 | 48 | WinBuiltinUsersSid = 27 # not exported by win32security. according to WELL_KNOWN_SID_TYPE enumeration from http://msdn.microsoft.com/en-us/library/windows/desktop/aa379650%28v=vs.85%29.aspx 49 | users = win32security.CreateWellKnownSid(WinBuiltinUsersSid) 50 | WinBuiltinAdministratorsSid = 26 # not exported by win32security. according to WELL_KNOWN_SID_TYPE enumeration from http://msdn.microsoft.com/en-us/library/windows/desktop/aa379650%28v=vs.85%29.aspx 51 | admins = win32security.CreateWellKnownSid(WinBuiltinAdministratorsSid) 52 | user, domain, type = win32security.LookupAccountName("", win32api.GetUserName()) 53 | 54 | sd = win32security.GetFileSecurity(filename, win32security.DACL_SECURITY_INFORMATION) 55 | 56 | dacl = win32security.ACL() 57 | dacl.AddAccessAllowedAce(win32security.ACL_REVISION, con.FILE_ALL_ACCESS, users) 58 | dacl.AddAccessAllowedAce(win32security.ACL_REVISION, con.FILE_ALL_ACCESS, user) 59 | dacl.AddAccessAllowedAce(win32security.ACL_REVISION, con.FILE_ALL_ACCESS, admins) 60 | sd.SetSecurityDescriptorDacl(1, dacl, 0) 61 | win32security.SetFileSecurity(filename, win32security.DACL_SECURITY_INFORMATION, sd) 62 | 63 | 64 | class Command(BaseCommand): 65 | args = '[root_path]' 66 | help = '''Installs the current application as a fastcgi application under windows. 67 | 68 | If the root path is not specified, the command will take the 69 | root directory of the project. 70 | 71 | Don't forget to run this command as Administrator 72 | ''' 73 | 74 | CONFIGURATION_TEMPLATE = '''/+[fullPath='{python_interpreter}',arguments='{script} winfcgi --pythonpath={project_dir}',maxInstances='{maxInstances}',idleTimeout='{idleTimeout}',activityTimeout='{activityTimeout}',requestTimeout='{requestTimeout}',instanceMaxRequests='{instanceMaxRequests}',protocol='NamedPipe',flushNamedPipe='False',monitorChangesTo='{monitorChangesTo}']''' 75 | 76 | DELETE_TEMPLATE = '''/[arguments='{script} winfcgi --pythonpath={project_dir}']''' 77 | 78 | FASTCGI_SECTION = 'system.webServer/fastCgi' 79 | 80 | def add_arguments(self, parser): 81 | parser.add_argument( 82 | '--delete', 83 | action='store_true', 84 | dest='delete', 85 | default=False, 86 | help='Deletes the configuration instead of creating it') 87 | parser.add_argument( 88 | '--max-instances', 89 | dest='maxInstances', 90 | default=4, 91 | help='Maximum number of pyhton processes') 92 | parser.add_argument( 93 | '--idle-timeout', 94 | dest='idleTimeout', 95 | default=1800, 96 | help='Idle time in seconds after which a python process is recycled') 97 | parser.add_argument( 98 | '--max-content-length', 99 | dest='maxContentLength', 100 | default=30000000, 101 | help='Maximum allowed request content length size') 102 | parser.add_argument( 103 | '--activity-timeout', 104 | dest='activityTimeout', 105 | default=30, 106 | help='Number of seconds without data transfer after which a process is stopped') 107 | parser.add_argument( 108 | '--request-timeout', 109 | dest='requestTimeout', 110 | default=90, 111 | help='Total time in seconds for a request') 112 | parser.add_argument( 113 | '--instance-max-requests', 114 | dest='instanceMaxRequests', 115 | default=10000, 116 | help='Number of requests after which a python process is recycled') 117 | parser.add_argument( 118 | '--monitor-changes-to', 119 | dest='monitorChangesTo', 120 | default='', 121 | help='Application is restarted when this file changes') 122 | parser.add_argument( 123 | '--site-name', 124 | dest='site_name', 125 | default='', 126 | help='IIS site name (defaults to name of installation directory)') 127 | parser.add_argument( 128 | '--binding', 129 | dest='binding', 130 | default='http://*:80', 131 | help='IIS site binding. Defaults to http://*:80') 132 | parser.add_argument( 133 | '--skip-fastcgi', 134 | action='store_true', 135 | dest='skip_fastcgi', 136 | default=False, 137 | help='Skips the FastCGI application installation') 138 | parser.add_argument( 139 | '--skip-site', 140 | action='store_true', 141 | dest='skip_site', 142 | default=False, 143 | help='Skips the site creation') 144 | parser.add_argument( 145 | '--skip-config', 146 | action='store_true', 147 | dest='skip_config', 148 | default=False, 149 | help='Skips the configuration creation') 150 | parser.add_argument( 151 | '--log-dir', 152 | dest='log_dir', 153 | default='', 154 | help=r'Directory for IIS logfiles (defaults to %SystemDrive%\inetpub\logs\LogFiles)') 155 | 156 | def __init__(self, *args, **kwargs): 157 | super(Command, self).__init__(*args, **kwargs) 158 | self.appcmd = os.path.join(os.environ['windir'], 'system32', 'inetsrv', 'appcmd.exe') 159 | self.current_script = os.path.abspath(sys.argv[0]) 160 | self.project_dir, self.script_name = os.path.split(self.current_script) 161 | self.python_interpreter = sys.executable 162 | self.last_command_error = None 163 | 164 | def config_command(self, command, section, *args): 165 | arguments = [self.appcmd, command, section] 166 | arguments.extend(args) 167 | # print ' '.join(arguments) 168 | return subprocess.Popen(arguments, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 169 | 170 | def run_config_command(self, command, section, *args): 171 | command_process = self.config_command(command, section, *args) 172 | (out, err) = command_process.communicate() 173 | result = command_process.returncode == 0 174 | self.last_command_error = out if not result else None 175 | return result 176 | 177 | def check_config_section_exists(self, section_name): 178 | return self.run_config_command('list', 'config', '-section:%s' % section_name) 179 | 180 | def create_fastcgi_section(self, options): 181 | template_options = options.copy() 182 | template_options['script'] = self.current_script 183 | template_options['project_dir'] = self.project_dir 184 | template_options['python_interpreter'] = self.python_interpreter 185 | param = self.CONFIGURATION_TEMPLATE.format(**template_options) 186 | return self.run_config_command('set', 'config', '-section:%s' % self.FASTCGI_SECTION, param, '/commit:apphost') 187 | 188 | def delete_fastcgi_section(self): 189 | template_options = dict(script=self.current_script, project_dir=self.project_dir) 190 | param = self.DELETE_TEMPLATE.format(**template_options) 191 | return self.run_config_command('clear', 'config', '-section:%s' % self.FASTCGI_SECTION, param, 192 | '/commit:apphost') 193 | 194 | def install(self, args, options): 195 | if os.path.exists(self.web_config) and not options['skip_config']: 196 | raise CommandError('A web site configuration already exists in [%s] !' % self.install_dir) 197 | 198 | # now getting static files directory and URL 199 | static_dir = os.path.normcase(os.path.abspath(getattr(settings, 'STATIC_ROOT', ''))) 200 | static_url = getattr(settings, 'STATIC_URL', '/static/') 201 | 202 | static_match = re.match('^/([^/]+)/$', static_url) 203 | if static_match: 204 | static_is_local = True 205 | static_name = static_match.group(1) 206 | static_needs_virtual_dir = static_dir != os.path.join(self.install_dir, static_name) 207 | else: 208 | static_is_local = False 209 | 210 | if static_dir == self.install_dir and static_is_local: 211 | raise CommandError('''\ 212 | The web site directory cannot be the same as the static directory, 213 | for we cannot have two different web.config files in the same 214 | directory !''') 215 | 216 | # create web.config 217 | if not options['skip_config']: 218 | print("Creating web.config") 219 | template = get_template('windows_tools/iis/web.config') 220 | file = open(self.web_config, 'w') 221 | file.write(template.render(self.__dict__)) 222 | file.close() 223 | set_file_readable(self.web_config) 224 | 225 | if options['monitorChangesTo'] == '': 226 | options['monitorChangesTo'] = os.path.join(self.install_dir, 'web.config') 227 | 228 | # create FastCGI application 229 | if not options['skip_fastcgi']: 230 | print("Creating FastCGI application") 231 | if not self.create_fastcgi_section(options): 232 | raise CommandError( 233 | 'The FastCGI application creation has failed with the following message :\n%s' % self.last_command_error) 234 | 235 | # Create sites 236 | if not options['skip_site']: 237 | site_name = options['site_name'] 238 | print("Creating application pool with name %s" % site_name) 239 | if not self.run_config_command('add', 'apppool', '/name:%s' % site_name): 240 | raise CommandError( 241 | 'The Application Pool creation has failed with the following message :\n%s' % self.last_command_error) 242 | 243 | print("Creating the site") 244 | if not self.run_config_command('add', 'site', '/name:%s' % site_name, '/bindings:%s' % options['binding'], 245 | '/physicalPath:%s' % self.install_dir): 246 | raise CommandError( 247 | 'The site creation has failed with the following message :\n%s' % self.last_command_error) 248 | 249 | print("Adding the site to the application pool") 250 | if not self.run_config_command('set', 'app', '%s/' % site_name, '/applicationPool:%s' % site_name): 251 | raise CommandError( 252 | 'Adding the site to the application pool has failed with the following message :\n%s' % self.last_command_error) 253 | 254 | if static_is_local and static_needs_virtual_dir: 255 | print("Creating virtual directory for [%s] in [%s]" % (static_dir, static_url)) 256 | if not self.run_config_command('add', 'vdir', '/app.name:%s/' % site_name, '/path:/%s' % static_name, 257 | '/physicalPath:%s' % static_dir): 258 | raise CommandError( 259 | 'Adding the static virtual directory has failed with the following message :\n%s' % self.last_command_error) 260 | 261 | log_dir = options['log_dir'] 262 | if log_dir: 263 | if not self.run_config_command('set', 'site', '%s/' % site_name, '/logFile.directory:%s' % log_dir): 264 | raise CommandError( 265 | 'Setting the logging directory has failed with the following message :\n%s' % self.last_command_error) 266 | 267 | maxContentLength = options['maxContentLength'] 268 | if not self.run_config_command('set', 'config', '/section:requestfiltering', 269 | '/requestlimits.maxallowedcontentlength:' + str(maxContentLength)): 270 | raise CommandError( 271 | 'Setting the maximum content length has failed with the following message :\n%s' % self.last_command_error) 272 | 273 | def delete(self, args, options): 274 | if not os.path.exists(self.web_config) and not options['skip_config']: 275 | raise CommandError('A web site configuration does not exists in [%s] !' % self.install_dir) 276 | 277 | if not options['skip_config']: 278 | print("Removing site configuration") 279 | os.remove(self.web_config) 280 | 281 | if not options['skip_site']: 282 | site_name = options['site_name'] 283 | print("Removing The site") 284 | if not self.run_config_command('delete', 'site', site_name): 285 | raise CommandError( 286 | 'Removing the site has failed with the following message :\n%s' % self.last_command_error) 287 | 288 | print("Removing The application pool") 289 | if not self.run_config_command('delete', 'apppool', site_name): 290 | raise CommandError( 291 | 'Removing the site has failed with the following message :\n%s' % self.last_command_error) 292 | 293 | if not options['skip_fastcgi']: 294 | print("Removing FastCGI application") 295 | if not self.delete_fastcgi_section(): 296 | raise CommandError('The FastCGI application removal has failed') 297 | 298 | def handle(self, *args, **options): 299 | if self.script_name == 'django-admin.py': 300 | raise CommandError("""\ 301 | This command does not work when used with django-admin.py. 302 | Please run it with the manage.py of the root directory of your project. 303 | """) 304 | # Getting installation directory and doing some little checks 305 | self.install_dir = args[0] if args else self.project_dir 306 | if not os.path.exists(self.install_dir): 307 | raise CommandError('The web site directory [%s] does not exist !' % self.install_dir) 308 | 309 | if not os.path.isdir(self.install_dir): 310 | raise CommandError('The web site directory [%s] is not a directory !' % self.install_dir) 311 | 312 | self.install_dir = os.path.normcase(os.path.abspath(self.install_dir)) 313 | 314 | print('Using installation directory %s' % self.install_dir) 315 | 316 | self.web_config = os.path.join(self.install_dir, 'web.config') 317 | 318 | if options['site_name'] == '': 319 | options['site_name'] = os.path.split(self.install_dir)[1] 320 | 321 | if not os.path.exists(self.appcmd): 322 | raise CommandError('It seems that IIS is not installed on your machine') 323 | 324 | if not self.check_config_section_exists(self.FASTCGI_SECTION): 325 | raise CommandError( 326 | 'Failed to detect the CGI module with the following message:\n%s' % self.last_command_error) 327 | 328 | if options['delete']: 329 | self.delete(args, options) 330 | else: 331 | self.install(args, options) 332 | 333 | 334 | if __name__ == '__main__': 335 | print('This is supposed to be run as a django management command') 336 | -------------------------------------------------------------------------------- /django_windows_tools/management/commands/winservice_install.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | # Django command installing a Windows Service that runs Django commands 4 | # 5 | # Copyright (c) 2012 Openance 6 | # All rights reserved. 7 | # 8 | # Redistribution and use in source and binary forms, with or without 9 | # modification, are permitted provided that the following conditions 10 | # are met: 11 | # 1. Redistributions of source code must retain the above copyright 12 | # notice, this list of conditions and the following disclaimer. 13 | # 2. Redistributions in binary form must reproduce the above copyright 14 | # notice, this list of conditions and the following disclaimer in the 15 | # documentation and/or other materials provided with the distribution. 16 | # 17 | # THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND 18 | # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 19 | # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 20 | # ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE 21 | # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 22 | # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS 23 | # OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 24 | # HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 25 | # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY 26 | # OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 27 | # SUCH DAMAGE. 28 | # 29 | 30 | from __future__ import print_function 31 | 32 | __author__ = 'Antoine Martin ' 33 | 34 | import os 35 | import sys 36 | import platform 37 | from django.core.management.base import BaseCommand, CommandError 38 | from django.template.loader import get_template 39 | 40 | 41 | def set_file_readable(filename): 42 | import win32api 43 | import win32security 44 | import ntsecuritycon as con 45 | 46 | users = win32security.ConvertStringSidToSid("S-1-5-32-545") 47 | admins = win32security.ConvertStringSidToSid("S-1-5-32-544") 48 | user, domain, type = win32security.LookupAccountName("", win32api.GetUserName()) 49 | 50 | sd = win32security.GetFileSecurity(filename, win32security.DACL_SECURITY_INFORMATION) 51 | 52 | dacl = win32security.ACL() 53 | dacl.AddAccessAllowedAce(win32security.ACL_REVISION, con.FILE_ALL_ACCESS, users) 54 | dacl.AddAccessAllowedAce(win32security.ACL_REVISION, con.FILE_ALL_ACCESS, user) 55 | dacl.AddAccessAllowedAce(win32security.ACL_REVISION, con.FILE_ALL_ACCESS, admins) 56 | sd.SetSecurityDescriptorDacl(1, dacl, 0) 57 | win32security.SetFileSecurity(filename, win32security.DACL_SECURITY_INFORMATION, sd) 58 | 59 | 60 | class Command(BaseCommand): 61 | args = '' 62 | help = '''Creates an NT Service that runs Django commands. 63 | 64 | This command creates a service.py script and a service.ini 65 | configuration file at the same level that the manage.py command. 66 | 67 | The django commands configured in the service.ini file can 68 | then be run as a Windows NT service by installing the service with 69 | the command: 70 | 71 | C:\project> python service.py --startup=auto install 72 | 73 | It can be started with the command: 74 | 75 | C:\project> python service.py start 76 | 77 | It can be stopped and removed with one of the commands: 78 | 79 | C:\project> python service.py stop 80 | C:\project> python service.py remove 81 | ''' 82 | 83 | def add_arguments(self, parser): 84 | parser.add_argument( 85 | '--service-name', 86 | dest='service_name', 87 | default='django-%s-service', 88 | help='Name of the service (takes the name of the project by default') 89 | parser.add_argument( 90 | '--display-name', 91 | dest='display_name', 92 | default='Django %s background service', 93 | help='Display name of the service') 94 | parser.add_argument( 95 | '--service-script-name', 96 | dest='service_script_name', 97 | default='service.py', 98 | help='Name of the service script (defaults to service.py)') 99 | parser.add_argument( 100 | '--config-file-name', 101 | dest='config_file_name', 102 | default='service.ini', 103 | help='Name of the service configuration file (defaults to service.ini)') 104 | parser.add_argument( 105 | '--log-directory', 106 | dest='log_directory', 107 | default=r'd:\logs', 108 | help=r'Location for log files (d:\logs by default)') 109 | parser.add_argument( 110 | '--beat-machine', 111 | dest='beat_machine', 112 | default='BEATSERVER', 113 | help='Name of the machine that will run the Beat scheduler') 114 | parser.add_argument( 115 | '--beat', 116 | action='store_true', 117 | dest='is_beat', 118 | default=False, 119 | help='Use this machine as host for the beat scheduler') 120 | parser.add_argument( 121 | '--overwrite', 122 | action='store_true', 123 | dest='overwrite', 124 | default=False, 125 | help='Overwrite existing files') 126 | 127 | def __init__(self, *args, **kwargs): 128 | super(Command, self).__init__(*args, **kwargs) 129 | self.current_script = os.path.abspath(sys.argv[0]) 130 | self.project_dir, self.script_name = os.path.split(self.current_script) 131 | self.project_name = os.path.split(self.project_dir)[1] 132 | 133 | def install_template(self, template_name, filename, overwrite=False, **kwargs): 134 | full_path = os.path.join(self.project_dir, filename) 135 | if os.path.exists(full_path) and not overwrite: 136 | raise CommandError('The file %s already exists !' % full_path) 137 | print("Creating %s " % full_path) 138 | template = get_template(template_name) 139 | file = open(full_path, 'w') 140 | file.write(template.render(kwargs)) 141 | file.close() 142 | set_file_readable(full_path) 143 | 144 | def handle(self, *args, **options): 145 | if self.script_name == 'django-admin.py': 146 | raise CommandError("""\ 147 | This command does not work when used with django-admin.py. 148 | Please run it with the manage.py of the root directory of your project. 149 | """) 150 | 151 | if "%s" in options['service_name']: 152 | options['service_name'] = options['service_name'] % self.project_name 153 | 154 | if "%s" in options['display_name']: 155 | options['display_name'] = options['display_name'] % self.project_name 156 | 157 | self.install_template( 158 | 'windows_tools/service/service.py', 159 | options['service_script_name'], 160 | options['overwrite'], 161 | service_name=options['service_name'], 162 | display_name=options['display_name'], 163 | config_file_name=options['config_file_name'], 164 | settings_module=os.environ['DJANGO_SETTINGS_MODULE'], 165 | ) 166 | 167 | if options['is_beat']: 168 | options['beat_machine'] = platform.node() 169 | 170 | if options['log_directory'][-1:] == '\\': 171 | options['log_directory'] = options['log_directory'][0:-1] 172 | 173 | self.install_template( 174 | 'windows_tools/service/service.ini', 175 | options['config_file_name'], 176 | options['overwrite'], 177 | log_directory=options['log_directory'], 178 | beat_machine=options['beat_machine'], 179 | config_file_name=options['config_file_name'], 180 | settings_module=os.environ['DJANGO_SETTINGS_MODULE'], 181 | ) 182 | 183 | 184 | if __name__ == '__main__': 185 | print('This is supposed to be run as a django management command') 186 | -------------------------------------------------------------------------------- /django_windows_tools/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | # Create your models here. 4 | -------------------------------------------------------------------------------- /django_windows_tools/service.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | # FastCGI-to-WSGI bridge for files/pipes transport (not socket) 4 | # 5 | # Copyright (c) 2012 Openance SARL 6 | # All rights reserved. 7 | # 8 | # Redistribution and use in source and binary forms, with or without 9 | # modification, are permitted provided that the following conditions 10 | # are met: 11 | # 1. Redistributions of source code must retain the above copyright 12 | # notice, this list of conditions and the following disclaimer. 13 | # 2. Redistributions in binary form must reproduce the above copyright 14 | # notice, this list of conditions and the following disclaimer in the 15 | # documentation and/or other materials provided with the distribution. 16 | # 17 | # THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND 18 | # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 19 | # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 20 | # ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE 21 | # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 22 | # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS 23 | # OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 24 | # HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 25 | # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY 26 | # OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 27 | # SUCH DAMAGE. 28 | # 29 | import win32serviceutil 30 | import os, os.path, sys, platform 31 | from os.path import abspath, dirname 32 | 33 | import win32service 34 | import win32event 35 | import win32con 36 | import win32file 37 | from multiprocessing import Process 38 | from multiprocessing.util import get_logger 39 | 40 | import ctypes 41 | import traceback 42 | 43 | try: 44 | import ConfigParser 45 | except ImportError: 46 | import configparser as ConfigParser 47 | 48 | # if using virtualenv under Windows, the sys.exec_prefix used in forking is 49 | # set to the base directory of the virtual environment, not to the Scripts 50 | # subdirectory where the python executable resides 51 | if hasattr(sys, 'real_prefix') and sys.platform == 'win32': 52 | sys.exec_prefix = os.path.join(sys.exec_prefix, 'Scripts') 53 | 54 | GenerateConsoleCtrlEvent = ctypes.windll.kernel32.GenerateConsoleCtrlEvent 55 | 56 | try: 57 | import multiprocessing.forking as forking 58 | main_path_key = 'main_path' 59 | except ImportError: 60 | import multiprocessing.spawn as forking 61 | main_path_key = 'init_main_from_path' 62 | 63 | # Monkey patch the Windows Process implementation to avoid thinking 64 | # that 'PythonService.exe' is a python script 65 | old_get_preparation_data = forking.get_preparation_data 66 | 67 | 68 | def new_get_preparation_data(name): 69 | d = old_get_preparation_data(name) 70 | if main_path_key in d and d[main_path_key].lower().endswith('.exe'): 71 | del d[main_path_key] 72 | return d 73 | 74 | 75 | forking.get_preparation_data = new_get_preparation_data 76 | 77 | # Do the same monkey patching on billiard which is a fork of 78 | # multiprocessing 79 | try: 80 | import billiard.forking as billiard_forking 81 | main_path_key = 'main_path' 82 | except ImportError: 83 | try: 84 | import billiard.spawn as billiard_forking 85 | main_path_key = 'init_main_from_path' 86 | except ImportError: 87 | pass 88 | 89 | try: 90 | billiard_old_get_preparation_data = billiard_forking.get_preparation_data 91 | 92 | def billiard_new_get_preparation_data(name): 93 | d = billiard_old_get_preparation_data(name) 94 | if main_path_key in d and d[main_path_key].lower().endswith('.exe'): 95 | d[main_path_key] = '__main__.py' 96 | return d 97 | 98 | billiard_forking.get_preparation_data = billiard_new_get_preparation_data 99 | except: 100 | pass 101 | 102 | 103 | def log(msg): 104 | '''Log a message in the Event Viewer as an informational message''' 105 | import servicemanager 106 | servicemanager.LogInfoMsg(str(msg)) 107 | 108 | 109 | def error(msg): 110 | '''Log a message in the Event Viewer as an error message''' 111 | import servicemanager 112 | servicemanager.LogErrorMsg(str(msg)) 113 | 114 | 115 | def initialize_logger(config): 116 | class StdErrWrapper: 117 | """ 118 | Call wrapper for stderr 119 | """ 120 | 121 | def write(self, s): 122 | get_logger().info(s) 123 | 124 | import logging 125 | 126 | logger = get_logger() 127 | values = dict( 128 | format='[%(levelname)s/%(processName)s] %(message)s', 129 | filename=None, 130 | level='INFO', 131 | ) 132 | if config and config.has_section('log'): 133 | for (name, value) in config.items('log'): 134 | values[name] = value 135 | 136 | if values['filename']: 137 | formatter = logging.Formatter(values['format']) 138 | handler = logging.FileHandler(values['filename']) 139 | handler.setFormatter(formatter) 140 | logger.addHandler(handler) 141 | logger.setLevel(getattr(logging, values['level'].upper(), logging.INFO)) 142 | sys.stderr = StdErrWrapper() 143 | 144 | 145 | def start_django_command(config, args): 146 | ''' 147 | Start a Django management command. 148 | 149 | This commands is supposed to start in a spawned (child process). 150 | It tries to import the settings of the project before handling the command. 151 | ''' 152 | initialize_logger(config) 153 | 154 | log('Starting command : %s' % ' '.join(args)) 155 | get_logger().info('Starting command : %s' % ' '.join(args)) 156 | from django.core.management import execute_from_command_line 157 | 158 | try: 159 | execute_from_command_line(args) 160 | except: 161 | error('Exception occured : %s' % traceback.format_exc()) 162 | 163 | 164 | def spawn_command(config, server_name): 165 | ''' 166 | Spawn a command specified in a configuration file and return the process object. 167 | ''' 168 | args = [getattr(sys.modules['__main__'], '__file__', __file__)] 169 | args.append(config.get(server_name, 'command')) 170 | args += config.get(server_name, 'parameters').split() 171 | process = Process(target=start_django_command, args=(config, args,)) 172 | process.start() 173 | log('Spawned %s' % ' '.join(args)) 174 | return process 175 | 176 | 177 | def start_commands(config): 178 | ''' 179 | Spawn all the commands specified in a configuration file and return an array containing all the processes. 180 | ''' 181 | processes = [] 182 | node_name = platform.node() 183 | services = config.get(node_name, 'run') if config.has_section(node_name) else config.get('services', 'run') 184 | for server_name in services.split(): 185 | processes.append(spawn_command(config, server_name)) 186 | 187 | return processes 188 | 189 | 190 | def end_commands(processes): 191 | ''' 192 | Terminate all the processes in the specified array. 193 | ''' 194 | for process in processes: 195 | # GenerateConsoleCtrlEvent(1, process.pid) 196 | process.terminate() 197 | process.join() 198 | 199 | 200 | def test_commands(base_path=None, timeout=10): 201 | ''' 202 | Method to test the spawn and termination of commands present in the configuration file. 203 | ''' 204 | config = read_config(base_path) 205 | initialize_logger(config) 206 | processes = start_commands(config) 207 | import time 208 | time.sleep(timeout) 209 | end_commands(processes) 210 | 211 | 212 | def get_config_modification_handle(path=None): 213 | '''Returns a Directory change handle on the configuration directory. 214 | 215 | This handle will be used to restart the Django commands child processes 216 | in case the configuration file has changed in the directory. 217 | ''' 218 | if not path: 219 | path = dirname(abspath(__file__)) 220 | 221 | change_handle = win32file.FindFirstChangeNotification( 222 | path, 223 | 0, 224 | win32con.FILE_NOTIFY_CHANGE_LAST_WRITE 225 | ) 226 | return change_handle 227 | 228 | 229 | def read_config(base_path=None, filename='service.ini'): 230 | ''' 231 | Reads the configuration file containing processes to spawn information 232 | ''' 233 | if not base_path: 234 | base_path = dirname(abspath(__file__)) 235 | config = ConfigParser.ConfigParser() 236 | config.optionxform = str 237 | path = os.path.join(base_path, filename) 238 | log(path) 239 | config.read(path) 240 | return config 241 | 242 | 243 | class DjangoService(win32serviceutil.ServiceFramework): 244 | """NT Service.""" 245 | 246 | _svc_name_ = "django-service" 247 | _svc_display_name_ = "Django Background Processes" 248 | _svc_description_ = "Run the Django background Processes" 249 | _config_filename = 'service.ini' 250 | 251 | def __init__(self, args): 252 | win32serviceutil.ServiceFramework.__init__(self, args) 253 | log('Initialization') 254 | # create an event that SvcDoRun can wait on and SvcStop 255 | # can set. 256 | self.config = read_config(self._base_path, self._config_filename) 257 | initialize_logger(self.config) 258 | if not self._base_path in sys.path: 259 | sys.path.append(self._base_path) 260 | 261 | parent_path = dirname(self._base_path) 262 | if not parent_path in sys.path: 263 | sys.path.append(parent_path) 264 | self.stop_event = win32event.CreateEvent(None, 0, 0, None) 265 | 266 | def SvcDoRun(self): 267 | self.ReportServiceStatus(win32service.SERVICE_START_PENDING) 268 | log('starting') 269 | self.ReportServiceStatus(win32service.SERVICE_RUNNING) 270 | 271 | self.modification_handle = get_config_modification_handle(self._base_path) 272 | self.configuration_mtime = os.stat(os.path.join(self._base_path, self._config_filename)).st_mtime 273 | 274 | keep_running = True 275 | do_start = True 276 | while keep_running: 277 | 278 | # do the actual start 279 | if do_start: 280 | self.start() 281 | 282 | log('Started. Waiting for stop') 283 | index = win32event.WaitForMultipleObjects([self.stop_event, self.modification_handle], False, 284 | win32event.INFINITE) 285 | if index == 0: 286 | # The stop event has been signaled. Stop execution. 287 | keep_running = False 288 | else: 289 | # re-initialise handle 290 | win32file.FindNextChangeNotification(self.modification_handle) 291 | 292 | new_mtime = os.stat(os.path.join(self._base_path, self._config_filename)).st_mtime 293 | if new_mtime != self.configuration_mtime: 294 | self.configuration_mtime = new_mtime 295 | do_start = True 296 | log('Restarting child processes as the configuration has changed') 297 | self.stop() 298 | self.config = read_config(self._base_path, self._config_filename) 299 | else: 300 | do_start = False 301 | 302 | win32file.FindCloseChangeNotification(self.modification_handle) 303 | 304 | def SvcStop(self): 305 | self.ReportServiceStatus(win32service.SERVICE_STOP_PENDING) 306 | log('Stopping') 307 | # Do the actual stop 308 | self.stop() 309 | log('Stopped') 310 | win32event.SetEvent(self.stop_event) 311 | self.ReportServiceStatus(win32service.SERVICE_STOPPED) 312 | 313 | def start(self): 314 | self.processes = start_commands(self.config) 315 | 316 | def stop(self): 317 | if self.processes: 318 | end_commands(self.processes) 319 | self.processes = [] 320 | node_name = platform.node() 321 | clean = self.config.get(node_name, 'clean') if self.config.has_section(node_name) else self.config.get( 322 | 'services', 'clean') 323 | if clean: 324 | for file in clean.split(';'): 325 | try: 326 | os.remove(file) 327 | except: 328 | error("Error while removing %s\n%s" % (file, traceback.format_exc())) 329 | 330 | 331 | if __name__ == '__main__': 332 | if len(sys.argv) > 1 and sys.argv[1] == 'test': 333 | test_commands() 334 | else: 335 | DjangoService._base_path = dirname(abspath(__file__)) 336 | win32serviceutil.HandleCommandLine(DjangoService) 337 | -------------------------------------------------------------------------------- /django_windows_tools/static/web.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /django_windows_tools/templates/windows_tools/iis/web.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /django_windows_tools/templates/windows_tools/service/service.ini: -------------------------------------------------------------------------------- 1 | [services] 2 | # Services to be run on all machines 3 | run=celeryd 4 | clean={{ log_directory }}\celery.log 5 | 6 | [{{ beat_machine }}] 7 | # There should be only one machine with the celerybeat service 8 | run=celeryd celerybeat 9 | clean={{ log_directory }}\celerybeat.pid;{{ log_directory }}\beat.log;{{ log_directory }}\celery.log 10 | 11 | [celeryd] 12 | command=celeryd 13 | parameters=-f {{ log_directory }}\celery.log -l info 14 | 15 | [celerybeat] 16 | command=celerybeat 17 | parameters=-f {{ log_directory }}\beat.log -l info --pidfile={{ log_directory }}\celerybeat.pid 18 | 19 | [runserver] 20 | # Runs the debug server and listen on port 8000 21 | # This one is just an example to show that any manage command can be used 22 | command=runserver 23 | parameters=--noreload --insecure 0.0.0.0:8000 24 | 25 | [log] 26 | filename={{ log_directory }}\service.log 27 | level=INFO -------------------------------------------------------------------------------- /django_windows_tools/templates/windows_tools/service/service.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import os.path 4 | import sys 5 | import win32serviceutil 6 | 7 | # This is my base path 8 | base_path = os.path.dirname(os.path.abspath(__file__)) 9 | if not base_path in sys.path: 10 | sys.path.append(base_path) 11 | 12 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "{{ settings_module }}") 13 | 14 | from django_windows_tools.service import DjangoService,test_commands 15 | 16 | class Service(DjangoService): 17 | 18 | _base_path = base_path 19 | _svc_name_ = "{{ service_name }}" 20 | _svc_display_name_ = "{{ display_name }}" 21 | _config_filename = "{{ config_file_name }}" 22 | 23 | 24 | if __name__ == "__main__": 25 | if len(sys.argv) > 1 and sys.argv[1] == 'test': 26 | test_commands(base_path) 27 | else: 28 | win32serviceutil.HandleCommandLine(Service) 29 | -------------------------------------------------------------------------------- /django_windows_tools/tests.py: -------------------------------------------------------------------------------- 1 | """ 2 | This file demonstrates writing tests using the unittest module. These will pass 3 | when you run "manage.py test". 4 | 5 | Replace this with more appropriate tests for your application. 6 | """ 7 | 8 | from django.test import TestCase 9 | 10 | 11 | class SimpleTest(TestCase): 12 | def test_basic_addition(self): 13 | """ 14 | Tests that 1 + 1 always equals 2. 15 | """ 16 | self.assertEqual(1 + 1, 2) 17 | -------------------------------------------------------------------------------- /django_windows_tools/views.py: -------------------------------------------------------------------------------- 1 | # Create your views here. 2 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # Internal variables. 11 | PAPEROPT_a4 = -D latex_paper_size=a4 12 | PAPEROPT_letter = -D latex_paper_size=letter 13 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 14 | # the i18n builder cannot share the environment and doctrees with the others 15 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 16 | 17 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext 18 | 19 | help: 20 | @echo "Please use \`make ' where is one of" 21 | @echo " html to make standalone HTML files" 22 | @echo " dirhtml to make HTML files named index.html in directories" 23 | @echo " singlehtml to make a single large HTML file" 24 | @echo " pickle to make pickle files" 25 | @echo " json to make JSON files" 26 | @echo " htmlhelp to make HTML files and a HTML help project" 27 | @echo " qthelp to make HTML files and a qthelp project" 28 | @echo " devhelp to make HTML files and a Devhelp project" 29 | @echo " epub to make an epub" 30 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 31 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 32 | @echo " text to make text files" 33 | @echo " man to make manual pages" 34 | @echo " texinfo to make Texinfo files" 35 | @echo " info to make Texinfo files and run them through makeinfo" 36 | @echo " gettext to make PO message catalogs" 37 | @echo " changes to make an overview of all changed/added/deprecated items" 38 | @echo " linkcheck to check all external links for integrity" 39 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 40 | 41 | clean: 42 | -rm -rf $(BUILDDIR)/* 43 | 44 | html: 45 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 46 | @echo 47 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 48 | 49 | dirhtml: 50 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 51 | @echo 52 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 53 | 54 | singlehtml: 55 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 56 | @echo 57 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 58 | 59 | pickle: 60 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 61 | @echo 62 | @echo "Build finished; now you can process the pickle files." 63 | 64 | json: 65 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 66 | @echo 67 | @echo "Build finished; now you can process the JSON files." 68 | 69 | htmlhelp: 70 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 71 | @echo 72 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 73 | ".hhp project file in $(BUILDDIR)/htmlhelp." 74 | 75 | qthelp: 76 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 77 | @echo 78 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 79 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 80 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/django-windows-tools.qhcp" 81 | @echo "To view the help file:" 82 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/django-windows-tools.qhc" 83 | 84 | devhelp: 85 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 86 | @echo 87 | @echo "Build finished." 88 | @echo "To view the help file:" 89 | @echo "# mkdir -p $$HOME/.local/share/devhelp/django-windows-tools" 90 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/django-windows-tools" 91 | @echo "# devhelp" 92 | 93 | epub: 94 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 95 | @echo 96 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 97 | 98 | latex: 99 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 100 | @echo 101 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 102 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 103 | "(use \`make latexpdf' here to do that automatically)." 104 | 105 | latexpdf: 106 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 107 | @echo "Running LaTeX files through pdflatex..." 108 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 109 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 110 | 111 | text: 112 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 113 | @echo 114 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 115 | 116 | man: 117 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 118 | @echo 119 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 120 | 121 | texinfo: 122 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 123 | @echo 124 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 125 | @echo "Run \`make' in that directory to run these through makeinfo" \ 126 | "(use \`make info' here to do that automatically)." 127 | 128 | info: 129 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 130 | @echo "Running Texinfo files through makeinfo..." 131 | make -C $(BUILDDIR)/texinfo info 132 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 133 | 134 | gettext: 135 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 136 | @echo 137 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 138 | 139 | changes: 140 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 141 | @echo 142 | @echo "The overview file is in $(BUILDDIR)/changes." 143 | 144 | linkcheck: 145 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 146 | @echo 147 | @echo "Link check complete; look for any errors in the above output " \ 148 | "or in $(BUILDDIR)/linkcheck/output.txt." 149 | 150 | doctest: 151 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 152 | @echo "Testing of doctests in the sources finished, look at the " \ 153 | "results in $(BUILDDIR)/doctest/output.txt." 154 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # django-windows-tools documentation build configuration file, created by 4 | # sphinx-quickstart on Tue Jul 03 18:56:34 2012. 5 | # 6 | # This file is execfile()d with the current directory set to its containing dir. 7 | # 8 | # Note that not all possible configuration values are present in this 9 | # autogenerated file. 10 | # 11 | # All configuration values have a default; values that are commented out 12 | # serve to show the default. 13 | 14 | import sys, os 15 | 16 | # If extensions (or modules to document with autodoc) are in another directory, 17 | # add these directories to sys.path here. If the directory is relative to the 18 | # documentation root, use os.path.abspath to make it absolute, like shown here. 19 | #sys.path.insert(0, os.path.abspath('.')) 20 | 21 | # -- General configuration ----------------------------------------------------- 22 | 23 | # If your documentation needs a minimal Sphinx version, state it here. 24 | #needs_sphinx = '1.0' 25 | 26 | # Add any Sphinx extension module names here, as strings. They can be extensions 27 | # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. 28 | extensions = ['sphinx.ext.autodoc', 'sphinx.ext.viewcode'] 29 | 30 | # Add any paths that contain templates here, relative to this directory. 31 | templates_path = ['_templates'] 32 | 33 | # The suffix of source filenames. 34 | source_suffix = '.rst' 35 | 36 | # The encoding of source files. 37 | #source_encoding = 'utf-8-sig' 38 | 39 | # The master toctree document. 40 | master_doc = 'index' 41 | 42 | # General information about the project. 43 | project = 'django-windows-tools' 44 | copyright = '2012, Antoine Martin' 45 | 46 | # The version info for the project you're documenting, acts as replacement for 47 | # |version| and |release|, also used in various other places throughout the 48 | # built documents. 49 | # 50 | # The short X.Y version. 51 | version = '0.2' 52 | # The full version, including alpha/beta/rc tags. 53 | release = '0.2.1' 54 | 55 | # The language for content autogenerated by Sphinx. Refer to documentation 56 | # for a list of supported languages. 57 | #language = None 58 | 59 | # There are two options for replacing |today|: either, you set today to some 60 | # non-false value, then it is used: 61 | #today = '' 62 | # Else, today_fmt is used as the format for a strftime call. 63 | #today_fmt = '%B %d, %Y' 64 | 65 | # List of patterns, relative to source directory, that match files and 66 | # directories to ignore when looking for source files. 67 | exclude_patterns = ['_build'] 68 | 69 | # The reST default role (used for this markup: `text`) to use for all documents. 70 | #default_role = None 71 | 72 | # If true, '()' will be appended to :func: etc. cross-reference text. 73 | #add_function_parentheses = True 74 | 75 | # If true, the current module name will be prepended to all description 76 | # unit titles (such as .. function::). 77 | #add_module_names = True 78 | 79 | # If true, sectionauthor and moduleauthor directives will be shown in the 80 | # output. They are ignored by default. 81 | #show_authors = False 82 | 83 | # The name of the Pygments (syntax highlighting) style to use. 84 | pygments_style = 'sphinx' 85 | 86 | # A list of ignored prefixes for module index sorting. 87 | #modindex_common_prefix = [] 88 | 89 | 90 | # -- Options for HTML output --------------------------------------------------- 91 | 92 | # The theme to use for HTML and HTML Help pages. See the documentation for 93 | # a list of builtin themes. 94 | html_theme = 'default' 95 | 96 | # Theme options are theme-specific and customize the look and feel of a theme 97 | # further. For a list of options available for each theme, see the 98 | # documentation. 99 | #html_theme_options = {} 100 | 101 | # Add any paths that contain custom themes here, relative to this directory. 102 | #html_theme_path = [] 103 | 104 | # The name for this set of Sphinx documents. If None, it defaults to 105 | # " v documentation". 106 | #html_title = None 107 | 108 | # A shorter title for the navigation bar. Default is the same as html_title. 109 | #html_short_title = None 110 | 111 | # The name of an image file (relative to this directory) to place at the top 112 | # of the sidebar. 113 | #html_logo = None 114 | 115 | # The name of an image file (within the static path) to use as favicon of the 116 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 117 | # pixels large. 118 | #html_favicon = None 119 | 120 | # Add any paths that contain custom static files (such as style sheets) here, 121 | # relative to this directory. They are copied after the builtin static files, 122 | # so a file named "default.css" will overwrite the builtin "default.css". 123 | html_static_path = ['_static'] 124 | 125 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 126 | # using the given strftime format. 127 | #html_last_updated_fmt = '%b %d, %Y' 128 | 129 | # If true, SmartyPants will be used to convert quotes and dashes to 130 | # typographically correct entities. 131 | #html_use_smartypants = True 132 | 133 | # Custom sidebar templates, maps document names to template names. 134 | #html_sidebars = {} 135 | 136 | # Additional templates that should be rendered to pages, maps page names to 137 | # template names. 138 | #html_additional_pages = {} 139 | 140 | # If false, no module index is generated. 141 | #html_domain_indices = True 142 | 143 | # If false, no index is generated. 144 | #html_use_index = True 145 | 146 | # If true, the index is split into individual pages for each letter. 147 | #html_split_index = False 148 | 149 | # If true, links to the reST sources are added to the pages. 150 | #html_show_sourcelink = True 151 | 152 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 153 | #html_show_sphinx = True 154 | 155 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 156 | #html_show_copyright = True 157 | 158 | # If true, an OpenSearch description file will be output, and all pages will 159 | # contain a tag referring to it. The value of this option must be the 160 | # base URL from which the finished HTML is served. 161 | #html_use_opensearch = '' 162 | 163 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 164 | #html_file_suffix = None 165 | 166 | # Output file base name for HTML help builder. 167 | htmlhelp_basename = 'django-windows-toolsdoc' 168 | 169 | 170 | # -- Options for LaTeX output -------------------------------------------------- 171 | 172 | latex_elements = { 173 | # The paper size ('letterpaper' or 'a4paper'). 174 | #'papersize': 'letterpaper', 175 | 176 | # The font size ('10pt', '11pt' or '12pt'). 177 | #'pointsize': '10pt', 178 | 179 | # Additional stuff for the LaTeX preamble. 180 | #'preamble': '', 181 | } 182 | 183 | # Grouping the document tree into LaTeX files. List of tuples 184 | # (source start file, target name, title, author, documentclass [howto/manual]). 185 | latex_documents = [ 186 | ('index', 'django-windows-tools.tex', u'django-windows-tools Documentation', 187 | u'Antoine Martin', 'manual'), 188 | ] 189 | 190 | # The name of an image file (relative to this directory) to place at the top of 191 | # the title page. 192 | #latex_logo = None 193 | 194 | # For "manual" documents, if this is true, then toplevel headings are parts, 195 | # not chapters. 196 | #latex_use_parts = False 197 | 198 | # If true, show page references after internal links. 199 | #latex_show_pagerefs = False 200 | 201 | # If true, show URL addresses after external links. 202 | #latex_show_urls = False 203 | 204 | # Documents to append as an appendix to all manuals. 205 | #latex_appendices = [] 206 | 207 | # If false, no module index is generated. 208 | #latex_domain_indices = True 209 | 210 | 211 | # -- Options for manual page output -------------------------------------------- 212 | 213 | # One entry per manual page. List of tuples 214 | # (source start file, name, description, authors, manual section). 215 | man_pages = [ 216 | ('index', 'django-windows-tools', 'django-windows-tools Documentation', 217 | ['Antoine Martin'], 1) 218 | ] 219 | 220 | # If true, show URL addresses after external links. 221 | #man_show_urls = False 222 | 223 | 224 | # -- Options for Texinfo output ------------------------------------------------ 225 | 226 | # Grouping the document tree into Texinfo files. List of tuples 227 | # (source start file, target name, title, author, 228 | # dir menu entry, description, category) 229 | texinfo_documents = [ 230 | ('index', 'django-windows-tools', 'django-windows-tools Documentation', 231 | 'Antoine Martin', 'django-windows-tools', 'One line description of project.', 232 | 'Miscellaneous'), 233 | ] 234 | 235 | # Documents to append as an appendix to all manuals. 236 | #texinfo_appendices = [] 237 | 238 | # If false, no module index is generated. 239 | #texinfo_domain_indices = True 240 | 241 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 242 | #texinfo_show_urls = 'footnote' 243 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. django-windows-tools documentation master file, created by 2 | sphinx-quickstart on Tue Jul 03 18:56:34 2012. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Welcome to django-windows-tools 7 | =============================== 8 | 9 | django-windows-tools is a small Django application providing management 10 | commands to help hosting Django projects in a Windows environment. 11 | 12 | This project started when a Django project that started as a temporary 13 | proof of concept running on a Linux box became something that needed to 14 | go to production in a IIS/SQL Server environment. 15 | 16 | We faced three concerns: 17 | 18 | - Database access. 19 | - Running a Django application behind IIS. 20 | - Running Django background processes (Celery, Celery Beat) 21 | 22 | The database is a no brainer with the help of `django-mssql`_ and pywin32 23 | as it allowed an allmost seamless switch between MySQL and SQL Server. 24 | 25 | For Hosting the Django project behind IIS, things became harder. There are 26 | several solutions around (such as the one from `HeliconTech`_), but they are 27 | either unmaintained, convoluted or Closed Source. We came out with 28 | a solution that needs only Open Source software and that can easily be automated. 29 | 30 | Last, for background and scheduled task, one wants to use celery and 31 | its beat scheduler. Again, we came out with a solution allowing to run 32 | the Django Background processes in a Windows Service. 33 | 34 | django-windows-tools packages the solutions we found 35 | and provides Django management commands that ease the deployment and configuration 36 | of a Django project on Windows. 37 | 38 | .. _django-mssql: https://bitbucket.org/Manfre/django-mssql/src 39 | .. _HeliconTech: http://www.helicontech.com/articles/running-django-on-windows-with-performance-tests/ 40 | 41 | Documentation 42 | ------------- 43 | 44 | .. toctree:: 45 | :maxdepth: 2 46 | 47 | quickstart 48 | 49 | 50 | Indices and tables 51 | ------------------ 52 | 53 | * :ref:`genindex` 54 | * :ref:`modindex` 55 | * :ref:`search` 56 | 57 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | REM Command file for Sphinx documentation 4 | 5 | if "%SPHINXBUILD%" == "" ( 6 | set SPHINXBUILD=sphinx-build 7 | ) 8 | set BUILDDIR=_build 9 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . 10 | set I18NSPHINXOPTS=%SPHINXOPTS% . 11 | if NOT "%PAPER%" == "" ( 12 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% 13 | set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% 14 | ) 15 | 16 | if "%1" == "" goto help 17 | 18 | if "%1" == "help" ( 19 | :help 20 | echo.Please use `make ^` where ^ is one of 21 | echo. html to make standalone HTML files 22 | echo. dirhtml to make HTML files named index.html in directories 23 | echo. singlehtml to make a single large HTML file 24 | echo. pickle to make pickle files 25 | echo. json to make JSON files 26 | echo. htmlhelp to make HTML files and a HTML help project 27 | echo. qthelp to make HTML files and a qthelp project 28 | echo. devhelp to make HTML files and a Devhelp project 29 | echo. epub to make an epub 30 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter 31 | echo. text to make text files 32 | echo. man to make manual pages 33 | echo. texinfo to make Texinfo files 34 | echo. gettext to make PO message catalogs 35 | echo. changes to make an overview over all changed/added/deprecated items 36 | echo. linkcheck to check all external links for integrity 37 | echo. doctest to run all doctests embedded in the documentation if enabled 38 | goto end 39 | ) 40 | 41 | if "%1" == "clean" ( 42 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i 43 | del /q /s %BUILDDIR%\* 44 | goto end 45 | ) 46 | 47 | if "%1" == "html" ( 48 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html 49 | if errorlevel 1 exit /b 1 50 | echo. 51 | echo.Build finished. The HTML pages are in %BUILDDIR%/html. 52 | goto end 53 | ) 54 | 55 | if "%1" == "dirhtml" ( 56 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml 57 | if errorlevel 1 exit /b 1 58 | echo. 59 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. 60 | goto end 61 | ) 62 | 63 | if "%1" == "singlehtml" ( 64 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml 65 | if errorlevel 1 exit /b 1 66 | echo. 67 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. 68 | goto end 69 | ) 70 | 71 | if "%1" == "pickle" ( 72 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle 73 | if errorlevel 1 exit /b 1 74 | echo. 75 | echo.Build finished; now you can process the pickle files. 76 | goto end 77 | ) 78 | 79 | if "%1" == "json" ( 80 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json 81 | if errorlevel 1 exit /b 1 82 | echo. 83 | echo.Build finished; now you can process the JSON files. 84 | goto end 85 | ) 86 | 87 | if "%1" == "htmlhelp" ( 88 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp 89 | if errorlevel 1 exit /b 1 90 | echo. 91 | echo.Build finished; now you can run HTML Help Workshop with the ^ 92 | .hhp project file in %BUILDDIR%/htmlhelp. 93 | goto end 94 | ) 95 | 96 | if "%1" == "qthelp" ( 97 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp 98 | if errorlevel 1 exit /b 1 99 | echo. 100 | echo.Build finished; now you can run "qcollectiongenerator" with the ^ 101 | .qhcp project file in %BUILDDIR%/qthelp, like this: 102 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\django-windows-tools.qhcp 103 | echo.To view the help file: 104 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\django-windows-tools.ghc 105 | goto end 106 | ) 107 | 108 | if "%1" == "devhelp" ( 109 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp 110 | if errorlevel 1 exit /b 1 111 | echo. 112 | echo.Build finished. 113 | goto end 114 | ) 115 | 116 | if "%1" == "epub" ( 117 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub 118 | if errorlevel 1 exit /b 1 119 | echo. 120 | echo.Build finished. The epub file is in %BUILDDIR%/epub. 121 | goto end 122 | ) 123 | 124 | if "%1" == "latex" ( 125 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 126 | if errorlevel 1 exit /b 1 127 | echo. 128 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. 129 | goto end 130 | ) 131 | 132 | if "%1" == "text" ( 133 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text 134 | if errorlevel 1 exit /b 1 135 | echo. 136 | echo.Build finished. The text files are in %BUILDDIR%/text. 137 | goto end 138 | ) 139 | 140 | if "%1" == "man" ( 141 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man 142 | if errorlevel 1 exit /b 1 143 | echo. 144 | echo.Build finished. The manual pages are in %BUILDDIR%/man. 145 | goto end 146 | ) 147 | 148 | if "%1" == "texinfo" ( 149 | %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo 150 | if errorlevel 1 exit /b 1 151 | echo. 152 | echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. 153 | goto end 154 | ) 155 | 156 | if "%1" == "gettext" ( 157 | %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale 158 | if errorlevel 1 exit /b 1 159 | echo. 160 | echo.Build finished. The message catalogs are in %BUILDDIR%/locale. 161 | goto end 162 | ) 163 | 164 | if "%1" == "changes" ( 165 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes 166 | if errorlevel 1 exit /b 1 167 | echo. 168 | echo.The overview file is in %BUILDDIR%/changes. 169 | goto end 170 | ) 171 | 172 | if "%1" == "linkcheck" ( 173 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck 174 | if errorlevel 1 exit /b 1 175 | echo. 176 | echo.Link check complete; look for any errors in the above output ^ 177 | or in %BUILDDIR%/linkcheck/output.txt. 178 | goto end 179 | ) 180 | 181 | if "%1" == "doctest" ( 182 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest 183 | if errorlevel 1 exit /b 1 184 | echo. 185 | echo.Testing of doctests in the sources finished, look at the ^ 186 | results in %BUILDDIR%/doctest/output.txt. 187 | goto end 188 | ) 189 | 190 | :end 191 | -------------------------------------------------------------------------------- /docs/quickstart.rst: -------------------------------------------------------------------------------- 1 | Quick Start 2 | =========== 3 | 4 | .. include:: ../README.rst 5 | :start-after: readthedocs.org>`_. -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | django>=1.4 2 | pypiwin32 3 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2012 Openance SARL 2 | # All rights reserved. 3 | # 4 | # Redistribution and use in source and binary forms, with or without 5 | # modification, are permitted provided that the following conditions 6 | # are met: 7 | # 1. Redistributions of source code must retain the above copyright 8 | # notice, this list of conditions and the following disclaimer. 9 | # 2. Redistributions in binary form must reproduce the above copyright 10 | # notice, this list of conditions and the following disclaimer in the 11 | # documentation and/or other materials provided with the distribution. 12 | # 13 | # THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND 14 | # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 15 | # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 16 | # ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE 17 | # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 18 | # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS 19 | # OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 20 | # HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 21 | # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY 22 | # OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 23 | # SUCH DAMAGE. 24 | # 25 | import os 26 | from setuptools import setup, find_packages 27 | 28 | def read_file(filename): 29 | """Read a file into a string""" 30 | path = os.path.abspath(os.path.dirname(__file__)) 31 | filepath = os.path.join(path, filename) 32 | try: 33 | return open(filepath).read() 34 | except IOError: 35 | return '' 36 | 37 | # Use the docstring of the __init__ file to be the description 38 | DESC = " ".join(__import__('django_windows_tools').__doc__.splitlines()).strip() 39 | 40 | setup( 41 | name = 'django-windows-tools', 42 | version = __import__('django_windows_tools').get_version().replace(' ', '-'), 43 | url = 'https://github.com/antoinemartin/django-windows-tools', 44 | author = 'Antoine Martin', 45 | author_email = 'antoine@openance.com', 46 | description = DESC, 47 | long_description = read_file('README.rst'), 48 | packages = find_packages(), 49 | include_package_data = True, 50 | install_requires=read_file('requirements.txt'), 51 | classifiers = [ # see http://pypi.python.org/pypi?:action=list_classifiers 52 | 'Development Status :: 4 - Beta', 53 | 'Environment :: Web Environment', 54 | 'Framework :: Django', 55 | 'Operating System :: Microsoft :: Windows', 56 | 'Intended Audience :: Developers', 57 | 'License :: OSI Approved :: BSD License', 58 | 'Operating System :: OS Independent', 59 | 'Programming Language :: Python', 60 | 'Topic :: Internet :: WWW/HTTP', 61 | 'Topic :: System :: Installation/Setup', 62 | ], 63 | keywords='Django Windows', 64 | entry_points=""" 65 | # -*- Entry points: -*- 66 | """, 67 | ) --------------------------------------------------------------------------------