diff options
-rwxr-xr-x | gerbolyze/__init__.py | 1 | ||||
-rw-r--r-- | gerboweb.service | 17 | ||||
-rw-r--r-- | gerboweb/Containerfile | 5 | ||||
-rw-r--r-- | gerboweb/gerboweb.py | 30 | ||||
-rw-r--r-- | gerboweb/gerboweb_prod.cfg | 1 | ||||
-rw-r--r-- | gerboweb/job_processor.py | 98 | ||||
-rw-r--r-- | gerboweb/static/style.css | 2 | ||||
-rw-r--r-- | gerboweb/templates/index.html | 13 | ||||
-rw-r--r-- | gerboweb/uwsgi-gerboweb.ini | 9 |
9 files changed, 132 insertions, 44 deletions
diff --git a/gerbolyze/__init__.py b/gerbolyze/__init__.py index 5a8cc43..fcae543 100755 --- a/gerbolyze/__init__.py +++ b/gerbolyze/__init__.py @@ -11,6 +11,7 @@ import sys import warnings import shutil from zipfile import ZipFile, is_zipfile +from pathlib import Path from lxml import etree import numpy as np diff --git a/gerboweb.service b/gerboweb.service new file mode 100644 index 0000000..8aa5210 --- /dev/null +++ b/gerboweb.service @@ -0,0 +1,17 @@ +[Unit] +Description=Gerboweb server service +Documentation=https://gitlab.com/gerbolyze/gerbolyze +Wants=network-online.target +Wants=nginx.service +After=nginx.service +RequiresMountsFor=/var/run/container/storage + +[Service] +ExecStart=/usr/bin/podman run --secret=gerboweb --conmon-pidfile=%t/container-gerboweb.pid --volume=${RUNTIME_DIRECTORY}:/run/uwsgi --detach gerboweb +ExecStop=/usr/bin/podman stop --time 2 gerboweb +Type=forking +PIDFile=%t/container-gerboweb.pid +RuntimeDirectory=gerboweb-uwsgi + +[Install] +WantedBy=default.target diff --git a/gerboweb/Containerfile b/gerboweb/Containerfile index 5733b48..e8a33f2 100644 --- a/gerboweb/Containerfile +++ b/gerboweb/Containerfile @@ -12,7 +12,8 @@ RUN --mount=type=bind,rw,destination=/git \ python3 -m pip --disable-pip-version-check install . RUN mkdir /gerboweb ADD ["gerboweb/uwsgi-gerboweb.ini","gerboweb/gerboweb.py","gerboweb/job_processor.py","gerboweb/job_queue.py","/gerboweb/"] -ADD ["gerboweb/static","/gerboweb/"] -ADD ["gerboweb/templates","/gerboweb/"] +ADD ["gerboweb/static","/gerboweb/static"] +ADD ["gerboweb/templates","/gerboweb/templates"] ADD gerboweb/gerboweb_prod.cfg /gerboweb/gerboweb.cfg +RUN mkdir /var/cache/gerboweb ENTRYPOINT uwsgi --ini /gerboweb/uwsgi-gerboweb.ini --chmod-socket=660 --socket=/run/uwsgi/socket diff --git a/gerboweb/gerboweb.py b/gerboweb/gerboweb.py index 6ab255c..fef713c 100644 --- a/gerboweb/gerboweb.py +++ b/gerboweb/gerboweb.py @@ -16,13 +16,15 @@ from flask_wtf.file import FileField, FileRequired from wtforms.fields import RadioField from wtforms.validators import DataRequired from werkzeug.utils import secure_filename +import uwsgidecorators from job_queue import JobQueue +import job_processor -app = Flask(__name__, static_url_path='/static') +app = Flask(__name__, static_url_path='/gerboweb/static') app.config.from_envvar('GERBOWEB_SETTINGS') if app.config['SECRET_KEY'] is None: - if (p := Path('/run/secrets/gerboweb')).isfile(): + if (p := Path('/run/secrets/gerboweb')).is_file(): app.config['SECRET_KEY'] = p.read_bytes() else: app.config['SECRET_KEY'] = os.urandom(32) @@ -38,6 +40,10 @@ class ResetForm(FlaskForm): job_queue = JobQueue(app.config['JOB_QUEUE_DB']) +@uwsgidecorators.timer(1) +def job_processor_timer(_num): + job_processor.process_job(job_queue) + def tempfile_path(namespace): """ Return a path for a per-session temporary file identified by the given namespace. Create the session tempfile dir if necessary. The application tempfile dir is controlled via the upload_path config value and not managed by @@ -92,7 +98,10 @@ def index(): def vectorize(): if 'vector_job' in session: - job_queue[session['vector_job']].abort() + try: + job_queue[session['vector_job']].abort() + except: + pass session['vector_job'] = job_queue.enqueue('vector', client=request.remote_addr, session_id=session['session_id'], @@ -102,7 +111,10 @@ def vectorize(): def render(): if 'render_job' in session: - job_queue[session['render_job']].abort() + try: + job_queue[session['render_job']].abort() + except: + pass session['render_job'] = job_queue.enqueue('render', session_id=session['session_id'], infile=tempfile_path('gerber.zip'), @@ -168,9 +180,15 @@ def output_download(): @require_session_id def session_reset(): if 'render_job' in session: - job_queue[session['render_job']].abort() + try: + job_queue[session['render_job']].abort() + except: + pass if 'vector_job' in session: - job_queue[session['vector_job']].abort() + try: + job_queue[session['vector_job']].abort() + except: + pass session.clear() flash('Session reset', 'success'); return redirect(url_for('index')) diff --git a/gerboweb/gerboweb_prod.cfg b/gerboweb/gerboweb_prod.cfg index 1f4b0f8..f6893e5 100644 --- a/gerboweb/gerboweb_prod.cfg +++ b/gerboweb/gerboweb_prod.cfg @@ -1,3 +1,4 @@ MAX_CONTENT_LENGTH=10000000 UPLOAD_PATH="/var/cache/gerboweb/upload" JOB_QUEUE_DB="/var/cache/gerboweb/job_queue.sqlite3" +APPLICATION_ROOT="/gerboweb/" diff --git a/gerboweb/job_processor.py b/gerboweb/job_processor.py index a916fd3..6c54d64 100644 --- a/gerboweb/job_processor.py +++ b/gerboweb/job_processor.py @@ -1,12 +1,79 @@ +import os +import sys import signal import subprocess import logging import itertools import tempfile +from pathlib import Path from job_queue import JobQueue +def run_resvg(*args): + if 'RESVG' in os.environ: + subprocess.run([os.environ['RESVG'], *args], check=True) + + else: + # By default, try four options: + for candidate in [ + # somewhere in $PATH + 'resvg', + + # in user-local cargo installation + Path.home() / '.cargo' / 'bin' / 'resvg', + + # somewhere in $PATH + 'wasi-resvg', + + # in user-local pip installation + Path.home() / '.local' / 'bin' / 'wasi-resvg', + + # next to our current python interpreter (e.g. in virtualenv) + str(Path(sys.executable).parent / 'resvg'), + str(Path(sys.executable).parent / 'wasi-resvg') ]: + + try: + subprocess.run([candidate, *args], check=True) + print('used svg-flatten at', candidate) + break + + except (FileNotFoundError, ModuleNotFoundError): + continue + + else: + raise SystemError('svg-flatten executable not found') + +def process_job(job_queue): + logging.debug('Checking for jobs') + for job in job_queue.job_iter('render'): + logging.info(f'Processing {job.type} job {job.id} session {job["session_id"]} from {job.client} submitted {job.created}') + with job: + try: + with tempfile.NamedTemporaryFile(suffix='.svg') as svg: + subprocess.run(['python3', '-m', 'gerbonara', '--top', job['infile'], svg.name], check=True) + run_resvg('--dpi', '300', svg.name, job['preview_top_out']) + with tempfile.NamedTemporaryFile(suffix='.svg') as svg: + subprocess.run(['python3', '-m', 'gerbonara', '--bottom', job['infile'], svg.name], check=True) + run_resvg('--dpi', '300', svg.name, job['preview_bottom_out']) + subprocess.run(['python3', '-m', 'gerbolyze', 'template', '--top', job['infile'], job['template_top_out']], check=True) + subprocess.run(['python3', '-m', 'gerbolyze', 'template', '--bottom', job['infile'], job['template_bottom_out']], check=True) + logging.info(f'Finishied processing {job.type} job {job.id}') + job.result = True + except: + logging.exception('Error during job processing') + job.result = False + + for job in job_queue.job_iter('vector'): + logging.info(f'Processing {job.type} job {job.id} session {job["session_id"]} from {job.client} submitted {job.created}') + with job: + try: + subprocess.run(['python3', '-m', 'gerbolyze', 'paste', job['gerber_in'], job['overlay'], job['gerber_out']], check=True) + logging.info(f'Finishied processing {job.type} job {job.id}') + job.result = True + except: + logging.exception('Error during job processing') + job.result = False if __name__ == '__main__': import argparse @@ -21,38 +88,11 @@ if __name__ == '__main__': logging.basicConfig(level=numeric_level) job_queue = JobQueue(args.queue) + print('Job processor online') signal.signal(signal.SIGALRM, lambda *args: None) # Ignore incoming alarm signals while processing jobs signal.setitimer(signal.ITIMER_REAL, 0.001, 1) while signal.sigwait([signal.SIGALRM, signal.SIGINT]) == signal.SIGALRM: - logging.debug('Checking for jobs') - for job in job_queue.job_iter('render'): - logging.info(f'Processing {job.type} job {job.id} session {job["session_id"]} from {job.client} submitted {job.created}') - with job: - try: - with tempfile.NamedTemporaryFile(suffix='.svg') as svg: - subprocess.run(['python3', '-m', 'gerbonara', '--top', job['infile'], svg.name], check=True) - subprocess.run(['resvg', '--dpi', '300', svg.name, job['preview_top_out']], check=True) - with tempfile.NamedTemporaryFile(suffix='.svg') as svg: - subprocess.run(['python3', '-m', 'gerbonara', '--bottom', job['infile'], svg.name], check=True) - subprocess.run(['resvg', '--dpi', '300', svg.name, job['preview_bottom_out']], check=True) - subprocess.run(['python3', '-m', 'gerbolyze', 'template', '--top', job['infile'], job['template_top_out']], check=True) - subprocess.run(['python3', '-m', 'gerbolyze', 'template', '--bottom', job['infile'], job['template_bottom_out']], check=True) - logging.info(f'Finishied processing {job.type} job {job.id}') - job.result = True - except: - logging.exception('Error during job processing') - job.result = False - - for job in job_queue.job_iter('vector'): - logging.info(f'Processing {job.type} job {job.id} session {job["session_id"]} from {job.client} submitted {job.created}') - with job: - try: - subprocess.run(['python3', '-m', 'gerbolyze', 'paste', job['gerber_in'], job['overlay'], job['gerber_out']], check=True) - logging.info(f'Finishied processing {job.type} job {job.id}') - job.result = True - except: - logging.exception('Error during job processing') - job.result = False + process_job(job_queue) logging.info('Caught SIGINT. Exiting.') diff --git a/gerboweb/static/style.css b/gerboweb/static/style.css index eb926dc..17b16de 100644 --- a/gerboweb/static/style.css +++ b/gerboweb/static/style.css @@ -73,7 +73,6 @@ body { } div.header { - background-image: url("/static/bg10.jpg"); background-position: center; background-size: cover; background-repeat: no-repeat; @@ -283,7 +282,6 @@ a.preview:link, a.preview:hover, a.preview:visited, a.preview:active { line-height: 70px; /* background-image: linear-gradient(to top right, var(--cg5), var(--cg6)); */ - background-image: url("/static/bg10.jpg"); background-position: center; background-size: cover; background-repeat: no-repeat; diff --git a/gerboweb/templates/index.html b/gerboweb/templates/index.html index fd5d410..8a77233 100644 --- a/gerboweb/templates/index.html +++ b/gerboweb/templates/index.html @@ -6,6 +6,17 @@ <link rel="icon" type="image/png" href="{{url_for('static', filename='favicon-512.png')}}"> <link rel="apple-touch-icon" href="{{url_for('static', filename='favicon-512.png')}}"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> +<style> + +div.header { + background-image: url("{{url_for('static', filename='bg10.jpg')}}"); +} + +.sample-images > h1 { + background-image: url("{{url_for('static', filename='bg10.jpg')}}"); +} +</style> + </head> <body> <div class="layout-container"> @@ -110,7 +121,7 @@ {{overlay_form.csrf_token}} </form> <div class="form-controls"> - <div class="form-label upload-label">Upload Overlay PNG file:</div> + <div class="form-label upload-label">Upload Overlay SVG file:</div> <input class='upload-button' form="overlay-upload-form" name="upload_file" size="20" type="file"> </div> <div class="submit-buttons"> diff --git a/gerboweb/uwsgi-gerboweb.ini b/gerboweb/uwsgi-gerboweb.ini index 94aaa88..7317dda 100644 --- a/gerboweb/uwsgi-gerboweb.ini +++ b/gerboweb/uwsgi-gerboweb.ini @@ -3,9 +3,10 @@ master = True cheap = True die-on-idle = False manage-script-name = True -plugins = python3 +plugins = python chdir = /gerboweb -mount = /=gerboweb:app +chmod-socket=666 +mount = /gerboweb=gerboweb:app env = GERBOWEB_SETTINGS=gerboweb.cfg -mule = job_processor.py - +mule = job_processor.py /var/cache/gerboweb/job_queue.sqlite3 +static-map = /static=/gerboweb/static |