├── .gitignore ├── ChangeLog.txt ├── LICENSE.txt ├── MANIFEST.in ├── README.rst ├── djsupervisor ├── __init__.py ├── config.py ├── events.py ├── management │ ├── __init__.py │ └── commands │ │ ├── __init__.py │ │ └── supervisor.py ├── models.py ├── templatetags │ ├── __init__.py │ └── djsupervisor_tags.py └── tests.py ├── setup.cfg └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.pyo 3 | *~ 4 | *.swp 5 | build/ 6 | dist/ 7 | .coverage 8 | cover 9 | .tox 10 | MANIFEST 11 | *.egg-info 12 | -------------------------------------------------------------------------------- /ChangeLog.txt: -------------------------------------------------------------------------------- 1 | v0.4.0: 2 | 3 | * Fix compatibility with Django 1.10; thanks Gabriel Duman. 4 | 5 | v0.3.4: 6 | 7 | * Fix cyclic import dependency in Django 1.9; thanks Alexander Komkov. 8 | 9 | v0.3.3: 10 | 11 | * Remove use of deprecated django.util.importlib; thanks David Costa. 12 | * Fix typo in documentation; thanks Shicha An 13 | 14 | 15 | v0.3.2: 16 | 17 | * Only require watchdog to be available if we're actually using the 18 | autoreload functionality; thanks Andrew Pendleton. 19 | 20 | 21 | v0.3.1: 22 | 23 | * Remove trailing slashes in MANIFEST.in, which break setuptools when 24 | used on Windows. 25 | 26 | 27 | v0.3.0: 28 | 29 | * Remove support for application-supplied config files. They seemed to be 30 | causing more problems than they solved, and none of the built-in ones 31 | provided more than a few lines of logic. 32 | * Remove the default "runserver" program, for the same reasons as above. 33 | * Add setting and option to specify the supervisord config file; that's 34 | SUPERVISOR_CONFIG_FILE and --config-file respectively. 35 | * Add "templated" template filter to apply templating to other config files; 36 | thanks Michael Rooney. 37 | * Make autoreload include/ignore patterns configurable via settings; 38 | thanks Hannes Struss. 39 | * Don't autoreload when .pyc or .pyo files change, only .py files. This 40 | avoids spurious reloads when a .py file is compiled for the first time. 41 | 42 | 43 | v0.2.8: 44 | 45 | * Ensure that the forked autoreload subprocess exits; thanks rassie. 46 | * Limit autoreload callback firing to once per second; thanks rassie. 47 | 48 | 49 | v0.2.7: 50 | 51 | * Use watchdog for the auto-reload mechanism instead of continuously 52 | polling the filesystem; thanks jezdez. 53 | 54 | 55 | v0.2.6: 56 | 57 | * Fix `manage.py supervisor getconfig` to work with the changes introduced 58 | in version 0.2.5; thanks sebleier. 59 | 60 | 61 | v0.2.5: 62 | 63 | * Fix `manage.py supervisor reload` to actually work, with the handy 64 | benefit of also reloading the config file from disk; thanks jezdez. 65 | 66 | 67 | v0.2.4: 68 | 69 | * Support for Django-1.4-style project directory layout; thanks j4mie. 70 | * Add --project-dir option; thanks j4mie. 71 | 72 | 73 | v0.2.3: 74 | 75 | * Add --pidfile and --logfile options; this is helpful when hooking the 76 | `manage.py supervisor` script into system init scripts. 77 | 78 | 79 | v0.2.2: 80 | 81 | * Fix handling of "--" in options list. 82 | * Explicitly use {{ PYTHON }} {{ PROJECT_DIR }}/manage.py in all default 83 | recipes. Using just {{ PROJECT_DIR }}/manage.py can be troublesome if 84 | the #! line in manage.py doesn't match the version of python you're 85 | using on the command-line. 86 | 87 | 88 | v0.2.1: 89 | 90 | * Stop manage.py trying to parse options intended for supervisorctl. 91 | * Ensure the auto-generated username is less than 8 characters. (This 92 | works around an authentication bug in some versions of supervisord). 93 | * Many tweaks to the default configuration based on early user feedback. 94 | 95 | 96 | v0.2.0: 97 | 98 | * More flexibility in selecting programs via command-line options. You 99 | can now override both the "autostart" and "exclude" options: 100 | -l/--launch => launch program automatically on startup 101 | -n/--nolaunch => don't launch automatically, but keep in config 102 | -x/--exclude => entirely remove program from the config 103 | -i/--include => keep program from the config 104 | * Rename "autorestart" to "autoreload" since the former is an existing 105 | supervisord option that means something very different. Also: 106 | * Allow selecting a subset of processes to auto-relod, via config file 107 | or command-line switch. 108 | * Add --noreload command-line switch to disable the autoreloader 109 | * Add a contrib supervisord.conf for django-ztask. 110 | * Add "getconfig" command, to print the final merged config to stdout. 111 | 112 | 113 | v0.1.1: 114 | 115 | * In debug mode, provide an "autorestart" process that watches your code 116 | and restarts all processes when something changes. 117 | * project-specific config: allow pre-specified [program] sections to be 118 | completely removed from the config, by specifying exclude=true. 119 | * project-specific config: allow a [program:__overrides__] section to 120 | override options in all program sections at once. Useful for things 121 | like globally switching autostart on/off or redirecting stderr. 122 | * app-specific config files: use only one of management/supervisord.conf 123 | or contrib//supervisord.conf, never both. This prevent us 124 | from accidentally conflicting with settings specified by app authors. 125 | 126 | 127 | v0.1.0: 128 | 129 | * Initial release; you might say *everything* has changed. 130 | 131 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011 Ryan Kelly 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | 2 | include README.rst 3 | include LICENSE.txt 4 | include ChangeLog.txt 5 | 6 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | 2 | Status: Unmaintained 3 | ==================== 4 | 5 | .. image:: http://unmaintained.tech/badge.svg 6 | :target: http://unmaintained.tech/ 7 | :alt: No Maintenance Intended 8 | 9 | I am `no longer actively maintaining this project `_. 10 | 11 | 12 | djsupervisor: easy integration between django and supervisord 13 | ============================================================== 14 | 15 | 16 | Django-supervisor combines the process-management awesomeness of supervisord 17 | with the convenience of Django's management scripts. 18 | 19 | 20 | Rationale 21 | --------- 22 | 23 | Running a Django project these days often entails much more than just starting 24 | up a webserver. You might need to have Django running under FCGI or CherryPy, 25 | with background tasks being managed by celeryd, periodic tasks scheduled by 26 | celerybeat, and any number of other processes all cooperating to keep the 27 | project up and running. 28 | 29 | When you're just developing or debugging, it's a pain having to start and 30 | stop all these different processes by hand. 31 | 32 | When you're deploying, it's a pain to make sure that each process is hooked 33 | into the system startup scripts with the correct configuration. 34 | 35 | Django-supervisor provides a convenient bridge between your Django project 36 | and the supervisord process control system. It makes starting all the 37 | processes required by your project as simple as:: 38 | 39 | $ python myproject/manage.py supervisor 40 | 41 | 42 | Advantages 43 | ---------- 44 | 45 | Django-supervisor is admittedly quite a thin layer on top of the wonderful 46 | functionality provided by supervisord. But by integrating tightly with 47 | Django's management scripts you gain several advantages: 48 | 49 | * manage.py remains the single point of control for running your project. 50 | * Running all those processes is just as easy in development as it 51 | is in production. 52 | * You get auto-reloading for *all* processes when running in debug mode. 53 | * Process configuration can depend on Django settings and environment 54 | variables, and have paths relative to your project and/or apps. 55 | * Apps can provide default process configurations, which projects can 56 | then tweak or override as needed. 57 | 58 | 59 | 60 | Configuration 61 | ------------- 62 | 63 | Django-supervisor is a wrapper around supervisord, so it uses the same 64 | configuration file format. Basically, you write an ini-style config file 65 | where each section defines a process to be launched. Some examples can be 66 | found below, but you'll want to refer to the supervisord docs for all the 67 | finer details: 68 | 69 | http://www.supervisord.org 70 | 71 | 72 | To get started, just include "djsupervisor" in your INSTALLED_APPS and drop 73 | a "supervisord.conf" file in your project directory, right next to the main 74 | manage.py script. 75 | 76 | A simple example config might run both the Django development server and the 77 | Celery task daemon:: 78 | 79 | [program:webserver] 80 | command={{ PYTHON }} {{ PROJECT_DIR }}/manage.py runserver --noreload 81 | 82 | [program:celeryworker] 83 | command={{ PYTHON }} {{ PROJECT_DIR }}/manage.py celery worker -l info 84 | 85 | [program:celerybeat] 86 | command={{ PYTHON }} {{ PROJECT_DIR }}/manage.py celery beat -l info 87 | 88 | Now when you run the "supervisor" management command, it will detect this 89 | file and start the two processes for you. 90 | 91 | Notice that the config file is interpreted using Django's templating engine. 92 | This lets you do fun things like locate files relative to the project root 93 | directory. 94 | 95 | Better yet, you can make parts of the config conditional based on project 96 | settings or on the environment. For example, you might start the development 97 | server when debugging but run under FCGI in production:: 98 | 99 | [program:webserver] 100 | {% if settings.DEBUG %} 101 | command={{ PYTHON }} {{ PROJECT_DIR }}/manage.py runserver 102 | {% else %} 103 | command={{ PYTHON }} {{ PROJECT_DIR }}/manage.py runfcgi host=127.0.0.1 port=8025 104 | {% endif %} 105 | 106 | 107 | Usage 108 | ----- 109 | 110 | Django-supervisor provides a new Django management command named "supervisor" 111 | which allows you to control all of the processes belonging to your project. 112 | 113 | When run without arguments, it will spawn supervisord to launch and monitor 114 | all the configured processs. Here's some example output using the config 115 | file shown in the previous section:: 116 | 117 | $ python myproject/manage.py supervisor 118 | 2011-06-07 23:46:45,253 INFO RPC interface 'supervisor' initialized 119 | 2011-06-07 23:46:45,253 INFO supervisord started with pid 4787 120 | 2011-06-07 23:46:46,258 INFO spawned: 'celeryd' with pid 4799 121 | 2011-06-07 23:46:46,275 INFO spawned: 'webserver' with pid 4801 122 | 2011-06-07 23:46:47,456 INFO success: webserver entered RUNNING state, process has stayed up for > than 1 seconds (startsecs) 123 | 2011-06-07 23:46:56,512 INFO success: celeryd entered RUNNING state, process has stayed up for > than 10 seconds (startsecs) 124 | 125 | By default the "supervisor" command will stay in the foreground and print 126 | status updates to the console. Pass the --daemonize option to have it 127 | run in the background. You can also tweak its behaviour using all of 128 | supervisord's standard options in the config file. 129 | 130 | Once the supervisor is up and running, you can interact with it to control the 131 | running processes. Running "manage.py supervisor shell" will launch the 132 | interactive supervisorctl command shell. From here you can view process 133 | status and start/stop/restart individual processes:: 134 | 135 | $ python myproject/manage.py supervisor shell 136 | celeryd RUNNING pid 4799, uptime 0:03:17 137 | webserver RUNNING pid 4801, uptime 0:03:17 138 | supervisor> 139 | supervisor> help 140 | 141 | default commands (type help ): 142 | ===================================== 143 | add clear fg open quit remove restart start stop update 144 | avail exit maintail pid reload reread shutdown status tail version 145 | 146 | supervisor> 147 | supervisor> stop celeryd 148 | celeryd: stopped 149 | supervisor> 150 | supervisor> status 151 | celeryd STOPPED Jun 07 11:51 PM 152 | webserver RUNNING pid 4801, uptime 0:04:45 153 | supervisor> 154 | 155 | 156 | You can also issue individual process-manangement commands directly on the 157 | command-line:: 158 | 159 | $ python myproject/manage.py supervisor start celeryd 160 | celeryd: started 161 | $ 162 | $ python myproject/manage.py supervisor status 163 | celeryd RUNNING pid 4937, uptime 0:00:55 164 | webserver RUNNING pid 4801, uptime 0:09:05 165 | $ 166 | $ python myproject/manage.py supervisor shutdown 167 | Shut down 168 | $ 169 | 170 | 171 | For details of all the available management commands, consult the supervisord 172 | documentation. 173 | 174 | 175 | Command-Line Options 176 | ~~~~~~~~~~~~~~~~~~~~ 177 | 178 | The "supervisor" command accepts the following options: 179 | 180 | --daemonize run the supervisord process in the background 181 | --pidfile store PID of supervisord process in this file 182 | --logfile write supervisord logs to this file 183 | --project-dir use this as the django project directory 184 | --launch=program launch program automatically at supervisor startup 185 | --nolaunch=program don't launch program automatically at startup 186 | --exclude=program remove program from the supervisord config 187 | --include=program include program in the supervisord config 188 | --autoreload=program restart program when code files change 189 | --noreload don't restart programs when code files change 190 | 191 | 192 | Extra Goodies 193 | ------------- 194 | 195 | Django-supervisor provides some extra niceties on top of the configuration 196 | language of supervisord. 197 | 198 | 199 | Templating 200 | ~~~~~~~~~~ 201 | 202 | All supervisord.conf files are rendered through Django's templating system. 203 | This allows you to interpolate values from the settings or environment, and 204 | conditionally switch processes on or off. The template context for each 205 | configuration file contains the following variables:: 206 | 207 | PROJECT_DIR the top-level directory of your project (i.e. the 208 | directory containing your manage.py script). 209 | 210 | APP_DIR for app-provided config files, the top-level 211 | directory containing the application code. 212 | 213 | PYTHON full path to the current python interpreter. 214 | 215 | SUPERVISOR_OPTIONS the command-line options passed to manage.py. 216 | 217 | settings the Django settings module, as seen by your code. 218 | 219 | environ the os.environ dict, as seen by your code. 220 | 221 | 222 | 223 | Defaults, Overrides and Excludes 224 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 225 | 226 | Django-supervisor recognises some special config-file options that are useful 227 | when merging multiple app-specific and project-specific configuration files. 228 | 229 | The [program:__defaults__] section can be used to provide default options 230 | for all other [program] sections. These options will only be used if none 231 | of the config files found by django-supervisor provide that option for 232 | a specific program. 233 | 234 | The [program:__overrides__] section can be used to override options for all 235 | configured programs. These options will be applied to all processes regardless 236 | of what any other config file has to say. 237 | 238 | Finally, you can completely disable a [program] section by setting the option 239 | "exclude" to true. This is mostly useful for disabling process definitions 240 | provided by a third-party application. 241 | 242 | Here's an example config file that shows them all in action:: 243 | 244 | ; We want all programs to redirect stderr by default, 245 | ; unless specifically configured otherwise. 246 | [program:__defaults__] 247 | redirect_stderr=true 248 | 249 | ; We force all programs to run as user "nobody" 250 | [program:__overrides__] 251 | user=nobody 252 | 253 | ; Disable auto-reloading on code changes by excluding that program. 254 | [program:autoreload] 255 | exclude=true 256 | 257 | 258 | 259 | Automatic Control Socket Config 260 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 261 | 262 | The supervisord and supervisorctl programs interact with each other via an 263 | XML-RPC control socket. This provides a great deal of flexibility and control 264 | over security, but you have to configure it just so or things won't work. 265 | 266 | For convenience during development, django-supervisor provides automatic 267 | control socket configuration. By default it binds the server to localhost 268 | on a fixed-but-randomish port, and sets up a username and password based on 269 | settings.SECRET_KEY. 270 | 271 | For production deployment, you might like to reconfigure this by setting up 272 | the [inet_http_server] or [unix_http_server] sections. Django-supervisor 273 | will honour any such settings you provide. 274 | 275 | 276 | 277 | Autoreload 278 | ~~~~~~~~~~ 279 | 280 | When running in debug mode, django-supervisor automatically defines a process 281 | named "autoreload". This is very similar to the auto-reloading feature of 282 | the Django development server, but works across all configured processes. 283 | For example, this will let you automatically restart both the dev server and 284 | celeryd whenever your code changes. 285 | 286 | To prevent an individual program from being auto-reloaded, set its "autoreload" 287 | option to false:: 288 | 289 | [program:non-python-related] 290 | autoreload=false 291 | 292 | To switch off the autoreload process entirely, you can pass the --noreload 293 | option to supervisor or just exclude it in your project config file like so:: 294 | 295 | [program:autoreload] 296 | exclude=true 297 | 298 | Optionally, the file patterns on which autoreload listens for changes can 299 | be set in your project's settings.py: 300 | 301 | SUPERVISOR_AUTORELOAD_PATTERNS = ["*.py", "*.pyc", "*.pyo"] 302 | SUPERVISOR_AUTORELOAD_IGNORE_PATTERNS = [".*", "#*", "*~"] 303 | 304 | 305 | 306 | More Info 307 | --------- 308 | 309 | There aren't any more docs online yet. Sorry. I'm working on a little tutorial 310 | and some examples, but I need to actually *use* the project a little more 311 | first to make sure it all fits together the way I want... 312 | 313 | -------------------------------------------------------------------------------- /djsupervisor/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | 3 | djsupervisor: easy integration between django and supervisord 4 | ============================================================== 5 | 6 | 7 | Django-supervisor combines the process-management awesomeness of supervisord 8 | with the convenience of Django's management scripts. 9 | 10 | 11 | Rationale 12 | --------- 13 | 14 | Running a Django project these days often entails much more than just starting 15 | up a webserver. You might need to have Django running under FCGI or CherryPy, 16 | with background tasks being managed by celeryd, periodic tasks scheduled by 17 | celerybeat, and any number of other processes all cooperating to keep the 18 | project up and running. 19 | 20 | When you're just developing or debugging, it's a pain having to start and 21 | stop all these different processes by hand. 22 | 23 | When you're deploying, it's a pain to make sure that each process is hooked 24 | into the system startup scripts with the correct configuration. 25 | 26 | Django-supervisor provides a convenient bridge between your Django project 27 | and the supervisord process control system. It makes starting all the 28 | processes required by your project as simple as:: 29 | 30 | $ python myproject/manage.py supervisor 31 | 32 | 33 | Advantages 34 | ---------- 35 | 36 | Django-supervisor is admittedly quite a thin layer on top of the wonderful 37 | functionality provided by supervisord. But by integrating tightly with 38 | Django's management scripts you gain several advantages: 39 | 40 | * manage.py remains the single point of control for running your project. 41 | * Running all those processes is just as easy in development as it 42 | is in production. 43 | * You get auto-reloading for *all* processes when running in debug mode. 44 | * Process configuration can depend on Django settings and environment 45 | variables, and have paths relative to your project and/or apps. 46 | 47 | 48 | Configuration 49 | ------------- 50 | 51 | Django-supervisor is a wrapper around supervisord, so it uses the same 52 | configuration file format. Basically, you write an ini-style config file 53 | where each section defines a process to be launched. Some examples can be 54 | found below, but you'll want to refer to the supervisord docs for all the 55 | finer details: 56 | 57 | http://www.supervisord.org 58 | 59 | 60 | To get started, just include "djsupervisor" in your INSTALLED_APPS and drop 61 | a "supervisord.conf" file in your project directory, right next to the main 62 | manage.py script. 63 | 64 | A simple example config might run both the Django development server and the 65 | Celery task daemon:: 66 | 67 | [program:webserver] 68 | command={{ PYTHON }} {{ PROJECT_DIR }}/manage.py runserver --noreload 69 | 70 | [program:celeryd] 71 | command={{ PYTHON }} {{ PROJECT_DIR }}/manage.py celeryd -l info 72 | 73 | 74 | Now when you run the "supervisor" management command, it will detect this 75 | file and start the two processes for you. 76 | 77 | Notice that the config file is interpreted using Django's templating engine. 78 | This lets you do fun things like locate files relative to the project root 79 | directory. 80 | 81 | Better yet, you can make parts of the config conditional based on project 82 | settings or on the environment. For example, you might start the development 83 | server when debugging but run under FCGI in production:: 84 | 85 | [program:webserver] 86 | {% if settings.DEBUG %} 87 | command={{ PYTHON }} {{ PROJECT_DIR }}/manage.py runserver 88 | {% else %} 89 | command={{ PYTHON }} {{ PROJECT_DIR }}/manage.py runfcgi host=127.0.0.1 port=8025 90 | {% endif %} 91 | 92 | 93 | Usage 94 | ----- 95 | 96 | Django-supervisor provides a new Django manangement command named "supervisor" 97 | which allows you to control all of the processes belonging to your project. 98 | 99 | When run without arguments, it will spawn supervisord to launch and monitor 100 | all the configured processs. Here's some example output using the config 101 | file shown in the previous section:: 102 | 103 | $ python myproject/manage.py supervisor 104 | 2011-06-07 23:46:45,253 INFO RPC interface 'supervisor' initialized 105 | 2011-06-07 23:46:45,253 INFO supervisord started with pid 4787 106 | 2011-06-07 23:46:46,258 INFO spawned: 'celeryd' with pid 4799 107 | 2011-06-07 23:46:46,275 INFO spawned: 'webserver' with pid 4801 108 | 2011-06-07 23:46:47,456 INFO success: webserver entered RUNNING state, process has stayed up for > than 1 seconds (startsecs) 109 | 2011-06-07 23:46:56,512 INFO success: celeryd entered RUNNING state, process has stayed up for > than 10 seconds (startsecs) 110 | 111 | By default the "supervisor" command will stay in the foreground and print 112 | status updates to the console. Pass the --daemonize option to have it 113 | run in the background. You can also tweak its behaviour using all of 114 | supervisord's standard options in the config file. 115 | 116 | Once the supervisor is up and running, you can interact with it to control the 117 | running processes. Running "manage.py supervisor shell" will launch the 118 | interactive supervisorctl command shell. From here you can view process 119 | status and start/stop/restart individual processes:: 120 | 121 | $ python myproject/manage.py supervisor shell 122 | celeryd RUNNING pid 4799, uptime 0:03:17 123 | webserver RUNNING pid 4801, uptime 0:03:17 124 | supervisor> 125 | supervisor> help 126 | 127 | default commands (type help ): 128 | ===================================== 129 | add clear fg open quit remove restart start stop update 130 | avail exit maintail pid reload reread shutdown status tail version 131 | 132 | supervisor> 133 | supervisor> stop celeryd 134 | celeryd: stopped 135 | supervisor> 136 | supervisor> status 137 | celeryd STOPPED Jun 07 11:51 PM 138 | webserver RUNNING pid 4801, uptime 0:04:45 139 | supervisor> 140 | 141 | 142 | You can also issue individual process-manangement commands directly on the 143 | command-line:: 144 | 145 | $ python myproject/manage.py supervisor start celeryd 146 | celeryd: started 147 | $ 148 | $ python myproject/manage.py supervisor status 149 | celeryd RUNNING pid 4937, uptime 0:00:55 150 | webserver RUNNING pid 4801, uptime 0:09:05 151 | $ 152 | $ python myproject/manage.py supervisor shutdown 153 | Shut down 154 | $ 155 | 156 | 157 | For details of all the available management commands, consult the supervisord 158 | documentation. 159 | 160 | 161 | Command-Line Options 162 | ~~~~~~~~~~~~~~~~~~~~ 163 | 164 | The "supervisor" command accepts the following options: 165 | 166 | --daemonize run the supervisord process in the background 167 | --pidfile store PID of supervisord process in this file 168 | --loggile write supervisord logs to this file 169 | --project-dir use this as the django project directory 170 | --launch=program launch program automatically at supervisor startup 171 | --nolaunch=program don't launch program automatically at startup 172 | --exclude=program remove program from the supervisord config 173 | --include=program include program in the supervisord config 174 | --autoreload=program restart program when code files change 175 | --noreload don't restart programs when code files change 176 | 177 | 178 | Extra Goodies 179 | ------------- 180 | 181 | Django-supervisor provides some extra niceties on top of the configuration 182 | language of supervisord. 183 | 184 | 185 | Templating 186 | ~~~~~~~~~~ 187 | 188 | All supervisord.conf files are rendered through Django's templating system. 189 | This allows you to interpolate values from the settings or environment, and 190 | conditionally switch processes on or off. The template context for each 191 | configuration file contains the following variables:: 192 | 193 | PROJECT_DIR the top-level directory of your project (i.e. the 194 | directory containing your manage.py script). 195 | 196 | APP_DIR for app-provided config files, the top-level 197 | directory containing the application code. 198 | 199 | PYTHON full path to the current python interpreter. 200 | 201 | SUPERVISOR_OPTIONS the command-line options passed to manage.py. 202 | 203 | settings the Django settings module, as seen by your code. 204 | 205 | environ the os.environ dict, as seen by your code. 206 | 207 | If your project has other configuration files that need to interpolate these 208 | values, you can refer to them via the "templated" filter, like this:: 209 | 210 | [program:nginx] 211 | command=nginx -c {{ "nginx.conf"|templated }} 212 | 213 | The file path is relative to your project directory. Django-supervisor will 214 | read the specified file, pass it through its templating logic, write out a 215 | matching "nginx.conf.templated" file, and insert the path to this file as the 216 | result of the filter. 217 | 218 | 219 | Defaults, Overrides and Excludes 220 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 221 | 222 | Django-supervisor recognises some special config-file options that are useful 223 | when merging multiple app-specific and project-specific configuration files. 224 | 225 | The [program:__defaults__] section can be used to provide default options 226 | for all other [program] sections. These options will only be used if none 227 | of the config files found by django-supervisor provide that option for 228 | a specific program. 229 | 230 | The [program:__overrides__] section can be used to override options for all 231 | configured programs. These options will be applied to all processes regardless 232 | of what any other config file has to say. 233 | 234 | Finally, you can completely disable a [program] section by setting the option 235 | "exclude" to true. This is mostly useful for disabling process definitions 236 | provided by a third-party application. 237 | 238 | Here's an example config file that shows them all in action:: 239 | 240 | ; We want all programs to redirect stderr by default, 241 | ; unless specifically configured otherwise. 242 | [program:__defaults__] 243 | redirect_stderr=true 244 | 245 | ; We force all programs to run as user "nobody" 246 | [program:__overrides__] 247 | user=nobody 248 | 249 | ; Don't reload programs when python code changes. 250 | [program:autoreload] 251 | exclude=true 252 | 253 | 254 | 255 | Automatic Control Socket Config 256 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 257 | 258 | The supervisord and supervisorctl programs interact with each other via an 259 | XML-RPC control socket. This provides a great deal of flexibility and control 260 | over security, but you have to configure it just so or things won't work. 261 | 262 | For convenience during development, django-supervisor provides automatic 263 | control socket configuration. By default it binds the server to localhost 264 | on a fixed-but-randomish port, and sets up a username and password based on 265 | settings.SECRET_KEY. 266 | 267 | For production deployment, you might like to reconfigure this by setting up 268 | the [inet_http_server] or [unix_http_server] sections. Django-supervisor 269 | will honour any such settings you provide. 270 | 271 | 272 | 273 | Autoreload 274 | ~~~~~~~~~~ 275 | 276 | When running in debug mode, django-supervisor automatically defines a process 277 | named "autoreload". This is very similar to the auto-reloading feature of 278 | the Django development server, but works across all configured processes. 279 | For example, this will let you automatically restart both the dev server and 280 | celeryd whenever your code changes. 281 | 282 | To prevent an individual program from being auto-reloaded, set its "autoreload" 283 | option to false:: 284 | 285 | [program:non-python-related] 286 | autoreload=false 287 | 288 | To switch off the autoreload process entirely, you can pass the --noreload 289 | option to supervisor or just exclude it in your project config file like so:: 290 | 291 | [program:autoreload] 292 | exclude=true 293 | 294 | """ 295 | 296 | __ver_major__ = 0 297 | __ver_minor__ = 4 298 | __ver_patch__ = 0 299 | __ver_sub__ = "" 300 | __version__ = "%d.%d.%d%s" % (__ver_major__,__ver_minor__,__ver_patch__,__ver_sub__) 301 | 302 | 303 | -------------------------------------------------------------------------------- /djsupervisor/config.py: -------------------------------------------------------------------------------- 1 | """ 2 | 3 | djsupervisor.config: config loading and merging code for djsupervisor 4 | ---------------------------------------------------------------------- 5 | 6 | The code in this module is responsible for finding the supervisord.conf 7 | files from all installed apps, merging them together with the config 8 | files from your project and any options specified on the command-line, 9 | and producing a final config file to control supervisord/supervisorctl. 10 | 11 | """ 12 | 13 | import sys 14 | import os 15 | import hashlib 16 | 17 | try: 18 | from cStringIO import StringIO 19 | except ImportError: 20 | from StringIO import StringIO 21 | 22 | from ConfigParser import RawConfigParser, NoSectionError, NoOptionError 23 | 24 | from django import template 25 | from django.conf import settings 26 | from importlib import import_module 27 | 28 | from djsupervisor.templatetags import djsupervisor_tags 29 | 30 | CONFIG_FILE = getattr(settings, "SUPERVISOR_CONFIG_FILE", "supervisord.conf") 31 | 32 | 33 | def get_merged_config(**options): 34 | """Get the final merged configuration for supvervisord, as a string. 35 | 36 | This is the top-level function exported by this module. It combines 37 | the config file from the main project with default settings and those 38 | specified in the command-line, processes various special section names, 39 | and returns the resulting configuration as a string. 40 | """ 41 | # Find and load the containing project module. 42 | # This can be specified explicity using the --project-dir option. 43 | # Otherwise, we attempt to guess by looking for the manage.py file. 44 | project_dir = options.get("project_dir") 45 | if project_dir is None: 46 | project_dir = guess_project_dir() 47 | # Find the config file to load. 48 | # Default to /supervisord.conf. 49 | config_file = options.get("config_file") 50 | if config_file is None: 51 | config_file = os.path.join(project_dir,CONFIG_FILE) 52 | # Build the default template context variables. 53 | # This is mostly useful information about the project and environment. 54 | ctx = { 55 | "PROJECT_DIR": project_dir, 56 | "PYTHON": os.path.realpath(os.path.abspath(sys.executable)), 57 | "SUPERVISOR_OPTIONS": rerender_options(options), 58 | "settings": settings, 59 | "environ": os.environ, 60 | } 61 | # Initialise the ConfigParser. 62 | # Fortunately for us, ConfigParser has merge-multiple-config-files 63 | # functionality built into it. You just read each file in turn, and 64 | # values from later files overwrite values from former. 65 | cfg = RawConfigParser() 66 | # Start from the default configuration options. 67 | data = render_config(DEFAULT_CONFIG,ctx) 68 | cfg.readfp(StringIO(data)) 69 | # Add in the project-specific config file. 70 | with open(config_file,"r") as f: 71 | data = render_config(f.read(),ctx) 72 | cfg.readfp(StringIO(data)) 73 | # Add in the options specified on the command-line. 74 | cfg.readfp(StringIO(get_config_from_options(**options))) 75 | # Add options from [program:__defaults__] to each program section 76 | # if it happens to be missing that option. 77 | PROG_DEFAULTS = "program:__defaults__" 78 | if cfg.has_section(PROG_DEFAULTS): 79 | for option in cfg.options(PROG_DEFAULTS): 80 | default = cfg.get(PROG_DEFAULTS,option) 81 | for section in cfg.sections(): 82 | if section.startswith("program:"): 83 | if not cfg.has_option(section,option): 84 | cfg.set(section,option,default) 85 | cfg.remove_section(PROG_DEFAULTS) 86 | # Add options from [program:__overrides__] to each program section 87 | # regardless of whether they already have that option. 88 | PROG_OVERRIDES = "program:__overrides__" 89 | if cfg.has_section(PROG_OVERRIDES): 90 | for option in cfg.options(PROG_OVERRIDES): 91 | override = cfg.get(PROG_OVERRIDES,option) 92 | for section in cfg.sections(): 93 | if section.startswith("program:"): 94 | cfg.set(section,option,override) 95 | cfg.remove_section(PROG_OVERRIDES) 96 | # Make sure we've got a port configured for supervisorctl to 97 | # talk to supervisord. It's passworded based on secret key. 98 | # If they have configured a unix socket then use that, otherwise 99 | # use an inet server on localhost at fixed-but-randomish port. 100 | username = hashlib.md5(settings.SECRET_KEY).hexdigest()[:7] 101 | password = hashlib.md5(username).hexdigest() 102 | if cfg.has_section("unix_http_server"): 103 | set_if_missing(cfg,"unix_http_server","username",username) 104 | set_if_missing(cfg,"unix_http_server","password",password) 105 | serverurl = "unix://" + cfg.get("unix_http_server","file") 106 | else: 107 | # This picks a "random" port in the 9000 range to listen on. 108 | # It's derived from the secret key, so it's stable for a given 109 | # project but multiple projects are unlikely to collide. 110 | port = int(hashlib.md5(password).hexdigest()[:3],16) % 1000 111 | addr = "127.0.0.1:9%03d" % (port,) 112 | set_if_missing(cfg,"inet_http_server","port",addr) 113 | set_if_missing(cfg,"inet_http_server","username",username) 114 | set_if_missing(cfg,"inet_http_server","password",password) 115 | serverurl = "http://" + cfg.get("inet_http_server","port") 116 | set_if_missing(cfg,"supervisorctl","serverurl",serverurl) 117 | set_if_missing(cfg,"supervisorctl","username",username) 118 | set_if_missing(cfg,"supervisorctl","password",password) 119 | set_if_missing(cfg,"rpcinterface:supervisor", 120 | "supervisor.rpcinterface_factory", 121 | "supervisor.rpcinterface:make_main_rpcinterface") 122 | # Remove any [program:] sections with exclude=true 123 | for section in cfg.sections(): 124 | try: 125 | if cfg.getboolean(section,"exclude"): 126 | cfg.remove_section(section) 127 | except NoOptionError: 128 | pass 129 | # Sanity-check to give better error messages. 130 | for section in cfg.sections(): 131 | if section.startswith("program:"): 132 | if not cfg.has_option(section,"command"): 133 | msg = "Process name '%s' has no command configured" 134 | raise ValueError(msg % (section.split(":",1)[-1])) 135 | # Write it out to a StringIO and return the data 136 | s = StringIO() 137 | cfg.write(s) 138 | return s.getvalue() 139 | 140 | 141 | def render_config(data,ctx): 142 | """Render the given config data using Django's template system. 143 | 144 | This function takes a config data string and a dict of context variables, 145 | renders the data through Django's template system, and returns the result. 146 | """ 147 | djsupervisor_tags.current_context = ctx 148 | data = "{% load djsupervisor_tags %}" + data 149 | t = template.Template(data) 150 | c = template.Context(ctx) 151 | return t.render(c).encode("ascii") 152 | 153 | 154 | def get_config_from_options(**options): 155 | """Get config file fragment reflecting command-line options.""" 156 | data = [] 157 | # Set whether or not to daemonize. 158 | # Unlike supervisord, our default is to stay in the foreground. 159 | data.append("[supervisord]\n") 160 | if options.get("daemonize",False): 161 | data.append("nodaemon=false\n") 162 | else: 163 | data.append("nodaemon=true\n") 164 | if options.get("pidfile",None): 165 | data.append("pidfile=%s\n" % (options["pidfile"],)) 166 | if options.get("logfile",None): 167 | data.append("logfile=%s\n" % (options["logfile"],)) 168 | # Set which programs to launch automatically on startup. 169 | for progname in options.get("launch",None) or []: 170 | data.append("[program:%s]\nautostart=true\n" % (progname,)) 171 | for progname in options.get("nolaunch",None) or []: 172 | data.append("[program:%s]\nautostart=false\n" % (progname,)) 173 | # Set which programs to include/exclude from the config 174 | for progname in options.get("include",None) or []: 175 | data.append("[program:%s]\nexclude=false\n" % (progname,)) 176 | for progname in options.get("exclude",None) or []: 177 | data.append("[program:%s]\nexclude=true\n" % (progname,)) 178 | # Set which programs to autoreload when code changes. 179 | # When this option is specified, the default for all other 180 | # programs becomes autoreload=false. 181 | if options.get("autoreload",None): 182 | data.append("[program:autoreload]\nexclude=false\nautostart=true\n") 183 | data.append("[program:__defaults__]\nautoreload=false\n") 184 | for progname in options["autoreload"]: 185 | data.append("[program:%s]\nautoreload=true\n" % (progname,)) 186 | # Set whether to use the autoreloader at all. 187 | if options.get("noreload",False): 188 | data.append("[program:autoreload]\nexclude=true\n") 189 | return "".join(data) 190 | 191 | 192 | def guess_project_dir(): 193 | """Find the top-level Django project directory. 194 | 195 | This function guesses the top-level Django project directory based on 196 | the current environment. It looks for module containing the currently- 197 | active settings module, in both pre-1.4 and post-1.4 layours. 198 | """ 199 | projname = settings.SETTINGS_MODULE.split(".",1)[0] 200 | projmod = import_module(projname) 201 | projdir = os.path.dirname(projmod.__file__) 202 | 203 | # For Django 1.3 and earlier, the manage.py file was located 204 | # in the same directory as the settings file. 205 | if os.path.isfile(os.path.join(projdir,"manage.py")): 206 | return projdir 207 | 208 | # For Django 1.4 and later, the manage.py file is located in 209 | # the directory *containing* the settings file. 210 | projdir = os.path.abspath(os.path.join(projdir, os.path.pardir)) 211 | if os.path.isfile(os.path.join(projdir,"manage.py")): 212 | return projdir 213 | 214 | msg = "Unable to determine the Django project directory;"\ 215 | " use --project-dir to specify it" 216 | raise RuntimeError(msg) 217 | 218 | 219 | def set_if_missing(cfg,section,option,value): 220 | """If the given option is missing, set to the given value.""" 221 | try: 222 | cfg.get(section,option) 223 | except NoSectionError: 224 | cfg.add_section(section) 225 | cfg.set(section,option,value) 226 | except NoOptionError: 227 | cfg.set(section,option,value) 228 | 229 | 230 | def rerender_options(options): 231 | """Helper function to re-render command-line options. 232 | 233 | This assumes that command-line options use the same name as their 234 | key in the options dictionary. 235 | """ 236 | args = [] 237 | for name,value in options.iteritems(): 238 | name = name.replace("_","-") 239 | if value is None: 240 | pass 241 | elif isinstance(value,bool): 242 | if value: 243 | args.append("--%s" % (name,)) 244 | elif isinstance(value,list): 245 | for item in value: 246 | args.append("--%s=%s" % (name,item)) 247 | else: 248 | args.append("--%s=%s" % (name,value)) 249 | return " ".join(args) 250 | 251 | 252 | # These are the default configuration options provided by djsupervisor. 253 | # 254 | DEFAULT_CONFIG = """ 255 | 256 | ; In debug mode, we watch for changes in the project directory and inside 257 | ; any installed apps. When something changes, restart all processes. 258 | [program:autoreload] 259 | command={{ PYTHON }} {{ PROJECT_DIR }}/manage.py supervisor {{ SUPERVISOR_OPTIONS }} autoreload 260 | autoreload=true 261 | {% if not settings.DEBUG %} 262 | exclude=true 263 | {% endif %} 264 | 265 | ; All programs are auto-reloaded by default. 266 | [program:__defaults__] 267 | autoreload=true 268 | redirect_stderr=true 269 | 270 | [supervisord] 271 | {% if settings.DEBUG %} 272 | loglevel=debug 273 | {% endif %} 274 | 275 | """ 276 | -------------------------------------------------------------------------------- /djsupervisor/events.py: -------------------------------------------------------------------------------- 1 | 2 | import time 3 | 4 | from watchdog.events import PatternMatchingEventHandler 5 | 6 | 7 | class CallbackModifiedHandler(PatternMatchingEventHandler): 8 | """ 9 | A pattern matching event handler that calls the provided 10 | callback when a file is modified. 11 | """ 12 | def __init__(self, callback, *args, **kwargs): 13 | self.callback = callback 14 | self.repeat_delay = kwargs.pop("repeat_delay", 0) 15 | self.last_fired_time = 0 16 | super(CallbackModifiedHandler, self).__init__(*args, **kwargs) 17 | 18 | def on_modified(self, event): 19 | super(CallbackModifiedHandler, self).on_modified(event) 20 | now = time.time() 21 | if self.last_fired_time + self.repeat_delay < now: 22 | if not event.is_directory: 23 | self.last_fired_time = now 24 | self.callback() 25 | -------------------------------------------------------------------------------- /djsupervisor/management/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /djsupervisor/management/commands/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rfk/django-supervisor/545a379d4a73ed2ae21c4aee6b8009ded8aeedc6/djsupervisor/management/commands/__init__.py -------------------------------------------------------------------------------- /djsupervisor/management/commands/supervisor.py: -------------------------------------------------------------------------------- 1 | """ 2 | 3 | djsupervisor.management.commands.supervisor: djsupervisor mangement command 4 | ---------------------------------------------------------------------------- 5 | 6 | This module defines the main management command for the djsupervisor app. 7 | The "supervisor" command acts like a combination of the supervisord and 8 | supervisorctl programs, allowing you to start up, shut down and manage all 9 | of the proceses defined in your Django project. 10 | 11 | The "supervisor" command suports several modes of operation: 12 | 13 | * called without arguments, it launches supervisord to spawn processes. 14 | 15 | * called with the single argument "getconfig", is prints the merged 16 | supervisord config to stdout. 17 | 18 | * called with the single argument "autoreload", it watches for changes 19 | to python modules and restarts all processes if things change. 20 | 21 | * called with any other arguments, it passes them on the supervisorctl. 22 | 23 | """ 24 | 25 | from __future__ import absolute_import, with_statement 26 | 27 | import sys 28 | import os 29 | import time 30 | from textwrap import dedent 31 | import traceback 32 | from ConfigParser import RawConfigParser, NoOptionError 33 | try: 34 | from cStringIO import StringIO 35 | except ImportError: 36 | from StringIO import StringIO 37 | 38 | from supervisor import supervisord, supervisorctl 39 | 40 | from django.core.management.base import BaseCommand, CommandError 41 | from django.conf import settings 42 | 43 | from djsupervisor.config import get_merged_config 44 | from djsupervisor.events import CallbackModifiedHandler 45 | 46 | AUTORELOAD_PATTERNS = getattr(settings, "SUPERVISOR_AUTORELOAD_PATTERNS", 47 | ['*.py']) 48 | AUTORELOAD_IGNORE = getattr(settings, "SUPERVISOR_AUTORELOAD_IGNORE_PATTERNS", 49 | [".*", "#*", "*~"]) 50 | 51 | class Command(BaseCommand): 52 | 53 | args = "[ [, ...]]" 54 | 55 | help = dedent(""" 56 | Manage processes with supervisord. 57 | 58 | With no arguments, this spawns the configured background processes. 59 | 60 | With a command argument it lets you control the running processes. 61 | Available commands include: 62 | 63 | supervisor getconfig 64 | supervisor shell 65 | supervisor start 66 | supervisor stop 67 | supervisor restart 68 | 69 | """).strip() 70 | 71 | def add_arguments(self, parser): 72 | parser.add_argument('ctl-command', nargs='*') 73 | parser.add_argument( 74 | "--daemonize", 75 | "-d", 76 | action="store_true", 77 | dest="daemonize", 78 | default=False, 79 | help="daemonize before launching subprocessess" 80 | ) 81 | parser.add_argument( 82 | "--pidfile", 83 | action="store", 84 | dest="pidfile", 85 | help="store daemon PID in this file" 86 | ) 87 | parser.add_argument( 88 | "--logfile", 89 | action="store", 90 | dest="logfile", 91 | help="write logging output to this file" 92 | ) 93 | parser.add_argument( 94 | "--project-dir", 95 | action="store", 96 | dest="project_dir", 97 | help="the root directory for the django project" 98 | " (by default this is guessed from the location" 99 | " of manage.py)" 100 | ) 101 | parser.add_argument( 102 | "--config-file", 103 | action="store", 104 | dest="config_file", 105 | help="the supervisord configuration file to load" 106 | " (by default this is /supervisord.conf)" 107 | ) 108 | parser.add_argument( 109 | "--launch", 110 | "-l", 111 | metavar="PROG", 112 | action="append", 113 | dest="launch", 114 | help="launch program automatically at supervisor startup" 115 | ) 116 | parser.add_argument( 117 | "--nolaunch", 118 | "-n", 119 | metavar="PROG", 120 | action="append", 121 | dest="nolaunch", 122 | help="don't launch program automatically at supervisor startup" 123 | ) 124 | parser.add_argument( 125 | "--exclude", 126 | "-x", 127 | metavar="PROG", 128 | action="append", 129 | dest="exclude", 130 | help="exclude program from supervisor config" 131 | ) 132 | parser.add_argument( 133 | "--include", 134 | "-i", 135 | metavar="PROG", 136 | action="append", 137 | dest="include", 138 | help="don't exclude program from supervisor config" 139 | ) 140 | parser.add_argument( 141 | "--autoreload", 142 | "-r", 143 | metavar="PROG", 144 | action="append", 145 | dest="autoreload", 146 | help="restart program automatically when code files change" 147 | " (debug mode only;" 148 | " if not set then all programs are autoreloaded)" 149 | ) 150 | parser.add_argument( 151 | "--noreload", 152 | action="store_true", 153 | dest="noreload", 154 | help="don't restart processes when code files change" 155 | ) 156 | 157 | def run_from_argv(self,argv): 158 | # Customize option handling so that it doesn't choke on any 159 | # options that are being passed straight on to supervisorctl. 160 | # Basically, we insert "--" before the supervisorctl command. 161 | # 162 | # For example, automatically turn this: 163 | # manage.py supervisor -l celeryd tail -f celeryd 164 | # Into this: 165 | # manage.py supervisor -l celeryd -- tail -f celeryd 166 | # 167 | i = 2 168 | while i < len(argv): 169 | arg = argv[i] 170 | if arg == "--": 171 | break 172 | elif arg.startswith("--"): 173 | i += 1 174 | elif arg.startswith("-"): 175 | i += 2 176 | else: 177 | argv = argv[:i] + ["--"] + argv[i:] 178 | break 179 | return super(Command,self).run_from_argv(argv) 180 | 181 | def handle(self, *args, **options): 182 | args = args or tuple(options.pop('ctl-command')) 183 | 184 | # We basically just construct the merged supervisord.conf file 185 | # and forward it on to either supervisord or supervisorctl. 186 | # Due to some very nice engineering on behalf of supervisord authors, 187 | # you can pass it a StringIO instance for the "-c" command-line 188 | # option. Saves us having to write the config to a tempfile. 189 | cfg_file = OnDemandStringIO(get_merged_config, **options) 190 | # With no arguments, we launch the processes under supervisord. 191 | if not args: 192 | return supervisord.main(("-c",cfg_file)) 193 | # With arguments, the first arg specifies the sub-command 194 | # Some commands we implement ourself with _handle_. 195 | # The rest we just pass on to supervisorctl. 196 | if not args[0].isalnum(): 197 | raise ValueError("Unknown supervisor command: %s" % (args[0],)) 198 | methname = "_handle_%s" % (args[0],) 199 | try: 200 | method = getattr(self,methname) 201 | except AttributeError: 202 | return supervisorctl.main(("-c",cfg_file) + args) 203 | else: 204 | return method(cfg_file,*args[1:],**options) 205 | 206 | # 207 | # The following methods implement custom sub-commands. 208 | # 209 | 210 | def _handle_shell(self,cfg_file,*args,**options): 211 | """Command 'supervisord shell' runs the interactive command shell.""" 212 | args = ("--interactive",) + args 213 | return supervisorctl.main(("-c",cfg_file) + args) 214 | 215 | def _handle_getconfig(self,cfg_file,*args,**options): 216 | """Command 'supervisor getconfig' prints merged config to stdout.""" 217 | if args: 218 | raise CommandError("supervisor getconfig takes no arguments") 219 | print cfg_file.read() 220 | return 0 221 | 222 | def _handle_autoreload(self,cfg_file,*args,**options): 223 | """Command 'supervisor autoreload' watches for code changes. 224 | 225 | This command provides a simulation of the Django dev server's 226 | auto-reloading mechanism that will restart all supervised processes. 227 | 228 | It's not quite as accurate as Django's autoreloader because it runs 229 | in a separate process, so it doesn't know the precise set of modules 230 | that have been loaded. Instead, it tries to watch all python files 231 | that are "nearby" the files loaded at startup by Django. 232 | """ 233 | if args: 234 | raise CommandError("supervisor autoreload takes no arguments") 235 | live_dirs = self._find_live_code_dirs() 236 | reload_progs = self._get_autoreload_programs(cfg_file) 237 | 238 | def autoreloader(): 239 | """ 240 | Forks a subprocess to make the restart call. 241 | Otherwise supervisord might kill us and cancel the restart! 242 | """ 243 | if os.fork() == 0: 244 | sys.exit(self.handle("restart", *reload_progs, **options)) 245 | 246 | # Call the autoreloader callback whenever a .py file changes. 247 | # To prevent thrashing, limit callbacks to one per second. 248 | handler = CallbackModifiedHandler(callback=autoreloader, 249 | repeat_delay=1, 250 | patterns=AUTORELOAD_PATTERNS, 251 | ignore_patterns=AUTORELOAD_IGNORE, 252 | ignore_directories=True) 253 | 254 | # Try to add watches using the platform-specific observer. 255 | # If this fails, print a warning and fall back to the PollingObserver. 256 | # This will avoid errors with e.g. too many inotify watches. 257 | from watchdog.observers import Observer 258 | from watchdog.observers.polling import PollingObserver 259 | 260 | observer = None 261 | for ObserverCls in (Observer, PollingObserver): 262 | observer = ObserverCls() 263 | try: 264 | for live_dir in set(live_dirs): 265 | observer.schedule(handler, live_dir, True) 266 | break 267 | except Exception: 268 | print>>sys.stderr, "COULD NOT WATCH FILESYSTEM USING" 269 | print>>sys.stderr, "OBSERVER CLASS: ", ObserverCls 270 | traceback.print_exc() 271 | observer.start() 272 | observer.stop() 273 | 274 | # Fail out if none of the observers worked. 275 | if observer is None: 276 | print>>sys.stderr, "COULD NOT WATCH FILESYSTEM" 277 | return 1 278 | 279 | # Poll if we have an observer. 280 | # TODO: Is this sleep necessary? Or will it suffice 281 | # to block indefinitely on something and wait to be killed? 282 | observer.start() 283 | try: 284 | while True: 285 | time.sleep(1) 286 | except KeyboardInterrupt: 287 | observer.stop() 288 | observer.join() 289 | return 0 290 | 291 | def _get_autoreload_programs(self,cfg_file): 292 | """Get the set of programs to auto-reload when code changes. 293 | 294 | Such programs will have autoreload=true in their config section. 295 | This can be affected by config file sections or command-line 296 | arguments, so we need to read it out of the merged config. 297 | """ 298 | cfg = RawConfigParser() 299 | cfg.readfp(cfg_file) 300 | reload_progs = [] 301 | for section in cfg.sections(): 302 | if section.startswith("program:"): 303 | try: 304 | if cfg.getboolean(section,"autoreload"): 305 | reload_progs.append(section.split(":",1)[1]) 306 | except NoOptionError: 307 | pass 308 | return reload_progs 309 | 310 | def _find_live_code_dirs(self): 311 | """Find all directories in which we might have live python code. 312 | 313 | This walks all of the currently-imported modules and adds their 314 | containing directory to the list of live dirs. After normalization 315 | and de-duplication, we get a pretty good approximation of the 316 | directories on sys.path that are actively in use. 317 | """ 318 | live_dirs = [] 319 | for mod in sys.modules.values(): 320 | # Get the directory containing that module. 321 | # This is deliberately casting a wide net. 322 | try: 323 | dirnm = os.path.dirname(mod.__file__) 324 | except AttributeError: 325 | continue 326 | # Normalize it for comparison purposes. 327 | dirnm = os.path.realpath(os.path.abspath(dirnm)) 328 | if not dirnm.endswith(os.sep): 329 | dirnm += os.sep 330 | # Check that it's not an egg or some other wierdness 331 | if not os.path.isdir(dirnm): 332 | continue 333 | # If it's a subdir of one we've already found, ignore it. 334 | for dirnm2 in live_dirs: 335 | if dirnm.startswith(dirnm2): 336 | break 337 | else: 338 | # Remove any ones we've found that are subdirs of it. 339 | live_dirs = [dirnm2 for dirnm2 in live_dirs\ 340 | if not dirnm2.startswith(dirnm)] 341 | live_dirs.append(dirnm) 342 | return live_dirs 343 | 344 | 345 | class OnDemandStringIO(object): 346 | """StringIO standin that demand-loads its contents and resets on EOF. 347 | 348 | This class is a little bit of a hack to make supervisord reloading work 349 | correctly. It provides the readlines() method expected by supervisord's 350 | config reader, but it resets itself after indicating end-of-file. If 351 | the supervisord process then SIGHUPs and tries to read the config again, 352 | it will be re-created and available for updates. 353 | """ 354 | 355 | def __init__(self, callback, *args, **kwds): 356 | self._fp = None 357 | self.callback = callback 358 | self.args = args 359 | self.kwds = kwds 360 | 361 | @property 362 | def fp(self): 363 | if self._fp is None: 364 | self._fp = StringIO(self.callback(*self.args, **self.kwds)) 365 | return self._fp 366 | 367 | def read(self, *args, **kwds): 368 | data = self.fp.read(*args, **kwds) 369 | if not data: 370 | self._fp = None 371 | return data 372 | 373 | def readline(self, *args, **kwds): 374 | line = self.fp.readline(*args, **kwds) 375 | if not line: 376 | self._fp = None 377 | return line 378 | -------------------------------------------------------------------------------- /djsupervisor/models.py: -------------------------------------------------------------------------------- 1 | """ 2 | 3 | djsupervisor.models: fake models file for djsupervisor 4 | ------------------------------------------------------- 5 | 6 | This application doesn't actually define any models. But Django will freak out 7 | if it's missing a "models.py file, so here we are... 8 | 9 | """ 10 | 11 | -------------------------------------------------------------------------------- /djsupervisor/templatetags/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rfk/django-supervisor/545a379d4a73ed2ae21c4aee6b8009ded8aeedc6/djsupervisor/templatetags/__init__.py -------------------------------------------------------------------------------- /djsupervisor/templatetags/djsupervisor_tags.py: -------------------------------------------------------------------------------- 1 | """ 2 | 3 | djsupervisor.templatetags.djsupervisor_tags: custom template tags 4 | ------------------------------------------------------------------ 5 | 6 | This module defines a custom template filter "templated" which can be used 7 | to apply the djsupervisor templating logic to other config files in your 8 | project. 9 | """ 10 | 11 | import os 12 | import shutil 13 | 14 | from django import template 15 | register = template.Library() 16 | 17 | current_context = None 18 | 19 | @register.filter 20 | def templated(template_path): 21 | import djsupervisor.config 22 | # Interpret paths relative to the project directory. 23 | project_dir = current_context["PROJECT_DIR"] 24 | full_path = os.path.join(project_dir, template_path) 25 | templated_path = full_path + ".templated" 26 | # If the target file doesn't exist, we will copy over source file metadata. 27 | # Do so *after* writing the file, as the changed permissions might e.g. 28 | # affect our ability to write to it. 29 | created = not os.path.exists(templated_path) 30 | # Read and process the source file. 31 | with open(full_path, "r") as f: 32 | templated = djsupervisor.config.render_config(f.read(), current_context) 33 | # Write it out to the corresponding .templated file. 34 | with open(templated_path, "w") as f: 35 | f.write(templated) 36 | # Copy metadata if necessary. 37 | if created: 38 | try: 39 | info = os.stat(full_path) 40 | shutil.copystat(full_path, templated_path) 41 | os.chown(templated_path, info.st_uid, info.st_gid) 42 | except EnvironmentError: 43 | pass 44 | return templated_path 45 | -------------------------------------------------------------------------------- /djsupervisor/tests.py: -------------------------------------------------------------------------------- 1 | """ 2 | 3 | djsupervisor.tests: testcases for djsupervisor 4 | ----------------------------------------------- 5 | 6 | These are just some simple tests for the moment, more to come... 7 | 8 | """ 9 | 10 | import os 11 | import sys 12 | import difflib 13 | import unittest 14 | 15 | import djsupervisor 16 | 17 | 18 | class TestDJSupervisorDocs(unittest.TestCase): 19 | 20 | def test_readme_matches_docstring(self): 21 | """Ensure that the README is in sync with the docstring. 22 | 23 | This test should always pass; if the README is out of sync it just 24 | updates it with the contents of djsupervisor.__doc__. 25 | """ 26 | dirname = os.path.dirname 27 | readme = os.path.join(dirname(dirname(__file__)),"README.rst") 28 | if not os.path.isfile(readme): 29 | f = open(readme,"wb") 30 | f.write(djsupervisor.__doc__.encode()) 31 | f.close() 32 | else: 33 | f = open(readme,"rb") 34 | if f.read() != djsupervisor.__doc__: 35 | f.close() 36 | f = open(readme,"wb") 37 | f.write(djsupervisor.__doc__.encode()) 38 | f.close() 39 | 40 | 41 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rfk/django-supervisor/545a379d4a73ed2ae21c4aee6b8009ded8aeedc6/setup.cfg -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | 2 | import sys 3 | setup_kwds = {} 4 | if sys.version_info > (3,): 5 | from setuptools import setup 6 | setup_kwds["test_suite"] = "djsupervisor.tests" 7 | setup_kwds["use_2to3"] = True 8 | else: 9 | try: 10 | from setuptools import setup 11 | except ImportError: 12 | from distutils.core import setup 13 | 14 | 15 | try: 16 | next = next 17 | except NameError: 18 | def next(i): 19 | return i.next() 20 | 21 | 22 | # Try to get "__version__" from the module itself. 23 | info = {} 24 | src = open("djsupervisor/__init__.py") 25 | lines = [] 26 | ln = next(src) 27 | while "__version__" not in ln: 28 | lines.append(ln) 29 | ln = next(src) 30 | while "__version__" in ln: 31 | lines.append(ln) 32 | ln = next(src) 33 | exec("".join(lines),info) 34 | 35 | 36 | NAME = "django-supervisor" 37 | VERSION = info["__version__"] 38 | DESCRIPTION = "easy integration between djangocl and supervisord" 39 | LONG_DESC = info["__doc__"] 40 | AUTHOR = "Ryan Kelly" 41 | AUTHOR_EMAIL = "ryan@rfk.id.au" 42 | URL="http://github.com/rfk/django-supervisor" 43 | LICENSE = "MIT" 44 | KEYWORDS = "django supervisord process" 45 | PACKAGES = ["djsupervisor","djsupervisor.management", 46 | "djsupervisor.management.commands", "djsupervisor.templatetags"] 47 | PACKAGE_DATA = { 48 | "djsupervisor": ["contrib/*/supervisord.conf",], 49 | } 50 | CLASSIFIERS = [ 51 | "Programming Language :: Python", 52 | "Programming Language :: Python :: 2", 53 | "License :: OSI Approved", 54 | "License :: OSI Approved :: MIT License", 55 | "Development Status :: 3 - Alpha", 56 | "Intended Audience :: Developers", 57 | ] 58 | 59 | setup( 60 | name=NAME, 61 | version=VERSION, 62 | author=AUTHOR, 63 | author_email=AUTHOR_EMAIL, 64 | url=URL, 65 | description=DESCRIPTION, 66 | long_description=LONG_DESC, 67 | license=LICENSE, 68 | keywords=KEYWORDS, 69 | packages=PACKAGES, 70 | package_data=PACKAGE_DATA, 71 | classifiers=CLASSIFIERS, 72 | install_requires=[ 73 | "supervisor", 74 | "watchdog", 75 | ], 76 | **setup_kwds 77 | ) 78 | 79 | --------------------------------------------------------------------------------