8e6ee31596
ci / test (push) Successful in 6m18s
History-free snapshot of the private development repository at cbe45c5 (2026-09-09). Code, docs and public catalogue only; no plant or job knowledge layers, no client or job names (tools/scrub.py clean). The intro deck is left out of this snapshot because its slide images could not be checked for job drawings.
517 lines
24 KiB
Python
517 lines
24 KiB
Python
#!/usr/bin/env python3
|
|
# -*- coding: utf-8 -*-
|
|
"""Beam Analysis's local server -- the CPython half of the HTML front/back end.
|
|
|
|
python tools/ba_web.py [--reads reads.json] [--job-root DIR]
|
|
[--port N|0] [--open] [--title T]
|
|
|
|
`tools/pa_web.py` with the tool swapped (docs/ba-web-spec.md section 5): same
|
|
argv, same first stdout line, same 127.0.0.1 bind, same
|
|
`http.server.ThreadingHTTPServer`, same reads/job-root/mark resolution. The
|
|
page the server feeds is built by `tools/ba_pagespec.py` (the job <-> values
|
|
half, `build_pagespec` / `geometry`), and `POST /api/run` runs the beamcalc
|
|
engine IN-PROCESS -- `beamcalc.model.from_job` -> `beamcalc.solve.analyse` --
|
|
the way PA's server calls `panelanalysis.build_page`, never a subprocess: one
|
|
interpreter, one traceback (spec section 5). Results are `<job-root>/
|
|
beamanalysis/<mark>.job.json` (the job actually run), `<mark>.ba.json`
|
|
(`beamcalc.cli.summary(a)`), `<mark>.html` (`beamcalc.report.render(a)`).
|
|
|
|
NO REVIT ANYWHERE ON THIS PATH. Like pa_web.py, this module registers the
|
|
bare `vibe` package stub at import (`vibe/__init__.py` imports the Revit
|
|
API, which is fatal off Revit), then imports `tools/pa_web.py` for its
|
|
job/reads plumbing and the shared static handler, and `tools/ba_pagespec.py`
|
|
for the pagespec and the job conversion. `beamcalc` is plain CPython
|
|
(imported by ba_pagespec the ordinary way, falling back to
|
|
`beamcalc/src` on `sys.path`); `beamcalc.report` pulls `panellift.report`,
|
|
which is CPython too.
|
|
|
|
Routes (shared handler and payload shapes inherited from pa_web.py -- the
|
|
PA `app.js` front end is reused for this tool, spec section 7):
|
|
|
|
GET / tools/ba_web/static/index.html when it exists,
|
|
else tools/pa_web/static/index.html (app.js,
|
|
draw.js, units.js and willtools.css are PA's
|
|
single copies -- ba_web's static dir only
|
|
overrides them).
|
|
GET /api/spec the pagespec JSON: `ba_pagespec.
|
|
build_pagespec(values=..., reads=...)` with the
|
|
values file a previous /api/save left for this
|
|
mark merged over the `--reads` prefill. reads
|
|
is a PARTIAL job (spec section 6): only what
|
|
the model could supply, plus `basis`.
|
|
POST /api/geometry body {"values": {...}} -- {"geometry":
|
|
ba_pagespec.geometry(ba_pagespec.job_from_values(
|
|
values))}, the SAME dict /api/spec computes off
|
|
the values the client currently holds (not a
|
|
save, not the prefill). A ValueError parsing the
|
|
values -- job_from_values raises one whose
|
|
message starts with the values key -- or any
|
|
other exception comes back {"geometry": {},
|
|
"error": text}, never a dead page.
|
|
POST /api/save body {"values": {...}} -- persists that dict as
|
|
this mark's saved edits
|
|
(`<job-root>/beamanalysis/<mark>.values.json`),
|
|
read back by the next /api/spec.
|
|
POST /api/run body {"values": {...}} -- `ba_pagespec.
|
|
job_from_values(values)`, `beamcalc.model.
|
|
from_job(job)` and `beamcalc.solve.analyse` in
|
|
process. On a ValueError from the conversion/
|
|
read (a value the engine refuses), {"ok": false,
|
|
"kind": "input", "problems": [{"key": <the
|
|
values key the message starts with, or ''>,
|
|
"text": ...}]}; on any other exception,
|
|
{"ok": false, "kind": "error", "problems":
|
|
[...]} with the exception text and the
|
|
traceback to stderr only. `kind` is what the
|
|
shared page renders the refusal by -- "input"
|
|
is the user's to fix, "error" is a bug (see
|
|
pa_web.run_analysis). On success it
|
|
writes `<mark>.job.json`, `<mark>.ba.json`
|
|
(`cli.summary(a)`) and `<mark>.html`
|
|
(`report.render(a)`) into the same
|
|
`beamanalysis/` folder and answers {"ok": true,
|
|
"result_url": "/results/<mark>.html"}.
|
|
GET /results/<file> serves a written results HTML (or anything else
|
|
landed in the same folder) by filename.
|
|
POST /api/close acknowledges, then shuts the server down (used
|
|
by a page's own Close button, and by the Revit
|
|
button once its WebView2 window closes).
|
|
POST /api/log body {"text": ...} (anything JSON) -- appends
|
|
one line, `<ISO timestamp>\t<body compact>`, to
|
|
`<job-root>/beamanalysis/<mark>.client.log` and
|
|
answers {"ok": true} -- the client-side error
|
|
sink PA's `app.js` posts to (ba-web-spec.md
|
|
section 5: the route PA's server already has
|
|
for Beam Analysis to reuse).
|
|
|
|
Static assets are served from `tools/ba_web/static/` first and
|
|
`tools/pa_web/static/` second (spec section 5): ba_web/static holds only
|
|
`index.html` and `ba_drawings.js`; `app.js`, `units.js`, `willtools.css`
|
|
and `draw.js` are PA's single copies, never duplicated. A static path in
|
|
neither directory is a 404.
|
|
|
|
Only binds 127.0.0.1. Threaded (`http.server.ThreadingHTTPServer`) so a slow
|
|
solve never blocks a concurrent static request. Every request and every solve
|
|
exception is logged to stderr; nothing here ever prints a plant, job or
|
|
client name (`--reads` / `--job-root` are data, supplied by the caller, never
|
|
this file's own business -- same rule `tools/pa_pagespec.py` follows; the
|
|
`mark`, the `job_root` and every path to a written file stay OFF stdout).
|
|
The one place a client-supplied value is written anywhere is `POST
|
|
/api/log`, into `<mark>.client.log` -- into the FILE, never stdout.
|
|
|
|
JOB ROOT resolution (where `<mark>.values.json` / `<mark>.job.json` / the
|
|
results HTML all live, one folder per mark under `<job-root>/beamanalysis/`):
|
|
identical to pa_web.py -- the `--reads` file's own `"job_root"` field wins
|
|
when present; else `--job-root`; else `%LOCALAPPDATA%\\WillTools\\panelanalysis`
|
|
(pa_web.py's own default; Beam Analysis deliberately keeps the same fallback
|
|
folder rather than invent a second). The MARK itself is `--reads`' own
|
|
`"mark"` field, slugged, or the literal string `'blank'` when there is no
|
|
mark at all -- so two blank-path runs never collide with a real beam's saved
|
|
files, and a re-run of the same mark picks its saved edits back up.
|
|
|
|
`--port 0` (the default) lets the OS pick a free port; the actual URL is
|
|
printed as `BA_WEB_URL=http://127.0.0.1:<port>/` -- the FIRST line on stdout,
|
|
flushed immediately, so a launcher (the Revit button, a test) can read it off
|
|
a pipe before the server does anything else. `--open` additionally opens it
|
|
in the default browser.
|
|
"""
|
|
from __future__ import print_function
|
|
|
|
import argparse
|
|
import io
|
|
import json
|
|
import os
|
|
import sys
|
|
import threading
|
|
import traceback
|
|
import types
|
|
import webbrowser
|
|
|
|
HERE = os.path.dirname(os.path.abspath(__file__))
|
|
REPO = os.path.abspath(os.path.join(HERE, os.pardir))
|
|
LIB = os.path.join(REPO, 'WillTools.extension', 'lib')
|
|
if LIB not in sys.path:
|
|
sys.path.insert(0, LIB)
|
|
|
|
# Same trick `tools/pa_web.py` / `tools/deckcheck.py` use: `vibe/__init__.py`
|
|
# imports the Revit API at module scope, which is fatal off Revit. Register a
|
|
# bare `vibe` package pointed at the directory so the Revit-free submodules
|
|
# import normally and `__init__.py` never runs. pa_web.py does the same at
|
|
# its own import, so whichever module registers first wins -- identical stub.
|
|
if 'vibe' not in sys.modules:
|
|
_stub = types.ModuleType('vibe')
|
|
_stub.__path__ = [os.path.join(LIB, 'vibe')]
|
|
sys.modules['vibe'] = _stub
|
|
|
|
if HERE not in sys.path:
|
|
sys.path.insert(0, HERE)
|
|
|
|
# `beamcalc` is plain CPython; ba_pagespec imports it the ordinary way and
|
|
# falls back to the in-repo source tree (<repo>/beamcalc/src on sys.path) when
|
|
# it is not installed -- importing ba_pagespec performs that fallback.
|
|
import pa_web # noqa: E402 -- job/reads plumbing + the shared handler
|
|
import ba_pagespec # noqa: E402 -- pagespec builder, job <-> values
|
|
import beamcalc.model as BM # noqa: E402
|
|
from beamcalc import cli as BC_CLI # noqa: E402
|
|
from beamcalc import report as BC_REPORT # noqa: E402
|
|
from beamcalc import solve as BC_SOLVE # noqa: E402
|
|
|
|
# Re-exported rather than re-derived (docs/ba-web-spec.md section 5: "same
|
|
# argv ... same job_root resolution ... files live under
|
|
# <job_root>/beamanalysis/"): these ARE pa_web.py's own functions, working on
|
|
# a Context that differs only in which folder it points at.
|
|
_load_reads = pa_web._load_reads
|
|
_slug = pa_web._slug
|
|
_ensure_dir = pa_web._ensure_dir
|
|
_default_job_root = pa_web._default_job_root
|
|
_send_file = pa_web.Handler._send_file
|
|
_serve_from = pa_web.Handler._serve_from
|
|
_read_json_body = pa_web.Handler._read_json_body
|
|
|
|
BA_DIR_NAME = 'beamanalysis'
|
|
STATIC_DIRS = (os.path.join(HERE, 'ba_web', 'static'),
|
|
os.path.join(HERE, 'pa_web', 'static'))
|
|
DEFAULT_JOB_ROOT = _default_job_root() # %LOCALAPPDATA%\WillTools\panelanalysis
|
|
DEFAULT_TITLE = 'Beam Analysis'
|
|
|
|
|
|
class Context(object):
|
|
"""Everything one server process needs, resolved once at startup -- the
|
|
beam analogue of `pa_web.Context`, with the folder swapped."""
|
|
|
|
def __init__(self, reads, job_root, ba_dir, mark):
|
|
self.reads = reads
|
|
self.job_root = job_root
|
|
self.ba_dir = ba_dir
|
|
self.mark = mark
|
|
|
|
|
|
def build_context(reads_path=None, job_root_override=None):
|
|
"""`(reads, job_root, ba_dir, mark)` resolved exactly the way pa_web.py
|
|
resolves its own: `--reads`' `job_root` field, else `job_root_override`,
|
|
else `pa_web._default_job_root()`; mark slugged off `--reads`' `mark`
|
|
field, else `'blank'`. The folder is `<job_root>/beamanalysis/`."""
|
|
reads, reads_err = _load_reads(reads_path)
|
|
if reads_err:
|
|
sys.stderr.write('ba_web: ' + reads_err + '\n')
|
|
|
|
job_root = reads.get('job_root') or job_root_override or DEFAULT_JOB_ROOT
|
|
ba_dir = os.path.join(job_root, BA_DIR_NAME)
|
|
_ensure_dir(ba_dir)
|
|
|
|
mark_raw = (reads.get('mark') or '').strip()
|
|
mark = _slug(mark_raw) if mark_raw else 'blank'
|
|
|
|
return Context(reads=reads, job_root=job_root, ba_dir=ba_dir, mark=mark)
|
|
|
|
|
|
# ------------------------------------------------------------- the pagespec
|
|
|
|
def _values_path(ctx):
|
|
return os.path.join(ctx.ba_dir, ctx.mark + '.values.json')
|
|
|
|
|
|
def _load_saved_values(ctx):
|
|
"""The `{key: value}` dict a previous `/api/save` for this mark left --
|
|
`{}` for anything else (no file yet, a file that will not parse, a file
|
|
that is not shaped `{"values": {...}}`). Never raises."""
|
|
path = _values_path(ctx)
|
|
if not os.path.isfile(path):
|
|
return {}
|
|
try:
|
|
with io.open(path, 'r', encoding='utf-8') as fh:
|
|
data = json.load(fh)
|
|
except Exception:
|
|
return {}
|
|
if isinstance(data, dict):
|
|
values = data.get('values')
|
|
if isinstance(values, dict):
|
|
return values
|
|
return {}
|
|
|
|
|
|
def _save_values(ctx, values):
|
|
_ensure_dir(ctx.ba_dir)
|
|
blob = json.dumps({'values': values}, indent=1, sort_keys=True,
|
|
ensure_ascii=False)
|
|
with io.open(_values_path(ctx), 'w', encoding='utf-8') as fh:
|
|
fh.write(blob)
|
|
|
|
|
|
def build_spec_json(ctx):
|
|
"""The pagespec dict for GET /api/spec: prefill off `ctx.reads` -- a
|
|
PARTIAL job (ba-web-spec.md section 6: absent means unread) -- flattened
|
|
to values by `ba_pagespec.build_pagespec`, with any saved edits for this
|
|
mark folded over the result before the geometry is computed, so a saved
|
|
length/width edit also redraws the geometry, not just the raw number."""
|
|
try:
|
|
saved = _load_saved_values(ctx)
|
|
except Exception:
|
|
saved = {}
|
|
values = None
|
|
if isinstance(saved, dict) and saved:
|
|
# The saved dict is a complete values set from the last /api/save;
|
|
# build_pagespec copies it and fills geometry from it.
|
|
values = saved
|
|
return ba_pagespec.build_pagespec(values=values, reads=ctx.reads)
|
|
|
|
|
|
def geometry_for(ctx, values):
|
|
"""`{'geometry': {...}}` for `POST /api/geometry` -- the page's own
|
|
`values` dict, whatever the client currently holds, run back through
|
|
`ba_pagespec.geometry(ba_pagespec.job_from_values(values))` so the dict
|
|
reflects an edit the client made seconds ago and has not (or will not)
|
|
have saved yet. Never raises -- a `ValueError` from `job_from_values`
|
|
(its messages name the values key that will not parse) and any other
|
|
exception come back as an empty geometry dict plus an `error` string, so
|
|
a client-side redraw degrades to 'nothing to draw' rather than a dead
|
|
page."""
|
|
try:
|
|
geo = ba_pagespec.geometry(ba_pagespec.job_from_values(values))
|
|
return {'geometry': geo if isinstance(geo, dict) else {}}
|
|
except Exception as exc:
|
|
sys.stderr.write('ba_web: geometry rebuild raised:\n' +
|
|
traceback.format_exc())
|
|
return {'geometry': {},
|
|
'error': '{0}: {1}'.format(type(exc).__name__, exc)}
|
|
|
|
|
|
# ------------------------------------------------------------- run / solve
|
|
|
|
def _key_for_problem(message):
|
|
"""The values key a `ValueError` message is about, or `''` when none.
|
|
`ba_pagespec.job_from_values` and `beamcalc.model.from_job` both raise
|
|
`ValueError`s whose text names the key or job path that was refused
|
|
(`"extents.length_ft: not a number: 'abc'"`; `JobError('missing required
|
|
value: extents.section.width_in')`); the message may quote the key
|
|
(`'unknown section type: ...'` names no key at all). Best-effort: a
|
|
dotted key at the START of the message is the values key itself; a
|
|
dotted key inside the message maps to the values key when it is one the
|
|
spec renders; anything else is `''`."""
|
|
if not message:
|
|
return ''
|
|
head = message.split(':', 1)[0].strip()
|
|
if head and all(part.replace('_', '').isalnum() for part in head.split('.')):
|
|
return head
|
|
return ''
|
|
|
|
|
|
def run_analysis(ctx, values):
|
|
"""`{"ok": True, "result_url": ...}` or `{"ok": False, "kind": ...,
|
|
"problems": [...]}` -- `kind` as in `pa_web.run_analysis`: `'input'`
|
|
for a value the engine refuses, `'error'` for a solve/write that raised.
|
|
|
|
Runs `job_from_values` -> `model.from_job` -> `solve.analyse` IN-PROCESS
|
|
(the way PA's server calls `panelanalysis.build_page`), writes
|
|
`<mark>.job.json` (the job actually run), `<mark>.ba.json`
|
|
(`cli.summary(a)`) and `<mark>.html` (`report.render(a)`) into
|
|
`<ctx.ba_dir>`, and answers the result URL. Never raises: a `ValueError`
|
|
from the conversion/read is reported as a problem keyed to the values
|
|
key whose value caused it where that can be told (spec section 5); any
|
|
other exception is reported with the exception text and the traceback to
|
|
stderr only."""
|
|
job = None
|
|
try:
|
|
job = ba_pagespec.job_from_values(values)
|
|
except ValueError as exc:
|
|
return {'ok': False, 'kind': 'input', 'problems': [
|
|
{'key': _key_for_problem(str(exc)), 'text': str(exc)}]}
|
|
|
|
try:
|
|
beam = BM.from_job(job)
|
|
except ValueError as exc:
|
|
return {'ok': False, 'kind': 'input', 'problems': [
|
|
{'key': _key_for_problem(str(exc)), 'text': str(exc)}]}
|
|
|
|
try:
|
|
analysis = BC_SOLVE.analyse(beam)
|
|
except Exception as exc:
|
|
sys.stderr.write('ba_web: beamcalc.solve.analyse raised:\n' +
|
|
traceback.format_exc())
|
|
return {'ok': False, 'kind': 'error', 'problems': [
|
|
{'key': '', 'text': '{0}: {1}'.format(type(exc).__name__, exc)}]}
|
|
|
|
_ensure_dir(ctx.ba_dir)
|
|
mark = ctx.mark
|
|
paths = {ext: os.path.join(ctx.ba_dir, mark + ext)
|
|
for ext in ('.job.json', '.ba.json', '.html')}
|
|
try:
|
|
with io.open(paths['.job.json'], 'w', encoding='utf-8') as fh:
|
|
json.dump(job, fh, indent=1, sort_keys=True, ensure_ascii=False)
|
|
with io.open(paths['.ba.json'], 'w', encoding='utf-8') as fh:
|
|
json.dump(BC_CLI.summary(analysis), fh, indent=1, sort_keys=True,
|
|
ensure_ascii=False)
|
|
with io.open(paths['.html'], 'w', encoding='utf-8') as fh:
|
|
fh.write(BC_REPORT.render(analysis))
|
|
except Exception as exc:
|
|
sys.stderr.write('ba_web: could not write results:\n' +
|
|
traceback.format_exc())
|
|
return {'ok': False, 'kind': 'error', 'problems': [
|
|
{'key': '', 'text': '{0}: {1}'.format(type(exc).__name__, exc)}]}
|
|
|
|
return {'ok': True, 'result_url': '/results/' + mark + '.html'}
|
|
|
|
|
|
# ------------------------------------------------------------------ server
|
|
|
|
def make_server(ctx, port=0):
|
|
"""Bind 127.0.0.1 ONLY (never a routable interface), on `port` (0 = let
|
|
the OS pick). Threaded so a slow solve never blocks a concurrent static
|
|
request -- the same server shape as pa_web."""
|
|
server = pa_web.ThreadingHTTPServer(('127.0.0.1', port), Handler)
|
|
server.ctx = ctx
|
|
return server
|
|
|
|
|
|
class Handler(pa_web.Handler):
|
|
"""The route table, inherited plumbing from `pa_web.Handler` (the JSON
|
|
body reader, `_send_file`, `_serve_from`, the 404/403 handling, the
|
|
stderr request log). The static search walks ba_web/static FIRST and
|
|
pa_web/static SECOND, so `index.html` and `ba_drawings.js` come from the
|
|
beam folder and `app.js` / `draw.js` / `units.js` / `willtools.css` from
|
|
PA's single copies."""
|
|
|
|
server_version = 'BAWeb/1'
|
|
|
|
def log_message(self, fmt, *args):
|
|
try:
|
|
sys.stderr.write('ba_web: {0} - {1}\n'.format(
|
|
self.address_string(), fmt % args))
|
|
except Exception:
|
|
pass
|
|
|
|
def _static_candidates(self, path):
|
|
rel = path.lstrip('/') or 'index.html'
|
|
for root in STATIC_DIRS:
|
|
yield root, rel
|
|
|
|
# -------------------------------------------------------------- GET
|
|
|
|
def do_GET(self):
|
|
ctx = self.server.ctx
|
|
path = pa_web.unquote(pa_web.urlparse(self.path).path)
|
|
try:
|
|
if path == '/api/spec':
|
|
self._send_json(build_spec_json(ctx))
|
|
return
|
|
if path.startswith('/results/'):
|
|
self._serve_from(ctx.ba_dir, path[len('/results/'):],
|
|
'text/html')
|
|
return
|
|
if path.startswith('/api/'):
|
|
self._send_json({'ok': False, 'problems': [
|
|
{'key': '', 'text': 'unknown endpoint'}]}, status=404)
|
|
return
|
|
for root, rel in self._static_candidates(path):
|
|
full = os.path.normpath(os.path.join(root, rel))
|
|
root_norm = os.path.normpath(root)
|
|
if not (full == root_norm or
|
|
full.startswith(root_norm + os.sep)):
|
|
self.send_error(403, 'forbidden')
|
|
return
|
|
if os.path.isfile(full):
|
|
self._send_file(full,
|
|
'text/html' if rel == 'index.html'
|
|
else 'application/octet-stream')
|
|
return
|
|
self.send_error(404, 'not found')
|
|
except Exception as exc:
|
|
self._send_error_json(exc)
|
|
|
|
# ------------------------------------------------------------- POST
|
|
|
|
def do_POST(self):
|
|
ctx = self.server.ctx
|
|
path = pa_web.urlparse(self.path).path
|
|
try:
|
|
if path == '/api/save':
|
|
values = self._read_json_body().get('values') or {}
|
|
_save_values(ctx, values)
|
|
self._send_json({'ok': True})
|
|
return
|
|
if path == '/api/run':
|
|
values = self._read_json_body().get('values') or {}
|
|
self._send_json(run_analysis(ctx, values))
|
|
return
|
|
if path == '/api/geometry':
|
|
values = self._read_json_body().get('values') or {}
|
|
self._send_json(geometry_for(ctx, values))
|
|
return
|
|
if path == '/api/log':
|
|
body = self._read_json_body()
|
|
try:
|
|
log_ctx = pa_web.Context(
|
|
reads=ctx.reads, know={}, job_root=ctx.job_root,
|
|
pa_dir=ctx.ba_dir, mark=ctx.mark,
|
|
static_dir=STATIC_DIRS[0])
|
|
pa_web._append_client_log(log_ctx, body)
|
|
except Exception as e: # the docstring's promise: never raises
|
|
sys.stderr.write('client.log append failed: %r\n' % (e,))
|
|
self._send_json({'ok': True})
|
|
return
|
|
if path == '/api/close':
|
|
self._send_json({'ok': True})
|
|
threading.Thread(target=self.server.shutdown).start()
|
|
return
|
|
self._send_json({'ok': False, 'problems': [
|
|
{'key': '', 'text': 'unknown endpoint'}]}, status=404)
|
|
except Exception as exc:
|
|
self._send_error_json(exc)
|
|
|
|
|
|
# --------------------------------------------------------------------- CLI
|
|
|
|
def main(argv=None):
|
|
ap = argparse.ArgumentParser(
|
|
description='Serve the Beam Analysis HTML front/back end.')
|
|
ap.add_argument('--reads', default=None,
|
|
help='JSON file of the model reads dict -- a PARTIAL job '
|
|
'plus "basis" (the Revit gatherer writes one; omit '
|
|
'for a blank, still-complete form)')
|
|
ap.add_argument('--job-root', dest='job_root', default=None,
|
|
help='folder to store this run under (overridden by '
|
|
"--reads' own job_root field, if present)")
|
|
ap.add_argument('--port', type=int, default=0,
|
|
help='0 (default) picks a free port')
|
|
ap.add_argument('--open', action='store_true',
|
|
help='open the page in the default browser')
|
|
ap.add_argument('--title', default=DEFAULT_TITLE,
|
|
help='cosmetic only -- logged at startup')
|
|
args = ap.parse_args(argv)
|
|
|
|
ctx = build_context(args.reads, args.job_root)
|
|
|
|
try:
|
|
server = make_server(ctx, args.port or 0)
|
|
except OSError as exc:
|
|
sys.stderr.write('ba_web: could not bind 127.0.0.1:{0}: {1}\n'.format(
|
|
args.port, exc))
|
|
return 1
|
|
|
|
url = 'http://127.0.0.1:{0}/'.format(server.server_address[1])
|
|
# FIRST line on stdout, flushed immediately -- a launcher (the Revit
|
|
# button, a test) reads this off a pipe before anything else happens.
|
|
# The mark / job_root / folder paths are written to STDERR only.
|
|
print('BA_WEB_URL={0}'.format(url))
|
|
sys.stdout.flush()
|
|
sys.stderr.write('ba_web: serving {0} for mark={1} (job_root={2}, '
|
|
'title={3})\n'.format(url, ctx.mark, ctx.job_root,
|
|
args.title))
|
|
|
|
if args.open:
|
|
try:
|
|
webbrowser.open(url)
|
|
except Exception as exc:
|
|
sys.stderr.write('ba_web: could not open a browser: {0}\n'
|
|
.format(exc))
|
|
|
|
try:
|
|
server.serve_forever()
|
|
finally:
|
|
server.server_close()
|
|
return 0
|
|
|
|
|
|
if __name__ == '__main__':
|
|
raise SystemExit(main())
|