diff --git a/docker/alpine/web2py-gevent/Dockerfile b/docker/alpine/web2py-gevent/Dockerfile old mode 100755 new mode 100644 diff --git a/docker/alpine/web2py-gevent/README.md b/docker/alpine/web2py-gevent/README.md old mode 100755 new mode 100644 diff --git a/docker/alpine/web2py-gevent/docker-compose.yml b/docker/alpine/web2py-gevent/docker-compose.yml old mode 100755 new mode 100644 diff --git a/docker/alpine/web2py-gunicorn/Dockerfile b/docker/alpine/web2py-gunicorn/Dockerfile old mode 100755 new mode 100644 diff --git a/docker/alpine/web2py-gunicorn/README.md b/docker/alpine/web2py-gunicorn/README.md old mode 100755 new mode 100644 diff --git a/docker/alpine/web2py-gunicorn/docker-compose.yml b/docker/alpine/web2py-gunicorn/docker-compose.yml old mode 100755 new mode 100644 diff --git a/docker/alpine/web2py-paste/Dockerfile b/docker/alpine/web2py-paste/Dockerfile old mode 100755 new mode 100644 diff --git a/docker/alpine/web2py-paste/README.md b/docker/alpine/web2py-paste/README.md old mode 100755 new mode 100644 diff --git a/docker/alpine/web2py-paste/docker-compose.yml b/docker/alpine/web2py-paste/docker-compose.yml old mode 100755 new mode 100644 diff --git a/docker/alpine/web2py-rocket-ssl/Dockerfile b/docker/alpine/web2py-rocket-ssl/Dockerfile old mode 100755 new mode 100644 diff --git a/docker/alpine/web2py-rocket-ssl/README.md b/docker/alpine/web2py-rocket-ssl/README.md old mode 100755 new mode 100644 diff --git a/docker/alpine/web2py-rocket-ssl/docker-compose.yml b/docker/alpine/web2py-rocket-ssl/docker-compose.yml old mode 100755 new mode 100644 diff --git a/docker/alpine/web2py-rocket/Dockerfile b/docker/alpine/web2py-rocket/Dockerfile old mode 100755 new mode 100644 diff --git a/docker/alpine/web2py-rocket/README.md b/docker/alpine/web2py-rocket/README.md old mode 100755 new mode 100644 diff --git a/docker/alpine/web2py-rocket/docker-compose.yml b/docker/alpine/web2py-rocket/docker-compose.yml old mode 100755 new mode 100644 diff --git a/docker/alpine/web2py-tornado/Dockerfile b/docker/alpine/web2py-tornado/Dockerfile old mode 100755 new mode 100644 diff --git a/docker/alpine/web2py-tornado/README.md b/docker/alpine/web2py-tornado/README.md old mode 100755 new mode 100644 diff --git a/docker/alpine/web2py-tornado/docker-compose.yml b/docker/alpine/web2py-tornado/docker-compose.yml old mode 100755 new mode 100644 diff --git a/docker/alpine/web2py-waitress/Dockerfile b/docker/alpine/web2py-waitress/Dockerfile old mode 100755 new mode 100644 diff --git a/docker/alpine/web2py-waitress/README.md b/docker/alpine/web2py-waitress/README.md old mode 100755 new mode 100644 diff --git a/docker/alpine/web2py-waitress/docker-compose.yml b/docker/alpine/web2py-waitress/docker-compose.yml old mode 100755 new mode 100644 diff --git a/docker/alpine/web2py-wsgiref/Dockerfile b/docker/alpine/web2py-wsgiref/Dockerfile old mode 100755 new mode 100644 diff --git a/docker/alpine/web2py-wsgiref/README.md b/docker/alpine/web2py-wsgiref/README.md old mode 100755 new mode 100644 diff --git a/docker/alpine/web2py-wsgiref/docker-compose.yml b/docker/alpine/web2py-wsgiref/docker-compose.yml old mode 100755 new mode 100644 diff --git a/docker/centos/web2py-eventlet/Dockerfile b/docker/centos/web2py-eventlet/Dockerfile old mode 100755 new mode 100644 diff --git a/docker/centos/web2py-eventlet/README.md b/docker/centos/web2py-eventlet/README.md old mode 100755 new mode 100644 diff --git a/docker/centos/web2py-eventlet/docker-compose.yml b/docker/centos/web2py-eventlet/docker-compose.yml old mode 100755 new mode 100644 diff --git a/docker/centos/web2py-gevent/Dockerfile b/docker/centos/web2py-gevent/Dockerfile old mode 100755 new mode 100644 diff --git a/docker/centos/web2py-gevent/README.md b/docker/centos/web2py-gevent/README.md old mode 100755 new mode 100644 diff --git a/docker/centos/web2py-gevent/docker-compose.yml b/docker/centos/web2py-gevent/docker-compose.yml old mode 100755 new mode 100644 diff --git a/docker/centos/web2py-gunicorn/Dockerfile b/docker/centos/web2py-gunicorn/Dockerfile old mode 100755 new mode 100644 diff --git a/docker/centos/web2py-gunicorn/README.md b/docker/centos/web2py-gunicorn/README.md old mode 100755 new mode 100644 diff --git a/docker/centos/web2py-gunicorn/docker-compose.yml b/docker/centos/web2py-gunicorn/docker-compose.yml old mode 100755 new mode 100644 diff --git a/docker/centos/web2py-paste/Dockerfile b/docker/centos/web2py-paste/Dockerfile old mode 100755 new mode 100644 diff --git a/docker/centos/web2py-paste/README.md b/docker/centos/web2py-paste/README.md old mode 100755 new mode 100644 diff --git a/docker/centos/web2py-paste/docker-compose.yml b/docker/centos/web2py-paste/docker-compose.yml old mode 100755 new mode 100644 diff --git a/docker/centos/web2py-rocket/Dockerfile b/docker/centos/web2py-rocket/Dockerfile old mode 100755 new mode 100644 diff --git a/docker/centos/web2py-rocket/README.md b/docker/centos/web2py-rocket/README.md old mode 100755 new mode 100644 diff --git a/docker/centos/web2py-rocket/docker-compose.yml b/docker/centos/web2py-rocket/docker-compose.yml old mode 100755 new mode 100644 diff --git a/docker/centos/web2py-tornado/Dockerfile b/docker/centos/web2py-tornado/Dockerfile old mode 100755 new mode 100644 diff --git a/docker/centos/web2py-tornado/README.md b/docker/centos/web2py-tornado/README.md old mode 100755 new mode 100644 diff --git a/docker/centos/web2py-tornado/docker-compose.yml b/docker/centos/web2py-tornado/docker-compose.yml old mode 100755 new mode 100644 diff --git a/docker/centos/web2py-twisted/Dockerfile b/docker/centos/web2py-twisted/Dockerfile old mode 100755 new mode 100644 diff --git a/docker/centos/web2py-twisted/README.md b/docker/centos/web2py-twisted/README.md old mode 100755 new mode 100644 diff --git a/docker/centos/web2py-twisted/docker-compose.yml b/docker/centos/web2py-twisted/docker-compose.yml old mode 100755 new mode 100644 diff --git a/docker/centos/web2py-waitress/Dockerfile b/docker/centos/web2py-waitress/Dockerfile old mode 100755 new mode 100644 diff --git a/docker/centos/web2py-waitress/README.md b/docker/centos/web2py-waitress/README.md old mode 100755 new mode 100644 diff --git a/docker/centos/web2py-waitress/docker-compose.yml b/docker/centos/web2py-waitress/docker-compose.yml old mode 100755 new mode 100644 diff --git a/docker/centos/web2py-wsgiref/Dockerfile b/docker/centos/web2py-wsgiref/Dockerfile old mode 100755 new mode 100644 diff --git a/docker/centos/web2py-wsgiref/README.md b/docker/centos/web2py-wsgiref/README.md old mode 100755 new mode 100644 diff --git a/docker/centos/web2py-wsgiref/docker-compose.yml b/docker/centos/web2py-wsgiref/docker-compose.yml old mode 100755 new mode 100644 diff --git a/docker/debian/web2py-diesel/Dockerfile b/docker/debian/web2py-diesel/Dockerfile old mode 100755 new mode 100644 diff --git a/docker/debian/web2py-diesel/README.md b/docker/debian/web2py-diesel/README.md old mode 100755 new mode 100644 diff --git a/docker/debian/web2py-diesel/docker-compose.yml b/docker/debian/web2py-diesel/docker-compose.yml old mode 100755 new mode 100644 diff --git a/docker/debian/web2py-eventlet/Dockerfile b/docker/debian/web2py-eventlet/Dockerfile old mode 100755 new mode 100644 diff --git a/docker/debian/web2py-eventlet/README.md b/docker/debian/web2py-eventlet/README.md old mode 100755 new mode 100644 diff --git a/docker/debian/web2py-eventlet/docker-compose.yml b/docker/debian/web2py-eventlet/docker-compose.yml old mode 100755 new mode 100644 diff --git a/docker/debian/web2py-gevent/Dockerfile b/docker/debian/web2py-gevent/Dockerfile old mode 100755 new mode 100644 diff --git a/docker/debian/web2py-gevent/README.md b/docker/debian/web2py-gevent/README.md old mode 100755 new mode 100644 diff --git a/docker/debian/web2py-gevent/docker-compose.yml b/docker/debian/web2py-gevent/docker-compose.yml old mode 100755 new mode 100644 diff --git a/docker/debian/web2py-gunicorn/Dockerfile b/docker/debian/web2py-gunicorn/Dockerfile old mode 100755 new mode 100644 diff --git a/docker/debian/web2py-gunicorn/README.md b/docker/debian/web2py-gunicorn/README.md old mode 100755 new mode 100644 diff --git a/docker/debian/web2py-gunicorn/docker-compose.yml b/docker/debian/web2py-gunicorn/docker-compose.yml old mode 100755 new mode 100644 diff --git a/docker/debian/web2py-paste/Dockerfile b/docker/debian/web2py-paste/Dockerfile old mode 100755 new mode 100644 diff --git a/docker/debian/web2py-paste/README.md b/docker/debian/web2py-paste/README.md old mode 100755 new mode 100644 diff --git a/docker/debian/web2py-paste/docker-compose.yml b/docker/debian/web2py-paste/docker-compose.yml old mode 100755 new mode 100644 diff --git a/docker/debian/web2py-rocket/Dockerfile b/docker/debian/web2py-rocket/Dockerfile old mode 100755 new mode 100644 diff --git a/docker/debian/web2py-rocket/README.md b/docker/debian/web2py-rocket/README.md old mode 100755 new mode 100644 diff --git a/docker/debian/web2py-rocket/docker-compose.yml b/docker/debian/web2py-rocket/docker-compose.yml old mode 100755 new mode 100644 diff --git a/docker/debian/web2py-tornado/Dockerfile b/docker/debian/web2py-tornado/Dockerfile old mode 100755 new mode 100644 diff --git a/docker/debian/web2py-tornado/README.md b/docker/debian/web2py-tornado/README.md old mode 100755 new mode 100644 diff --git a/docker/debian/web2py-tornado/docker-compose.yml b/docker/debian/web2py-tornado/docker-compose.yml old mode 100755 new mode 100644 diff --git a/docker/debian/web2py-twisted/Dockerfile b/docker/debian/web2py-twisted/Dockerfile old mode 100755 new mode 100644 diff --git a/docker/debian/web2py-twisted/README.md b/docker/debian/web2py-twisted/README.md old mode 100755 new mode 100644 diff --git a/docker/debian/web2py-twisted/docker-compose.yml b/docker/debian/web2py-twisted/docker-compose.yml old mode 100755 new mode 100644 diff --git a/docker/debian/web2py-waitress/Dockerfile b/docker/debian/web2py-waitress/Dockerfile old mode 100755 new mode 100644 diff --git a/docker/debian/web2py-waitress/README.md b/docker/debian/web2py-waitress/README.md old mode 100755 new mode 100644 diff --git a/docker/debian/web2py-waitress/docker-compose.yml b/docker/debian/web2py-waitress/docker-compose.yml old mode 100755 new mode 100644 diff --git a/docker/debian/web2py-wsgiref/Dockerfile b/docker/debian/web2py-wsgiref/Dockerfile old mode 100755 new mode 100644 diff --git a/docker/debian/web2py-wsgiref/README.md b/docker/debian/web2py-wsgiref/README.md old mode 100755 new mode 100644 diff --git a/docker/debian/web2py-wsgiref/docker-compose.yml b/docker/debian/web2py-wsgiref/docker-compose.yml old mode 100755 new mode 100644 diff --git a/docker/fedora/web2py-eventlet/Dockerfile b/docker/fedora/web2py-eventlet/Dockerfile old mode 100755 new mode 100644 diff --git a/docker/fedora/web2py-eventlet/README.md b/docker/fedora/web2py-eventlet/README.md old mode 100755 new mode 100644 diff --git a/docker/fedora/web2py-eventlet/docker-compose.yml b/docker/fedora/web2py-eventlet/docker-compose.yml old mode 100755 new mode 100644 diff --git a/docker/fedora/web2py-gevent/Dockerfile b/docker/fedora/web2py-gevent/Dockerfile old mode 100755 new mode 100644 diff --git a/docker/fedora/web2py-gevent/README.md b/docker/fedora/web2py-gevent/README.md old mode 100755 new mode 100644 diff --git a/docker/fedora/web2py-gevent/docker-compose.yml b/docker/fedora/web2py-gevent/docker-compose.yml old mode 100755 new mode 100644 diff --git a/docker/fedora/web2py-gunicorn/Dockerfile b/docker/fedora/web2py-gunicorn/Dockerfile old mode 100755 new mode 100644 diff --git a/docker/fedora/web2py-gunicorn/README.md b/docker/fedora/web2py-gunicorn/README.md old mode 100755 new mode 100644 diff --git a/docker/fedora/web2py-gunicorn/docker-compose.yml b/docker/fedora/web2py-gunicorn/docker-compose.yml old mode 100755 new mode 100644 diff --git a/docker/fedora/web2py-paste/Dockerfile b/docker/fedora/web2py-paste/Dockerfile old mode 100755 new mode 100644 diff --git a/docker/fedora/web2py-paste/README.md b/docker/fedora/web2py-paste/README.md old mode 100755 new mode 100644 diff --git a/docker/fedora/web2py-paste/docker-compose.yml b/docker/fedora/web2py-paste/docker-compose.yml old mode 100755 new mode 100644 diff --git a/docker/fedora/web2py-rocket/Dockerfile b/docker/fedora/web2py-rocket/Dockerfile old mode 100755 new mode 100644 diff --git a/docker/fedora/web2py-rocket/README.md b/docker/fedora/web2py-rocket/README.md old mode 100755 new mode 100644 diff --git a/docker/fedora/web2py-rocket/docker-compose.yml b/docker/fedora/web2py-rocket/docker-compose.yml old mode 100755 new mode 100644 diff --git a/docker/fedora/web2py-tornado/Dockerfile b/docker/fedora/web2py-tornado/Dockerfile old mode 100755 new mode 100644 diff --git a/docker/fedora/web2py-tornado/README.md b/docker/fedora/web2py-tornado/README.md old mode 100755 new mode 100644 diff --git a/docker/fedora/web2py-tornado/docker-compose.yml b/docker/fedora/web2py-tornado/docker-compose.yml old mode 100755 new mode 100644 diff --git a/docker/fedora/web2py-twisted/Dockerfile b/docker/fedora/web2py-twisted/Dockerfile old mode 100755 new mode 100644 diff --git a/docker/fedora/web2py-twisted/README.md b/docker/fedora/web2py-twisted/README.md old mode 100755 new mode 100644 diff --git a/docker/fedora/web2py-twisted/docker-compose.yml b/docker/fedora/web2py-twisted/docker-compose.yml old mode 100755 new mode 100644 diff --git a/docker/fedora/web2py-waitress/Dockerfile b/docker/fedora/web2py-waitress/Dockerfile old mode 100755 new mode 100644 diff --git a/docker/fedora/web2py-waitress/README.md b/docker/fedora/web2py-waitress/README.md old mode 100755 new mode 100644 diff --git a/docker/fedora/web2py-waitress/docker-compose.yml b/docker/fedora/web2py-waitress/docker-compose.yml old mode 100755 new mode 100644 diff --git a/docker/fedora/web2py-wsgiref/Dockerfile b/docker/fedora/web2py-wsgiref/Dockerfile old mode 100755 new mode 100644 diff --git a/docker/fedora/web2py-wsgiref/README.md b/docker/fedora/web2py-wsgiref/README.md old mode 100755 new mode 100644 diff --git a/docker/fedora/web2py-wsgiref/docker-compose.yml b/docker/fedora/web2py-wsgiref/docker-compose.yml old mode 100755 new mode 100644 diff --git a/docker/opensuse/web2py-eventlet/Dockerfile b/docker/opensuse/web2py-eventlet/Dockerfile old mode 100755 new mode 100644 diff --git a/docker/opensuse/web2py-eventlet/README.md b/docker/opensuse/web2py-eventlet/README.md old mode 100755 new mode 100644 diff --git a/docker/opensuse/web2py-eventlet/docker-compose.yml b/docker/opensuse/web2py-eventlet/docker-compose.yml old mode 100755 new mode 100644 diff --git a/docker/opensuse/web2py-gevent/Dockerfile b/docker/opensuse/web2py-gevent/Dockerfile old mode 100755 new mode 100644 diff --git a/docker/opensuse/web2py-gevent/README.md b/docker/opensuse/web2py-gevent/README.md old mode 100755 new mode 100644 diff --git a/docker/opensuse/web2py-gevent/docker-compose.yml b/docker/opensuse/web2py-gevent/docker-compose.yml old mode 100755 new mode 100644 diff --git a/docker/opensuse/web2py-gunicorn/Dockerfile b/docker/opensuse/web2py-gunicorn/Dockerfile old mode 100755 new mode 100644 diff --git a/docker/opensuse/web2py-gunicorn/README.md b/docker/opensuse/web2py-gunicorn/README.md old mode 100755 new mode 100644 diff --git a/docker/opensuse/web2py-gunicorn/docker-compose.yml b/docker/opensuse/web2py-gunicorn/docker-compose.yml old mode 100755 new mode 100644 diff --git a/docker/opensuse/web2py-paste/Dockerfile b/docker/opensuse/web2py-paste/Dockerfile old mode 100755 new mode 100644 diff --git a/docker/opensuse/web2py-paste/README.md b/docker/opensuse/web2py-paste/README.md old mode 100755 new mode 100644 diff --git a/docker/opensuse/web2py-paste/docker-compose.yml b/docker/opensuse/web2py-paste/docker-compose.yml old mode 100755 new mode 100644 diff --git a/docker/opensuse/web2py-rocket/Dockerfile b/docker/opensuse/web2py-rocket/Dockerfile old mode 100755 new mode 100644 diff --git a/docker/opensuse/web2py-rocket/README.md b/docker/opensuse/web2py-rocket/README.md old mode 100755 new mode 100644 diff --git a/docker/opensuse/web2py-rocket/docker-compose.yml b/docker/opensuse/web2py-rocket/docker-compose.yml old mode 100755 new mode 100644 diff --git a/docker/opensuse/web2py-tornado/Dockerfile b/docker/opensuse/web2py-tornado/Dockerfile old mode 100755 new mode 100644 diff --git a/docker/opensuse/web2py-tornado/README.md b/docker/opensuse/web2py-tornado/README.md old mode 100755 new mode 100644 diff --git a/docker/opensuse/web2py-tornado/docker-compose.yml b/docker/opensuse/web2py-tornado/docker-compose.yml old mode 100755 new mode 100644 diff --git a/docker/opensuse/web2py-twisted/Dockerfile b/docker/opensuse/web2py-twisted/Dockerfile old mode 100755 new mode 100644 diff --git a/docker/opensuse/web2py-twisted/README.md b/docker/opensuse/web2py-twisted/README.md old mode 100755 new mode 100644 diff --git a/docker/opensuse/web2py-twisted/docker-compose.yml b/docker/opensuse/web2py-twisted/docker-compose.yml old mode 100755 new mode 100644 diff --git a/docker/opensuse/web2py-waitress/Dockerfile b/docker/opensuse/web2py-waitress/Dockerfile old mode 100755 new mode 100644 diff --git a/docker/opensuse/web2py-waitress/README.md b/docker/opensuse/web2py-waitress/README.md old mode 100755 new mode 100644 diff --git a/docker/opensuse/web2py-waitress/docker-compose.yml b/docker/opensuse/web2py-waitress/docker-compose.yml old mode 100755 new mode 100644 diff --git a/docker/opensuse/web2py-wsgiref/Dockerfile b/docker/opensuse/web2py-wsgiref/Dockerfile old mode 100755 new mode 100644 diff --git a/docker/opensuse/web2py-wsgiref/README.md b/docker/opensuse/web2py-wsgiref/README.md old mode 100755 new mode 100644 diff --git a/docker/opensuse/web2py-wsgiref/docker-compose.yml b/docker/opensuse/web2py-wsgiref/docker-compose.yml old mode 100755 new mode 100644 diff --git a/docker/python/web2py-diesel/Dockerfile b/docker/python/web2py-diesel/Dockerfile old mode 100755 new mode 100644 diff --git a/docker/python/web2py-diesel/README.md b/docker/python/web2py-diesel/README.md old mode 100755 new mode 100644 diff --git a/docker/python/web2py-diesel/docker-compose.yml b/docker/python/web2py-diesel/docker-compose.yml old mode 100755 new mode 100644 diff --git a/docker/python/web2py-gevent/Dockerfile b/docker/python/web2py-gevent/Dockerfile old mode 100755 new mode 100644 diff --git a/docker/python/web2py-gevent/README.md b/docker/python/web2py-gevent/README.md old mode 100755 new mode 100644 diff --git a/docker/python/web2py-gevent/docker-compose.yml b/docker/python/web2py-gevent/docker-compose.yml old mode 100755 new mode 100644 diff --git a/docker/python/web2py-gunicorn/Dockerfile b/docker/python/web2py-gunicorn/Dockerfile old mode 100755 new mode 100644 diff --git a/docker/python/web2py-gunicorn/README.md b/docker/python/web2py-gunicorn/README.md old mode 100755 new mode 100644 diff --git a/docker/python/web2py-gunicorn/docker-compose.yml b/docker/python/web2py-gunicorn/docker-compose.yml old mode 100755 new mode 100644 diff --git a/docker/python/web2py-paste/Dockerfile b/docker/python/web2py-paste/Dockerfile old mode 100755 new mode 100644 diff --git a/docker/python/web2py-paste/README.md b/docker/python/web2py-paste/README.md old mode 100755 new mode 100644 diff --git a/docker/python/web2py-paste/docker-compose.yml b/docker/python/web2py-paste/docker-compose.yml old mode 100755 new mode 100644 diff --git a/docker/python/web2py-rocket-ssl/Dockerfile b/docker/python/web2py-rocket-ssl/Dockerfile old mode 100755 new mode 100644 diff --git a/docker/python/web2py-rocket-ssl/README.md b/docker/python/web2py-rocket-ssl/README.md old mode 100755 new mode 100644 diff --git a/docker/python/web2py-rocket-ssl/docker-compose.yml b/docker/python/web2py-rocket-ssl/docker-compose.yml old mode 100755 new mode 100644 diff --git a/docker/python/web2py-rocket/Dockerfile b/docker/python/web2py-rocket/Dockerfile old mode 100755 new mode 100644 diff --git a/docker/python/web2py-rocket/README.md b/docker/python/web2py-rocket/README.md old mode 100755 new mode 100644 diff --git a/docker/python/web2py-rocket/docker-compose.yml b/docker/python/web2py-rocket/docker-compose.yml old mode 100755 new mode 100644 diff --git a/docker/python/web2py-tornado/Dockerfile b/docker/python/web2py-tornado/Dockerfile old mode 100755 new mode 100644 diff --git a/docker/python/web2py-tornado/README.md b/docker/python/web2py-tornado/README.md old mode 100755 new mode 100644 diff --git a/docker/python/web2py-tornado/docker-compose.yml b/docker/python/web2py-tornado/docker-compose.yml old mode 100755 new mode 100644 diff --git a/docker/python/web2py-twisted/Dockerfile b/docker/python/web2py-twisted/Dockerfile old mode 100755 new mode 100644 diff --git a/docker/python/web2py-twisted/README.md b/docker/python/web2py-twisted/README.md old mode 100755 new mode 100644 diff --git a/docker/python/web2py-twisted/docker-compose.yml b/docker/python/web2py-twisted/docker-compose.yml old mode 100755 new mode 100644 diff --git a/docker/python/web2py-waitress/Dockerfile b/docker/python/web2py-waitress/Dockerfile old mode 100755 new mode 100644 diff --git a/docker/python/web2py-waitress/README.md b/docker/python/web2py-waitress/README.md old mode 100755 new mode 100644 diff --git a/docker/python/web2py-waitress/docker-compose.yml b/docker/python/web2py-waitress/docker-compose.yml old mode 100755 new mode 100644 diff --git a/docker/python/web2py-wsgiref/Dockerfile b/docker/python/web2py-wsgiref/Dockerfile old mode 100755 new mode 100644 diff --git a/docker/python/web2py-wsgiref/README.md b/docker/python/web2py-wsgiref/README.md old mode 100755 new mode 100644 diff --git a/docker/python/web2py-wsgiref/docker-compose.yml b/docker/python/web2py-wsgiref/docker-compose.yml old mode 100755 new mode 100644 diff --git a/docker/stack/web2py-gunicorn-nginx/README.md b/docker/stack/web2py-gunicorn-nginx/README.md old mode 100755 new mode 100644 diff --git a/docker/stack/web2py-gunicorn-nginx/docker-compose.yml b/docker/stack/web2py-gunicorn-nginx/docker-compose.yml old mode 100755 new mode 100644 diff --git a/docker/stack/web2py-rocket-nginx/README.md b/docker/stack/web2py-rocket-nginx/README.md old mode 100755 new mode 100644 diff --git a/docker/stack/web2py-rocket-nginx/docker-compose.yml b/docker/stack/web2py-rocket-nginx/docker-compose.yml old mode 100755 new mode 100644 diff --git a/docker/stack/web2py-rocket-ssl-nginx-db-adminer/README.md b/docker/stack/web2py-rocket-ssl-nginx-db-adminer/README.md old mode 100755 new mode 100644 diff --git a/docker/stack/web2py-rocket-ssl-nginx-db-adminer/docker-compose.yml b/docker/stack/web2py-rocket-ssl-nginx-db-adminer/docker-compose.yml old mode 100755 new mode 100644 diff --git a/docker/stack/web2py-rocket-ssl-nginx-memcached/README.md b/docker/stack/web2py-rocket-ssl-nginx-memcached/README.md old mode 100755 new mode 100644 diff --git a/docker/stack/web2py-rocket-ssl-nginx-memcached/docker-compose.yml b/docker/stack/web2py-rocket-ssl-nginx-memcached/docker-compose.yml old mode 100755 new mode 100644 diff --git a/docker/stack/web2py-rocket-ssl-nginx-redis/README.md b/docker/stack/web2py-rocket-ssl-nginx-redis/README.md old mode 100755 new mode 100644 diff --git a/docker/stack/web2py-rocket-ssl-nginx-redis/docker-compose.yml b/docker/stack/web2py-rocket-ssl-nginx-redis/docker-compose.yml old mode 100755 new mode 100644 diff --git a/docker/stack/web2py-rocket-ssl-nginx/README.md b/docker/stack/web2py-rocket-ssl-nginx/README.md old mode 100755 new mode 100644 diff --git a/docker/stack/web2py-rocket-ssl-nginx/docker-compose.yml b/docker/stack/web2py-rocket-ssl-nginx/docker-compose.yml old mode 100755 new mode 100644 diff --git a/docker/stack/web2py-tornado-nginx/README.md b/docker/stack/web2py-tornado-nginx/README.md old mode 100755 new mode 100644 diff --git a/docker/stack/web2py-tornado-nginx/docker-compose.yml b/docker/stack/web2py-tornado-nginx/docker-compose.yml old mode 100755 new mode 100644 diff --git a/docker/ubuntu/web2py-diesel/Dockerfile b/docker/ubuntu/web2py-diesel/Dockerfile old mode 100755 new mode 100644 diff --git a/docker/ubuntu/web2py-diesel/README.md b/docker/ubuntu/web2py-diesel/README.md old mode 100755 new mode 100644 diff --git a/docker/ubuntu/web2py-diesel/docker-compose.yml b/docker/ubuntu/web2py-diesel/docker-compose.yml old mode 100755 new mode 100644 diff --git a/docker/ubuntu/web2py-eventlet/Dockerfile b/docker/ubuntu/web2py-eventlet/Dockerfile old mode 100755 new mode 100644 diff --git a/docker/ubuntu/web2py-eventlet/README.md b/docker/ubuntu/web2py-eventlet/README.md old mode 100755 new mode 100644 diff --git a/docker/ubuntu/web2py-eventlet/docker-compose.yml b/docker/ubuntu/web2py-eventlet/docker-compose.yml old mode 100755 new mode 100644 diff --git a/docker/ubuntu/web2py-gevent/Dockerfile b/docker/ubuntu/web2py-gevent/Dockerfile old mode 100755 new mode 100644 diff --git a/docker/ubuntu/web2py-gevent/README.md b/docker/ubuntu/web2py-gevent/README.md old mode 100755 new mode 100644 diff --git a/docker/ubuntu/web2py-gevent/docker-compose.yml b/docker/ubuntu/web2py-gevent/docker-compose.yml old mode 100755 new mode 100644 diff --git a/docker/ubuntu/web2py-gunicorn/Dockerfile b/docker/ubuntu/web2py-gunicorn/Dockerfile old mode 100755 new mode 100644 diff --git a/docker/ubuntu/web2py-gunicorn/README.md b/docker/ubuntu/web2py-gunicorn/README.md old mode 100755 new mode 100644 diff --git a/docker/ubuntu/web2py-gunicorn/docker-compose.yml b/docker/ubuntu/web2py-gunicorn/docker-compose.yml old mode 100755 new mode 100644 diff --git a/docker/ubuntu/web2py-paste/Dockerfile b/docker/ubuntu/web2py-paste/Dockerfile old mode 100755 new mode 100644 diff --git a/docker/ubuntu/web2py-paste/README.md b/docker/ubuntu/web2py-paste/README.md old mode 100755 new mode 100644 diff --git a/docker/ubuntu/web2py-paste/docker-compose.yml b/docker/ubuntu/web2py-paste/docker-compose.yml old mode 100755 new mode 100644 diff --git a/docker/ubuntu/web2py-rocket/Dockerfile b/docker/ubuntu/web2py-rocket/Dockerfile old mode 100755 new mode 100644 diff --git a/docker/ubuntu/web2py-rocket/README.md b/docker/ubuntu/web2py-rocket/README.md old mode 100755 new mode 100644 diff --git a/docker/ubuntu/web2py-rocket/docker-compose.yml b/docker/ubuntu/web2py-rocket/docker-compose.yml old mode 100755 new mode 100644 diff --git a/docker/ubuntu/web2py-tornado/Dockerfile b/docker/ubuntu/web2py-tornado/Dockerfile old mode 100755 new mode 100644 diff --git a/docker/ubuntu/web2py-tornado/README.md b/docker/ubuntu/web2py-tornado/README.md old mode 100755 new mode 100644 diff --git a/docker/ubuntu/web2py-tornado/docker-compose.yml b/docker/ubuntu/web2py-tornado/docker-compose.yml old mode 100755 new mode 100644 diff --git a/docker/ubuntu/web2py-twisted/Dockerfile b/docker/ubuntu/web2py-twisted/Dockerfile old mode 100755 new mode 100644 diff --git a/docker/ubuntu/web2py-twisted/README.md b/docker/ubuntu/web2py-twisted/README.md old mode 100755 new mode 100644 diff --git a/docker/ubuntu/web2py-twisted/docker-compose.yml b/docker/ubuntu/web2py-twisted/docker-compose.yml old mode 100755 new mode 100644 diff --git a/docker/ubuntu/web2py-waitress/Dockerfile b/docker/ubuntu/web2py-waitress/Dockerfile old mode 100755 new mode 100644 diff --git a/docker/ubuntu/web2py-waitress/README.md b/docker/ubuntu/web2py-waitress/README.md old mode 100755 new mode 100644 diff --git a/docker/ubuntu/web2py-waitress/docker-compose.yml b/docker/ubuntu/web2py-waitress/docker-compose.yml old mode 100755 new mode 100644 diff --git a/docker/ubuntu/web2py-wsgiref/Dockerfile b/docker/ubuntu/web2py-wsgiref/Dockerfile old mode 100755 new mode 100644 diff --git a/docker/ubuntu/web2py-wsgiref/README.md b/docker/ubuntu/web2py-wsgiref/README.md old mode 100755 new mode 100644 diff --git a/docker/ubuntu/web2py-wsgiref/docker-compose.yml b/docker/ubuntu/web2py-wsgiref/docker-compose.yml old mode 100755 new mode 100644 diff --git a/gluon/main.py b/gluon/main.py index 131d5ec5..8f5fac1b 100644 --- a/gluon/main.py +++ b/gluon/main.py @@ -554,10 +554,12 @@ def wsgibase(environ, responder): http_response, request, environ, ticket) if not http_response: return wsgibase(new_environ, responder) + if global_settings.web2py_crontype == 'soft': cmd_opts = global_settings.cmd_options newcron.softcron(global_settings.applications_parent, - apps=cmd_opts and cmd_opts.crontabs).start() + apps=cmd_opts and cmd_opts.crontabs) + return http_response.to(responder, env=env) @@ -783,7 +785,11 @@ class HttpServer(object): """ stop cron and the web server """ - newcron.stopcron() + if global_settings.web2py_crontype == 'soft': + try: + newcron.stopcron() + except: + pass self.server.stop(stoplogging) try: os.unlink(self.pid_filename) diff --git a/gluon/newcron.py b/gluon/newcron.py index 4d208265..a5888ad5 100644 --- a/gluon/newcron.py +++ b/gluon/newcron.py @@ -5,6 +5,7 @@ | This file is part of the web2py Web Framework | Created by Attila Csipa | Modified by Massimo Di Pierro +| Worker, SoftWorker and SimplePool added by Paolo Pastori | License: LGPLv3 (http://www.gnu.org/licenses/lgpl.html) Cron-style interface @@ -12,16 +13,15 @@ Cron-style interface import threading import os -from logging import getLogger, DEBUG +from logging import getLogger import time import sched import sys import re +from functools import reduce +import datetime import shlex -import datetime -from functools import reduce -from gluon.settings import global_settings from gluon import fileutils from gluon._compat import to_bytes, pickle from pydal.contrib import portalocker @@ -29,14 +29,19 @@ from pydal.contrib import portalocker logger_name = 'web2py.cron' -_cron_stopping = False +_stopping = False -_cron_subprocs_lock = threading.RLock() -_cron_subprocs = [] +def reset(): + global _stopping + _stopping = False + + +_subprocs_lock = threading.RLock() +_subprocs = [] def subprocess_count(): - with _cron_subprocs_lock: - return len(_cron_subprocs) + with _subprocs_lock: + return len(_subprocs) def absolute_path_link(path): @@ -54,11 +59,11 @@ def absolute_path_link(path): def stopcron(): """Graceful shutdown of cron""" - global _cron_stopping - _cron_stopping = True + global _stopping + _stopping = True while subprocess_count(): - with _cron_subprocs_lock: - proc = _cron_subprocs.pop() + with _subprocs_lock: + proc = _subprocs.pop() if proc.poll() is None: try: proc.terminate() @@ -66,18 +71,9 @@ def stopcron(): getLogger(logger_name).exception('error in stopcron') -class extcron(threading.Thread): - - def __init__(self, applications_parent, apps=None): - threading.Thread.__init__(self) - self.setDaemon(False) - self.path = applications_parent - self.apps = apps - - def run(self): - if not _cron_stopping: - getLogger(logger_name).debug('external cron invocation') - crondance(self.path, 'external', startup=False, apps=self.apps) +def extcron(applications_parent, apps=None): + getLogger(logger_name).debug('external cron invocation') + crondance(applications_parent, 'external', startup=False, apps=apps) class hardcron(threading.Thread): @@ -92,7 +88,7 @@ class hardcron(threading.Thread): crondance(self.path, 'hard', startup=True, apps=self.apps) def launch(self): - if not _cron_stopping: + if not _stopping: self.logger.debug('hard cron invocation') crondance(self.path, 'hard', startup=False, apps=self.apps) @@ -100,26 +96,21 @@ class hardcron(threading.Thread): self.logger = getLogger(logger_name) self.logger.info('hard cron daemon started') s = sched.scheduler(time.time, time.sleep) - while not _cron_stopping: + while not _stopping: now = time.time() s.enter(60 - now % 60, 1, self.launch, ()) s.run() -class softcron(threading.Thread): - - def __init__(self, applications_parent, apps=None): - threading.Thread.__init__(self) - self.path = applications_parent - self.apps = apps - - def run(self): - if not _cron_stopping: - getLogger(logger_name).debug('soft cron invocation') - crondance(self.path, 'soft', startup=False, apps=self.apps) +def softcron(applications_parent, apps=None): + logger = getLogger(logger_name) + try: + if not _dancer((applications_parent, apps)): + logger.warning('no thread available for soft crondance') + except Exception: + logger.exception('error executing soft crondance') -# TODO: context manager class Token(object): def __init__(self, path): @@ -140,7 +131,7 @@ class Token(object): stop == 0 if job started but did not yet complete if a cron job started within less than 60 seconds, acquire returns None if a cron job started before 60 seconds and did not stop, - a warning is issue "Stale cron.master detected" + a warning is issued ("Stale cron.master detected") """ if sys.platform == 'win32': locktime = 59.5 @@ -150,8 +141,8 @@ class Token(object): self.logger.warning('cron disabled because no file locking') return None self.master = fileutils.open_file(self.path, 'rb+') + ret = None try: - ret = None portalocker.lock(self.master, portalocker.LOCK_EX) try: (start, stop) = pickle.load(self.master) @@ -254,36 +245,144 @@ def parsecronline(line): return task -class cronlauncher(threading.Thread): +class Worker(threading.Thread): - def __init__(self, cmd): + def __init__(self, pool): threading.Thread.__init__(self) - self.cmd = cmd + self.setDaemon(True) + self.pool = pool + self.run_lock = threading.Lock() + self.run_lock.acquire() + self.payload = None def run(self): - import subprocess logger = getLogger(logger_name) - proc = subprocess.Popen(self.cmd, - stdin=subprocess.PIPE, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE) - with _cron_subprocs_lock: - _cron_subprocs.append(proc) - (stdoutdata, stderrdata) = proc.communicate() - try: - with _cron_subprocs_lock: - _cron_subprocs.remove(proc) - except ValueError: - pass - if proc.returncode != 0: - logger.warning('%r call returned code %s:\n%s\n%s', - ' '.join(self.cmd), proc.returncode, stdoutdata, stderrdata) - elif logger.isEnabledFor(DEBUG): - logger.debug('%r call returned success:\n%s', - ' '.join(self.cmd), stdoutdata) + logger.info('Worker %s: started', self.name) + while True: + try: + with self.run_lock: # waiting for run_lock.release() + cmd = ' '.join(self.payload) + logger.debug('Worker %s: now calling %r', self.name, cmd) + import subprocess + proc = subprocess.Popen(self.payload, + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE) + with _subprocs_lock: + _subprocs.append(proc) + stdoutdata, stderrdata = proc.communicate() + try: + with _subprocs_lock: + _subprocs.remove(proc) + except ValueError: + pass + if proc.returncode != 0: + logger.warning('Worker %s: %r call returned code %s:\n%s\n%s', + self.name, cmd, proc.returncode, stdoutdata, stderrdata) + else: + logger.debug('Worker %s: %r call returned success:\n%s', + self.name, cmd, stdoutdata) + finally: + self.run_lock.acquire() + self.pool.stop(self) -def crondance(applications_parent, ctype='soft', startup=False, apps=None): +class SoftWorker(threading.Thread): + + def __init__(self, pool): + threading.Thread.__init__(self) + self.setDaemon(True) + self.pool = pool + self.run_lock = threading.Lock() + self.run_lock.acquire() + self.payload = None + + def run(self): + logger = getLogger(logger_name) + logger.info('SoftWorker %s: started', self.name) + while True: + try: + with self.run_lock: # waiting for run_lock.release() + getLogger(logger_name).debug('soft cron invocation') + applications_parent, apps = self.payload + crondance(applications_parent, 'soft', startup=False, apps=apps) + finally: + self.run_lock.acquire() + self.pool.stop(self) + + +class SimplePool(object): + """ + Very simple thread pool, + (re)uses a maximum number of threads to launch cron tasks. + + Pool size can be incremented after initialization, + this allows delayed configuration of a global instance + for the case you do not want to use lazy initialization. + """ + + def __init__(self, size, worker_cls=Worker): + """ + Create the pool setting initial size. + + Notice that no thread is created until the instance is called. + """ + self.size = size + self.worker_cls = worker_cls + self.lock = threading.RLock() + self.idle = list() + self.running = set() + + def grow(self, size): + if size > self.size: + self.size = size + + def start(self, t): + with self.lock: + try: + self.idle.remove(t) + except ValueError: + pass + self.running.add(t) + + def stop(self, t): + with self.lock: + self.idle.append(t) + try: + self.running.remove(t) + except KeyError: + pass + + def __call__(self, payload): + """ + Pass payload to a thread for immediate execution. + + Returns a boolean indicating if a thread is available. + """ + with self.lock: + if len(self.running) == self.size: + # no worker available + return False + idle_num = len(self.idle) + if idle_num: + # use an existing (idle) thread + t = self.idle.pop(0) + else: + # create a new thread + t = self.worker_cls(self) + self.start(t) + t.payload = payload + t.run_lock.release() + if not idle_num: + t.start() + return True + + +_dancer = SimplePool(5, worker_cls=SoftWorker) + +_launcher = SimplePool(5) + +def crondance(applications_parent, ctype='hard', startup=False, apps=None): """ Does the periodic job of cron service: read the crontab(s) and launch the various commands. @@ -293,96 +392,94 @@ def crondance(applications_parent, ctype='soft', startup=False, apps=None): cronmaster = token.acquire(startup=startup) if not cronmaster: return - now_s = time.localtime() - checks = (('min', now_s.tm_min), - ('hr', now_s.tm_hour), - ('mon', now_s.tm_mon), - ('dom', now_s.tm_mday), - ('dow', (now_s.tm_wday + 1) % 7)) + try: + now_s = time.localtime() + checks = (('min', now_s.tm_min), + ('hr', now_s.tm_hour), + ('mon', now_s.tm_mon), + ('dom', now_s.tm_mday), + ('dow', (now_s.tm_wday + 1) % 7)) + logger = getLogger(logger_name) - logger = getLogger(logger_name) + if not apps: + apps = [x for x in os.listdir(apppath) + if os.path.isdir(os.path.join(apppath, x))] - if not apps: - apps = [x for x in os.listdir(apppath) - if os.path.isdir(os.path.join(apppath, x))] + full_apath_links = set() - full_apath_links = set() - - if sys.executable.lower().endswith('pythonservice.exe'): - _python_exe = os.path.join(sys.exec_prefix, 'python.exe') - else: - _python_exe = sys.executable - base_commands = [_python_exe] - w2p_path = fileutils.abspath('web2py.py', gluon=True) - if os.path.exists(w2p_path): - base_commands.append(w2p_path) - if applications_parent != global_settings.gluon_parent: - base_commands.extend(('-f', applications_parent)) - base_commands.extend(('--cron_job', '--no_banner', '--no_gui', '--plain')) - - for app in apps: - if _cron_stopping: - break - apath = os.path.join(apppath, app) - - # if app is a symbolic link to other app, skip it - full_apath_link = absolute_path_link(apath) - if full_apath_link in full_apath_links: - continue + if sys.executable.lower().endswith('pythonservice.exe'): + _python_exe = os.path.join(sys.exec_prefix, 'python.exe') else: - full_apath_links.add(full_apath_link) + _python_exe = sys.executable + base_commands = [_python_exe] + w2p_path = fileutils.abspath('web2py.py', gluon=True) + if os.path.exists(w2p_path): + base_commands.append(w2p_path) + base_commands.extend(('--cron_job', '--no_banner', '--no_gui', '--plain')) - cronpath = os.path.join(apath, 'cron') - crontab = os.path.join(cronpath, 'crontab') - if not os.path.exists(crontab): - continue - try: - cronlines = [line.strip() for line in fileutils.readlines_file(crontab, 'rt')] - lines = [line for line in cronlines if line and not line.startswith('#')] - tasks = [parsecronline(cline) for cline in lines] - except Exception as e: - logger.error('crontab read error %s', e) - continue - - for task in tasks: - if _cron_stopping: + for app in apps: + if _stopping: break - if not task: - continue - task_min = task.get('min', []) - if not startup and task_min == [-1]: - continue - citems = [(k in task and not v in task[k]) for k, v in checks] - if task_min != [-1] and reduce(lambda a, b: a or b, citems): - continue + apath = os.path.join(apppath, app) - logger.info('%s cron: %s executing %r in %s at %s', - ctype, app, task.get('cmd'), - os.getcwd(), datetime.datetime.now()) - action = models = False - command = task['cmd'] - if command.startswith('**'): - action = True - command = command[2:] - elif command.startswith('*'): - action = models = True - command = command[1:] - - if action: - commands = base_commands[:] - if command.endswith('.py'): - commands.extend(('-S', app, '-R', command)) - else: - commands.extend(('-S', app + '/' + command)) - if models: - commands.append('-M') + # if app is a symbolic link to other app, skip it + full_apath_link = absolute_path_link(apath) + if full_apath_link in full_apath_links: + continue else: - commands = shlex.split(command) + full_apath_links.add(full_apath_link) + cronpath = os.path.join(apath, 'cron') + crontab = os.path.join(cronpath, 'crontab') + if not os.path.exists(crontab): + continue try: - # FIXME: using a new thread every time there is a task to - # launch is not a good idea in a long running process - cronlauncher(commands).start() - except Exception: - logger.exception('error starting %r', task['cmd']) - token.release() + cronlines = [line.strip() for line in fileutils.readlines_file(crontab, 'rt')] + lines = [line for line in cronlines if line and not line.startswith('#')] + tasks = [parsecronline(cline) for cline in lines] + except Exception as e: + logger.error('crontab read error %s', e) + continue + + for task in tasks: + if _stopping: + break + if not task: + continue + task_min = task.get('min', []) + if not startup and task_min == [-1]: + continue + citems = [(k in task and not v in task[k]) for k, v in checks] + if task_min != [-1] and reduce(lambda a, b: a or b, citems): + continue + + logger.info('%s cron: %s executing %r in %s at %s', + ctype, app, task.get('cmd'), + os.getcwd(), datetime.datetime.now()) + action = models = False + command = task['cmd'] + if command.startswith('**'): + action = True + command = command[2:] + elif command.startswith('*'): + action = models = True + command = command[1:] + + if action: + commands = base_commands[:] + if command.endswith('.py'): + commands.extend(('-S', app, '-R', command)) + else: + commands.extend(('-S', app + '/' + command)) + if models: + commands.append('-M') + else: + commands = shlex.split(command) + + try: + if not _launcher(commands): + logger.warning('no thread available, cannot execute %r', task['cmd']) + except Exception: + logger.exception('error executing %r', task['cmd']) + finally: + token.release() diff --git a/gluon/packages/dal b/gluon/packages/dal index 3815fbe9..1fd32c51 160000 --- a/gluon/packages/dal +++ b/gluon/packages/dal @@ -1 +1 @@ -Subproject commit 3815fbe9b6f7af4e2fa28c509010fea10d072b02 +Subproject commit 1fd32c51338abce26b3c328887cfb513c9a2ce6f diff --git a/gluon/recfile.py b/gluon/recfile.py old mode 100755 new mode 100644 diff --git a/gluon/tests/test_cron.py b/gluon/tests/test_cron.py index c25f3ce5..b31b7f78 100644 --- a/gluon/tests/test_cron.py +++ b/gluon/tests/test_cron.py @@ -8,8 +8,10 @@ import unittest import os import shutil import time +import sys -from gluon.newcron import Token, crondance, subprocess_count +from gluon.newcron import (Token, crondance, subprocess_count, + SimplePool, stopcron, reset) from gluon.fileutils import create_app, write_file @@ -28,13 +30,18 @@ def tearDownModule(): TEST_CRONTAB = """@reboot peppe **applications/%s/cron/test.py """ % test_app_name -TEST_SCRIPT = """ +TEST_SCRIPT1 = """ from os.path import join as pjoin with open(pjoin(request.folder, 'private', 'cron_req'), 'w') as f: f.write(str(request)) """ TARGET = os.path.join(appdir, 'private', 'cron_req') +TEST_SCRIPT2 = """ +import time + +time.sleep(13) +""" class TestCron(unittest.TestCase): @@ -44,7 +51,7 @@ class TestCron(unittest.TestCase): if os.path.exists(master): os.unlink(master) - def test_Token(self): + def test_1_Token(self): app_path = os.path.join(os.getcwd(), 'applications', test_app_name) t = Token(path=app_path) self.assertEqual(t.acquire(), t.now) @@ -52,10 +59,10 @@ class TestCron(unittest.TestCase): self.assertIsNone(t.acquire()) self.assertTrue(t.release()) - def test_crondance(self): + def test_2_crondance(self): base = os.path.join(appdir, 'cron') write_file(os.path.join(base, 'crontab'), TEST_CRONTAB) - write_file(os.path.join(base, 'test.py'), TEST_SCRIPT) + write_file(os.path.join(base, 'test.py'), TEST_SCRIPT1) if os.path.exists(TARGET): os.unlink(TARGET) crondance(os.getcwd(), 'hard', startup=True, apps=[test_app_name]) @@ -65,3 +72,19 @@ class TestCron(unittest.TestCase): time.sleep(1) time.sleep(1) self.assertTrue(os.path.exists(TARGET)) + + def test_3_SimplePool(self): + base = os.path.join(appdir, 'cron') + write_file(os.path.join(base, 'test.py'), TEST_SCRIPT2) + w2p_path = os.path.join(os.getcwd(), 'web2py.py') + self.assertTrue(os.path.exists(w2p_path)) + launcher = SimplePool(1) + cmd1 = [sys.executable, w2p_path, + '--cron_job', '--no_banner', '--no_gui', '--plain', + '-S', test_app_name, '-R', "applications/%s/cron/test.py" % test_app_name] + self.assertTrue(launcher(cmd1)) + self.assertFalse(launcher(None)) + time.sleep(1) + stopcron() + time.sleep(1) + reset() diff --git a/gluon/widget.py b/gluon/widget.py index 4fd0869f..a3922b14 100644 --- a/gluon/widget.py +++ b/gluon/widget.py @@ -1,4 +1,5 @@ # -*- coding: utf-8 -*- +# vim: set ts=4 sw=4 et ai: """ | This file is part of the web2py Web Framework @@ -11,23 +12,26 @@ GUI widget and services start function from __future__ import print_function -import sys -from gluon._compat import thread, xrange, PY2 import time -import threading +import sys import os +from collections import OrderedDict import socket -import signal +import threading import math import logging +import signal import getpass -from gluon import main, newcron from gluon.fileutils import read_file, create_welcome_w2p -from gluon.console import console -from gluon.settings import global_settings from gluon.shell import die, run, test -from gluon.utils import is_valid_ip_address, is_loopback_ip_address, getipaddrinfo +from gluon._compat import PY2, xrange +from gluon.utils import (getipaddrinfo, is_loopback_ip_address, + is_valid_ip_address) +from gluon.console import is_appdir, console +from gluon import newcron +from gluon import main +from gluon.settings import global_settings ProgramName = 'web2py Web Framework' @@ -119,24 +123,27 @@ class web2pyDialog(object): import tkinter from tkinter import messagebox - - bg_color = 'white' root.withdraw() + bg_color = 'white' self.root = tkinter.Toplevel(root, bg=bg_color) self.root.resizable(0, 0) self.root.title(ProgramName) self.options = options - self.scheduler_processes = {} - self.menu = tkinter.Menu(self.root) - servermenu = tkinter.Menu(self.menu, tearoff=0) - httplog = os.path.join(self.options.folder, self.options.log_filename) + self.scheduler_processes_lock = threading.RLock() + self.scheduler_processes = OrderedDict() + iconphoto = os.path.join('extras', 'icons', 'web2py.gif') if os.path.exists(iconphoto): img = tkinter.PhotoImage(file=iconphoto) self.root.tk.call('wm', 'iconphoto', self.root._w, img) + # Building the Menu + self.menu = tkinter.Menu(self.root) + servermenu = tkinter.Menu(self.menu, tearoff=0) + + httplog = os.path.join(options.folder, options.log_filename) item = lambda: start_browser(httplog) servermenu.add_command(label='View httpserver.log', command=item) @@ -149,10 +156,9 @@ class web2pyDialog(object): self.pagesmenu = tkinter.Menu(self.menu, tearoff=0) self.menu.add_cascade(label='Pages', menu=self.pagesmenu) - #scheduler menu self.schedmenu = tkinter.Menu(self.menu, tearoff=0) self.menu.add_cascade(label='Scheduler', menu=self.schedmenu) - #start and register schedulers from options + # register and start schedulers self.update_schedulers(start=True) helpmenu = tkinter.Menu(self.menu, tearoff=0) @@ -257,7 +263,7 @@ class web2pyDialog(object): sticky=sticky) self.port_number = tkinter.Entry(self.root) - self.port_number.insert(tkinter.END, self.options.port) + self.port_number.insert(tkinter.END, options.port) self.port_number.grid(row=shift, column=2, sticky=sticky, pady=10) # Password @@ -314,6 +320,26 @@ class web2pyDialog(object): self.tb = None def update_schedulers(self, start=False): + applications_folder = os.path.join(self.options.folder, 'applications') + available_apps = [ + arq for arq in os.listdir(applications_folder) + if os.path.isdir(os.path.join(applications_folder, arq)) + ] + with self.scheduler_processes_lock: + # reset the menu + # since applications can disappear (be disinstalled) must + # clear the menu (should use tkinter.END or tkinter.LAST) + self.schedmenu.delete(0, 'end') + for arq in available_apps: + if arq not in self.scheduler_processes: + item = lambda a = arq: self.try_start_scheduler(a) + self.schedmenu.add_command(label="start %s" % arq, + command=item) + if arq in self.scheduler_processes: + item = lambda a = arq: self.try_stop_scheduler(a) + self.schedmenu.add_command(label="stop %s" % arq, + command=item) + if start and self.options.with_scheduler and self.options.schedulers: # the widget takes care of starting the schedulers apps = [ag.split(':', 1)[0] for ag in self.options.schedulers] @@ -322,100 +348,90 @@ class web2pyDialog(object): for app in apps: self.try_start_scheduler(app) - # reset the menu - applications_folder = os.path.join(self.options.folder, 'applications') - available_apps = [ - arq for arq in os.listdir(applications_folder) - if os.path.isdir(os.path.join(applications_folder, arq)) - ] - self.schedmenu.delete(0, len(available_apps)) - - for arq in available_apps: - if arq not in self.scheduler_processes: - item = lambda u = arq: self.try_start_scheduler(u) - self.schedmenu.add_command(label="start %s" % arq, - command=item) - if arq in self.scheduler_processes: - item = lambda u = arq: self.try_stop_scheduler(u) - self.schedmenu.add_command(label="stop %s" % arq, - command=item) - def start_schedulers(self, app): - try: - from multiprocessing import Process - except: - sys.stderr.write('Sorry, -K only supported for Python 2.6+\n') - return + from multiprocessing import Process code = "from gluon.globals import current;current._scheduler.loop()" print('starting scheduler from widget for "%s"...' % app) args = (app, True, True, None, False, code, False, True) - logging.getLogger().setLevel(self.options.log_level) p = Process(target=run, args=args) - self.scheduler_processes[app] = p - self.update_schedulers() - print("Currently running %s scheduler processes" % ( - len(self.scheduler_processes))) + with self.scheduler_processes_lock: + self.scheduler_processes[app] = p + self.update_schedulers() + print("Currently running %s scheduler processes" % ( + len(self.scheduler_processes))) p.start() print("Processes started") - def try_stop_scheduler(self, app): - if app in self.scheduler_processes: - p = self.scheduler_processes[app] - del self.scheduler_processes[app] + def try_stop_scheduler(self, app, skip_update=False): + p = None + with self.scheduler_processes_lock: + if app in self.scheduler_processes: + p = self.scheduler_processes[app] + del self.scheduler_processes[app] + if p is not None: p.terminate() p.join() - self.update_schedulers() + if not skip_update: + self.update_schedulers() def try_start_scheduler(self, app): - if app not in self.scheduler_processes: - t = threading.Thread(target=self.start_schedulers, args=(app,)) + t = None + with self.scheduler_processes_lock: + if not is_appdir(self.options.folder, app): + self.schedmenu.delete("start %s" % app) + return + if app not in self.scheduler_processes: + t = threading.Thread(target=self.start_schedulers, args=(app,)) + if t is not None: t.start() def checkTaskBar(self): """ Checks taskbar status """ - - if self.tb.status: - if self.tb.status[0] == self.tb.EnumStatus.QUIT: + tb = self.tb + if tb.status: + st0 = tb.status[0] + EnumStatus = tb.EnumStatus + if st0 == EnumStatus.QUIT: self.quit() - elif self.tb.status[0] == self.tb.EnumStatus.TOGGLE: + elif st0 == EnumStatus.TOGGLE: if self.root.state() == 'withdrawn': self.root.deiconify() else: self.root.withdraw() - elif self.tb.status[0] == self.tb.EnumStatus.STOP: + elif st0 == EnumStatus.STOP: self.stop() - elif self.tb.status[0] == self.tb.EnumStatus.START: + elif st0 == EnumStatus.START: self.start() - elif self.tb.status[0] == self.tb.EnumStatus.RESTART: + elif st0 == EnumStatus.RESTART: self.stop() self.start() - del self.tb.status[0] + del tb.status[0] self.root.after(1000, self.checkTaskBar) - def update(self, text): - """ Updates app text """ - - try: - self.text.configure(state='normal') - self.text.insert('end', text) - self.text.configure(state='disabled') - except: - pass # ## this should only happen in case app is destroyed - def connect_pages(self): """ Connects pages """ - # reset the menu + # reset the menu, + # since applications can disappear (be disinstalled) must + # clear the menu (should use tkinter.END or tkinter.LAST) + self.pagesmenu.delete(0, 'end') applications_folder = os.path.join(self.options.folder, 'applications') available_apps = [ arq for arq in os.listdir(applications_folder) if os.path.exists(os.path.join(applications_folder, arq, '__init__.py')) ] - self.pagesmenu.delete(0, len(available_apps)) for arq in available_apps: url = self.url + arq + item = lambda a = arq: self.try_start_browser(a) self.pagesmenu.add_command( - label=url, command=lambda u=url: start_browser(u)) + label=url, command=item) + + def try_start_browser(self, app): + url = self.url + app + if not is_appdir(self.options.folder, app): + self.pagesmenu.delete(url) + return + start_browser(url) def quit(self, justHide=False): """ Finishes the program execution """ @@ -423,16 +439,20 @@ class web2pyDialog(object): self.root.withdraw() else: try: - scheds = self.scheduler_processes.keys() + with self.scheduler_processes_lock: + scheds = list(self.scheduler_processes.keys()) for t in scheds: - self.try_stop_scheduler(t) - except: - pass - try: - newcron.stopcron() + self.try_stop_scheduler(t, skip_update=True) except: pass + if self.options.with_cron and not self.options.soft_cron: + # shutting down hardcron + try: + newcron.stopcron() + except: + pass try: + # HttpServer.stop takes care of stopping softcron self.server.stop() except: pass @@ -450,34 +470,40 @@ class web2pyDialog(object): import tkMessageBox as messagebox else: from tkinter import messagebox - messagebox.showerror('web2py start server', message) def start(self): """ Starts web2py server """ - password = self.password.get() - if not password: self.error('no password, no web admin interface') ip = self.selected_ip.get() - if not is_valid_ip_address(ip): return self.error('invalid host ip address') - try: port = int(self.port_number.get()) - except: + except ValueError: return self.error('invalid port number') if self.options.server_key and self.options.server_cert: proto = 'https' else: proto = 'http' - self.url = get_url(ip, proto=proto, port=port) + self.connect_pages() + self.update_schedulers() + + # softcron is stopped with HttpServer, thus if starting again + # need to reset newcron._stopping to re-enable cron + if self.options.soft_cron: + newcron.reset() + + # FIXME: if the HttpServer is stopped, then started again, + # does not start because of following error: + # WARNING:Rocket.Errors.Port8000:Listener started when not ready. + self.button_start.configure(state='disabled') try: @@ -502,7 +528,7 @@ class web2pyDialog(object): path=options.folder, interfaces=options.interfaces) - thread.start_new_thread(self.server.start, ()) + threading.Thread(target=self.server.start).start() except Exception as e: self.button_start.configure(state='normal') return self.error(str(e)) @@ -514,11 +540,14 @@ class web2pyDialog(object): self.button_stop.configure(state='normal') if not options.taskbar: - thread.start_new_thread( - start_browser, (get_url(ip, proto=proto, port=port), True)) + cpt = threading.Thread(target=start_browser, + args=(get_url(ip, proto=proto, port=port), True)) + cpt.setDaemon(True) + cpt.start() self.password.configure(state='readonly') - [ip.configure(state='disabled') for ip in self.ips.values()] + for ip in self.ips.values(): + ip.configure(state='disabled') self.port_number.configure(state='readonly') if self.tb: @@ -528,16 +557,15 @@ class web2pyDialog(object): for listener in self.server.server.listeners: if listener.ready: return True - return False def stop(self): """ Stops web2py server """ - self.button_start.configure(state='normal') self.button_stop.configure(state='disabled') self.password.configure(state='normal') - [ip.configure(state='normal') for ip in self.ips.values()] + for ip in self.ips.values(): + ip.configure(state='normal') self.port_number.configure(state='normal') self.server.stop() @@ -546,48 +574,41 @@ class web2pyDialog(object): def update_canvas(self): """ Updates canvas """ - httplog = os.path.join(self.options.folder, self.options.log_filename) + canvas = self.canvas try: t1 = os.path.getsize(httplog) - except: - self.canvas.after(1000, self.update_canvas) + except OSError: + canvas.after(1000, self.update_canvas) return + points = 400 try: - fp = open(httplog, 'r') - fp.seek(self.t0) - data = fp.read(t1 - self.t0) - fp.close() - value = self.p0[1:] + [10 + 90.0 / math.sqrt(1 + data.count('\n'))] - self.p0 = value + pvalues = self.p0[1:] + with open(httplog, 'r') as fp: + fp.seek(self.t0) + data = fp.read(t1 - self.t0) + self.p0 = pvalues + [10 + 90.0 / math.sqrt(1 + data.count('\n'))] - for i in xrange(len(self.p0) - 1): - c = self.canvas.coords(self.q0[i]) - self.canvas.coords(self.q0[i], - (c[0], - self.p0[i], - c[2], - self.p0[i + 1])) + for i in xrange(points - 1): + c = canvas.coords(self.q0[i]) + canvas.coords(self.q0[i], + (c[0], self.p0[i], + c[2], self.p0[i + 1])) self.t0 = t1 - except BaseException: + except AttributeError: self.t0 = time.time() self.t0 = t1 - self.p0 = [100] * 400 - self.q0 = [self.canvas.create_line(i, 100, i + 1, 100, - fill='green') for i in xrange(len(self.p0) - 1)] + self.p0 = [100] * points + self.q0 = [canvas.create_line(i, 100, i + 1, 100, + fill='green') for i in xrange(points - 1)] - self.canvas.after(1000, self.update_canvas) + canvas.after(1000, self.update_canvas) -def check_existent_app(options, appname): - if os.path.isdir(os.path.join(options.folder, 'applications', appname)): - return True - - -def get_code_for_scheduler(app_groups, options): +def get_code_for_scheduler(applications_parent, app_groups): app = app_groups[0] - if not check_existent_app(options, app): + if not is_appdir(applications_parent, app): print("Application '%s' doesn't exist, skipping" % app) return None, None code = 'from gluon.globals import current;' @@ -599,16 +620,10 @@ def get_code_for_scheduler(app_groups, options): def start_schedulers(options): - try: - from multiprocessing import Process - except: - sys.stderr.write('Sorry, -K only supported for Python 2.6+\n') - return - logging.getLogger().setLevel(options.log_level) - + from multiprocessing import Process apps = [ag.split(':') for ag in options.schedulers] if not options.with_scheduler and len(apps) == 1: - app, code = get_code_for_scheduler(apps[0], options) + app, code = get_code_for_scheduler(options.folder, apps[0]) if not app: return print('starting single-scheduler for "%s"...' % app) @@ -624,7 +639,7 @@ def start_schedulers(options): processes = [] for app_groups in apps: - app, code = get_code_for_scheduler(app_groups, options) + app, code = get_code_for_scheduler(options.folder, app_groups) if not app: continue print('starting scheduler for "%s"...' % app) @@ -652,6 +667,12 @@ def start(): # get command line arguments options = console(version=ProgramVersion) + if options.with_scheduler or len(options.schedulers) > 1: + try: + from multiprocessing import Process + except: + die('Sorry, -K/--scheduler only supported for Python 2.6+') + if options.gae: # write app.yaml, gaehandler.py, and exit if not os.path.exists('app.yaml'): @@ -673,6 +694,7 @@ def start(): logger = logging.getLogger("web2py") logger.setLevel(options.log_level) + logging.getLogger().setLevel(options.log_level) # root logger # on new installation build the scaffolding app create_welcome_w2p() @@ -724,11 +746,9 @@ def start(): if options.cron_run: # run cron (extcron) and exit - logger.debug('Starting extcron...') + logger.debug('Running extcron...') global_settings.web2py_crontype = 'external' - extcron = newcron.extcron(options.folder, apps=options.crontabs) - extcron.start() - extcron.join() + newcron.extcron(options.folder, apps=options.crontabs) return if not options.with_scheduler and options.schedulers: @@ -807,7 +827,7 @@ end tell options.password = getpass.getpass('choose a password:') if not options.password and not options.no_banner: - print('no password, disable admin interface') + print('no password, no web admin interface') # Use first interface IP and port if interfaces specified, since the # interfaces option overrides the IP (and related) options. @@ -883,5 +903,4 @@ end tell spt.join() except: logger.exception('error terminating schedulers') - pass logging.shutdown() diff --git a/scripts/autoroutes.py b/scripts/autoroutes.py old mode 100644 new mode 100755 diff --git a/scripts/check_lang_progress.py b/scripts/check_lang_progress.py old mode 100644 new mode 100755 diff --git a/scripts/cpdb.py b/scripts/cpdb.py old mode 100644 new mode 100755 diff --git a/scripts/cpplugin.py b/scripts/cpplugin.py old mode 100644 new mode 100755 diff --git a/scripts/dict_diff.py b/scripts/dict_diff.py old mode 100644 new mode 100755 diff --git a/scripts/drop-pgsql-tables.sh b/scripts/drop-pgsql-tables.sh old mode 100644 new mode 100755 diff --git a/scripts/extract_mssql_models.py b/scripts/extract_mssql_models.py old mode 100644 new mode 100755 diff --git a/scripts/extract_mysql_models.py b/scripts/extract_mysql_models.py old mode 100644 new mode 100755 diff --git a/scripts/extract_oracle_models.py b/scripts/extract_oracle_models.py old mode 100644 new mode 100755 diff --git a/scripts/extract_pgsql_models.py b/scripts/extract_pgsql_models.py old mode 100644 new mode 100755 diff --git a/scripts/extract_sqlite_models.py b/scripts/extract_sqlite_models.py old mode 100644 new mode 100755 diff --git a/scripts/import_static.py b/scripts/import_static.py old mode 100644 new mode 100755 diff --git a/scripts/make_min_web2py.py b/scripts/make_min_web2py.py old mode 100644 new mode 100755 diff --git a/scripts/parse_top_level_domains.py b/scripts/parse_top_level_domains.py old mode 100644 new mode 100755 diff --git a/scripts/rmorphans.py b/scripts/rmorphans.py old mode 100644 new mode 100755 diff --git a/scripts/setup-scheduler-centos.sh b/scripts/setup-scheduler-centos.sh old mode 100644 new mode 100755 diff --git a/scripts/setup-web2py-centos7.sh b/scripts/setup-web2py-centos7.sh old mode 100644 new mode 100755 diff --git a/scripts/setup-web2py-debian-sid.sh b/scripts/setup-web2py-debian-sid.sh old mode 100644 new mode 100755 diff --git a/scripts/setup-web2py-fedora.sh b/scripts/setup-web2py-fedora.sh old mode 100644 new mode 100755 diff --git a/scripts/setup-web2py-heroku.sh b/scripts/setup-web2py-heroku.sh old mode 100644 new mode 100755 diff --git a/scripts/setup-web2py-nginx-uwsgi-centos64.sh b/scripts/setup-web2py-nginx-uwsgi-centos64.sh old mode 100644 new mode 100755 diff --git a/scripts/setup-web2py-nginx-uwsgi-centos7.sh b/scripts/setup-web2py-nginx-uwsgi-centos7.sh old mode 100644 new mode 100755 diff --git a/scripts/setup-web2py-nginx-uwsgi-on-centos.sh b/scripts/setup-web2py-nginx-uwsgi-on-centos.sh old mode 100644 new mode 100755 diff --git a/scripts/setup-web2py-nginx-uwsgi-opensuse.sh b/scripts/setup-web2py-nginx-uwsgi-opensuse.sh old mode 100644 new mode 100755 diff --git a/scripts/setup-web2py-nginx-uwsgi-ubuntu.sh b/scripts/setup-web2py-nginx-uwsgi-ubuntu.sh old mode 100644 new mode 100755 diff --git a/scripts/setup-web2py-ubuntu.sh b/scripts/setup-web2py-ubuntu.sh old mode 100644 new mode 100755 diff --git a/scripts/standalone_exe_cxfreeze.py b/scripts/standalone_exe_cxfreeze.py old mode 100644 new mode 100755 diff --git a/scripts/update_languages.py b/scripts/update_languages.py old mode 100644 new mode 100755 diff --git a/scripts/web2py-lock.sh b/scripts/web2py-lock.sh old mode 100644 new mode 100755 diff --git a/scripts/web2py-scheduler.conf b/scripts/web2py-scheduler.conf old mode 100755 new mode 100644 diff --git a/scripts/web2py.fedora.sh b/scripts/web2py.fedora.sh old mode 100644 new mode 100755 diff --git a/scripts/zip_static_files.py b/scripts/zip_static_files.py old mode 100644 new mode 100755