added plantUML support
fixed bug with subscription being notified multiple times
This commit is contained in:
@@ -32,7 +32,8 @@ VOLUME /srv/http
|
|||||||
WORKDIR /srv/http
|
WORKDIR /srv/http
|
||||||
|
|
||||||
ENV GRANIAN_HOST=0.0.0.0
|
ENV GRANIAN_HOST=0.0.0.0
|
||||||
ENV GRANIAN_INTERFACE=asginl
|
ENV GRANIAN_PORT=8000
|
||||||
|
ENV GRANIAN_INTERFACE=asgi
|
||||||
ENV GRANIAN_LOOP=asyncio
|
ENV GRANIAN_LOOP=asyncio
|
||||||
ENV GRANIAN_LOG_ENABLED=false
|
ENV GRANIAN_LOG_ENABLED=false
|
||||||
|
|
||||||
|
@@ -1,9 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
set -e
|
|
||||||
|
|
||||||
venv/bin/python -m build
|
|
||||||
mkdir -p docker/build
|
|
||||||
cp dist/bugis-*.whl docker/build/
|
|
||||||
cp docker/Dockerfile docker/build/Dockerfile
|
|
||||||
|
|
||||||
docker build docker/build --tag bugis:latest
|
|
18
conf/log.yml
18
conf/log.yml
@@ -1,18 +0,0 @@
|
|||||||
version: 1
|
|
||||||
disable_existing_loggers: True
|
|
||||||
handlers:
|
|
||||||
console:
|
|
||||||
class : logging.StreamHandler
|
|
||||||
formatter: default
|
|
||||||
level : INFO
|
|
||||||
stream : ext://sys.stdout
|
|
||||||
formatters:
|
|
||||||
brief:
|
|
||||||
format: '%(message)s'
|
|
||||||
default:
|
|
||||||
format: '%(asctime)s %(levelname)-8s %(name)-15s %(threadName)s %(message)s'
|
|
||||||
datefmt: '%Y-%m-%d %H:%M:%S'
|
|
||||||
loggers:
|
|
||||||
root:
|
|
||||||
handlers: [console]
|
|
||||||
level: INFO
|
|
46
docker-compose.yaml
Normal file
46
docker-compose.yaml
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
networks:
|
||||||
|
default:
|
||||||
|
external: false
|
||||||
|
ipam:
|
||||||
|
driver: default
|
||||||
|
config:
|
||||||
|
- subnet: 172.128.0.0/16
|
||||||
|
ip_range: 172.128.0.0/16
|
||||||
|
gateway: 172.128.0.254
|
||||||
|
|
||||||
|
services:
|
||||||
|
granian:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
user: $UID:$GID
|
||||||
|
restart: unless-stopped
|
||||||
|
# container_name: granian
|
||||||
|
environment:
|
||||||
|
PLANT_UML_SERVER_ADDRESS: http://plant_uml:8080
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
cpus: "0.5"
|
||||||
|
memory: 512M
|
||||||
|
volumes:
|
||||||
|
- ${STATIC_ROOT}:/srv/http
|
||||||
|
plant_uml:
|
||||||
|
image: plantuml/plantuml-server:jetty
|
||||||
|
# container_name: plantUML
|
||||||
|
restart: unless-stopped
|
||||||
|
tmpfs: /tmp/jetty
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
cpus: "4"
|
||||||
|
memory: 1G
|
||||||
|
nginx:
|
||||||
|
image: gitea.woggioni.net/woggioni/nginx:v1.27.2
|
||||||
|
# container_name: nginx
|
||||||
|
restart: unless-stopped
|
||||||
|
depends_on:
|
||||||
|
- granian
|
||||||
|
volumes:
|
||||||
|
- ./conf/nginx-bugis.conf:/etc/nginx/conf.d/bugis.conf:ro
|
||||||
|
ports:
|
||||||
|
- 127.0.0.1:80:8080
|
@@ -28,7 +28,8 @@ dependencies = [
|
|||||||
"pwo",
|
"pwo",
|
||||||
"PyYAML",
|
"PyYAML",
|
||||||
"pygraphviz",
|
"pygraphviz",
|
||||||
"aiofiles"
|
"aiofiles",
|
||||||
|
"aiohttp[speedups]"
|
||||||
]
|
]
|
||||||
|
|
||||||
[project.optional-dependencies]
|
[project.optional-dependencies]
|
||||||
|
@@ -1,22 +1,39 @@
|
|||||||
#
|
#
|
||||||
# This file is autogenerated by pip-compile with Python 3.12
|
# This file is autogenerated by pip-compile with Python 3.10
|
||||||
# by the following command:
|
# by the following command:
|
||||||
#
|
#
|
||||||
# pip-compile --extra=dev --output-file=requirements-dev.txt pyproject.toml
|
# pip-compile --extra-index-url=https://gitea.woggioni.net/api/packages/woggioni/pypi/simple --extra=dev --output-file=requirements-dev.txt --strip-extras pyproject.toml
|
||||||
#
|
#
|
||||||
--index-url https://gitea.woggioni.net/api/packages/woggioni/pypi/simple
|
--extra-index-url https://gitea.woggioni.net/api/packages/woggioni/pypi/simple
|
||||||
--extra-index-url https://pypi.org/simple
|
|
||||||
|
|
||||||
|
aiodns==3.2.0
|
||||||
|
# via aiohttp
|
||||||
aiofiles==24.1.0
|
aiofiles==24.1.0
|
||||||
# via bugis (pyproject.toml)
|
# via bugis (pyproject.toml)
|
||||||
|
aiohappyeyeballs==2.4.3
|
||||||
|
# via aiohttp
|
||||||
|
aiohttp==3.10.10
|
||||||
|
# via bugis (pyproject.toml)
|
||||||
|
aiosignal==1.3.1
|
||||||
|
# via aiohttp
|
||||||
asttokens==2.4.1
|
asttokens==2.4.1
|
||||||
# via stack-data
|
# via stack-data
|
||||||
|
async-timeout==4.0.3
|
||||||
|
# via aiohttp
|
||||||
|
attrs==24.2.0
|
||||||
|
# via aiohttp
|
||||||
|
backports-tarfile==1.2.0
|
||||||
|
# via jaraco-context
|
||||||
|
brotli==1.1.0
|
||||||
|
# via aiohttp
|
||||||
build==1.2.2.post1
|
build==1.2.2.post1
|
||||||
# via bugis (pyproject.toml)
|
# via bugis (pyproject.toml)
|
||||||
certifi==2024.8.30
|
certifi==2024.8.30
|
||||||
# via requests
|
# via requests
|
||||||
cffi==1.17.1
|
cffi==1.17.1
|
||||||
# via cryptography
|
# via
|
||||||
|
# cryptography
|
||||||
|
# pycares
|
||||||
charset-normalizer==3.4.0
|
charset-normalizer==3.4.0
|
||||||
# via requests
|
# via requests
|
||||||
click==8.1.7
|
click==8.1.7
|
||||||
@@ -29,14 +46,24 @@ decorator==5.1.1
|
|||||||
# ipython
|
# ipython
|
||||||
docutils==0.21.2
|
docutils==0.21.2
|
||||||
# via readme-renderer
|
# via readme-renderer
|
||||||
|
exceptiongroup==1.2.2
|
||||||
|
# via ipython
|
||||||
executing==2.1.0
|
executing==2.1.0
|
||||||
# via stack-data
|
# via stack-data
|
||||||
|
frozenlist==1.4.1
|
||||||
|
# via
|
||||||
|
# aiohttp
|
||||||
|
# aiosignal
|
||||||
granian==1.6.1
|
granian==1.6.1
|
||||||
# via bugis (pyproject.toml)
|
# via bugis (pyproject.toml)
|
||||||
idna==3.10
|
idna==3.10
|
||||||
# via requests
|
# via
|
||||||
|
# requests
|
||||||
|
# yarl
|
||||||
importlib-metadata==8.5.0
|
importlib-metadata==8.5.0
|
||||||
# via twine
|
# via
|
||||||
|
# keyring
|
||||||
|
# twine
|
||||||
ipdb==0.13.13
|
ipdb==0.13.13
|
||||||
# via bugis (pyproject.toml)
|
# via bugis (pyproject.toml)
|
||||||
ipython==8.28.0
|
ipython==8.28.0
|
||||||
@@ -67,6 +94,10 @@ more-itertools==10.5.0
|
|||||||
# via
|
# via
|
||||||
# jaraco-classes
|
# jaraco-classes
|
||||||
# jaraco-functools
|
# jaraco-functools
|
||||||
|
multidict==6.1.0
|
||||||
|
# via
|
||||||
|
# aiohttp
|
||||||
|
# yarl
|
||||||
mypy==1.12.1
|
mypy==1.12.1
|
||||||
# via bugis (pyproject.toml)
|
# via bugis (pyproject.toml)
|
||||||
mypy-extensions==1.0.0
|
mypy-extensions==1.0.0
|
||||||
@@ -83,12 +114,16 @@ pkginfo==1.10.0
|
|||||||
# via twine
|
# via twine
|
||||||
prompt-toolkit==3.0.48
|
prompt-toolkit==3.0.48
|
||||||
# via ipython
|
# via ipython
|
||||||
|
propcache==0.2.0
|
||||||
|
# via yarl
|
||||||
ptyprocess==0.7.0
|
ptyprocess==0.7.0
|
||||||
# via pexpect
|
# via pexpect
|
||||||
pure-eval==0.2.3
|
pure-eval==0.2.3
|
||||||
# via stack-data
|
# via stack-data
|
||||||
pwo==0.0.3
|
pwo==0.0.3
|
||||||
# via bugis (pyproject.toml)
|
# via bugis (pyproject.toml)
|
||||||
|
pycares==4.4.0
|
||||||
|
# via aiodns
|
||||||
pycparser==2.22
|
pycparser==2.22
|
||||||
# via cffi
|
# via cffi
|
||||||
pygments==2.18.0
|
pygments==2.18.0
|
||||||
@@ -121,6 +156,11 @@ six==1.16.0
|
|||||||
# via asttokens
|
# via asttokens
|
||||||
stack-data==0.6.3
|
stack-data==0.6.3
|
||||||
# via ipython
|
# via ipython
|
||||||
|
tomli==2.0.2
|
||||||
|
# via
|
||||||
|
# build
|
||||||
|
# ipdb
|
||||||
|
# mypy
|
||||||
traitlets==5.14.3
|
traitlets==5.14.3
|
||||||
# via
|
# via
|
||||||
# ipython
|
# ipython
|
||||||
@@ -129,8 +169,11 @@ twine==5.1.1
|
|||||||
# via bugis (pyproject.toml)
|
# via bugis (pyproject.toml)
|
||||||
typing-extensions==4.7.1
|
typing-extensions==4.7.1
|
||||||
# via
|
# via
|
||||||
|
# ipython
|
||||||
|
# multidict
|
||||||
# mypy
|
# mypy
|
||||||
# pwo
|
# pwo
|
||||||
|
# rich
|
||||||
urllib3==2.2.3
|
urllib3==2.2.3
|
||||||
# via
|
# via
|
||||||
# requests
|
# requests
|
||||||
@@ -141,5 +184,7 @@ watchdog==5.0.3
|
|||||||
# via bugis (pyproject.toml)
|
# via bugis (pyproject.toml)
|
||||||
wcwidth==0.2.13
|
wcwidth==0.2.13
|
||||||
# via prompt-toolkit
|
# via prompt-toolkit
|
||||||
|
yarl==1.16.0
|
||||||
|
# via aiohttp
|
||||||
zipp==3.20.2
|
zipp==3.20.2
|
||||||
# via importlib-metadata
|
# via importlib-metadata
|
||||||
|
@@ -1,22 +1,53 @@
|
|||||||
#
|
#
|
||||||
# This file is autogenerated by pip-compile with Python 3.12
|
# This file is autogenerated by pip-compile with Python 3.10
|
||||||
# by the following command:
|
# by the following command:
|
||||||
#
|
#
|
||||||
# pip-compile --extra=run --output-file=requirements-run.txt pyproject.toml
|
# pip-compile --extra-index-url=https://gitea.woggioni.net/api/packages/woggioni/pypi/simple --extra=run --output-file=requirements-run.txt --strip-extras pyproject.toml
|
||||||
#
|
#
|
||||||
--index-url https://gitea.woggioni.net/api/packages/woggioni/pypi/simple
|
--extra-index-url https://gitea.woggioni.net/api/packages/woggioni/pypi/simple
|
||||||
--extra-index-url https://pypi.org/simple
|
|
||||||
|
|
||||||
|
aiodns==3.2.0
|
||||||
|
# via aiohttp
|
||||||
aiofiles==24.1.0
|
aiofiles==24.1.0
|
||||||
# via bugis (pyproject.toml)
|
# via bugis (pyproject.toml)
|
||||||
|
aiohappyeyeballs==2.4.3
|
||||||
|
# via aiohttp
|
||||||
|
aiohttp==3.10.10
|
||||||
|
# via bugis (pyproject.toml)
|
||||||
|
aiosignal==1.3.1
|
||||||
|
# via aiohttp
|
||||||
|
async-timeout==4.0.3
|
||||||
|
# via aiohttp
|
||||||
|
attrs==24.2.0
|
||||||
|
# via aiohttp
|
||||||
|
brotli==1.1.0
|
||||||
|
# via aiohttp
|
||||||
|
cffi==1.17.1
|
||||||
|
# via pycares
|
||||||
click==8.1.7
|
click==8.1.7
|
||||||
# via granian
|
# via granian
|
||||||
|
frozenlist==1.4.1
|
||||||
|
# via
|
||||||
|
# aiohttp
|
||||||
|
# aiosignal
|
||||||
granian==1.6.1
|
granian==1.6.1
|
||||||
# via bugis (pyproject.toml)
|
# via bugis (pyproject.toml)
|
||||||
|
idna==3.10
|
||||||
|
# via yarl
|
||||||
markdown==3.7
|
markdown==3.7
|
||||||
# via bugis (pyproject.toml)
|
# via bugis (pyproject.toml)
|
||||||
|
multidict==6.1.0
|
||||||
|
# via
|
||||||
|
# aiohttp
|
||||||
|
# yarl
|
||||||
|
propcache==0.2.0
|
||||||
|
# via yarl
|
||||||
pwo==0.0.3
|
pwo==0.0.3
|
||||||
# via bugis (pyproject.toml)
|
# via bugis (pyproject.toml)
|
||||||
|
pycares==4.4.0
|
||||||
|
# via aiodns
|
||||||
|
pycparser==2.22
|
||||||
|
# via cffi
|
||||||
pygments==2.18.0
|
pygments==2.18.0
|
||||||
# via bugis (pyproject.toml)
|
# via bugis (pyproject.toml)
|
||||||
pygraphviz==1.14
|
pygraphviz==1.14
|
||||||
@@ -24,8 +55,12 @@ pygraphviz==1.14
|
|||||||
pyyaml==6.0.2
|
pyyaml==6.0.2
|
||||||
# via bugis (pyproject.toml)
|
# via bugis (pyproject.toml)
|
||||||
typing-extensions==4.7.1
|
typing-extensions==4.7.1
|
||||||
# via pwo
|
# via
|
||||||
|
# multidict
|
||||||
|
# pwo
|
||||||
uvloop==0.21.0
|
uvloop==0.21.0
|
||||||
# via granian
|
# via granian
|
||||||
watchdog==5.0.3
|
watchdog==5.0.3
|
||||||
# via bugis (pyproject.toml)
|
# via bugis (pyproject.toml)
|
||||||
|
yarl==1.16.0
|
||||||
|
# via aiohttp
|
||||||
|
@@ -1,18 +1,49 @@
|
|||||||
#
|
#
|
||||||
# This file is autogenerated by pip-compile with Python 3.12
|
# This file is autogenerated by pip-compile with Python 3.10
|
||||||
# by the following command:
|
# by the following command:
|
||||||
#
|
#
|
||||||
# pip-compile --extra='' --output-file=requirements-.txt pyproject.toml
|
# pip-compile --extra-index-url=https://gitea.woggioni.net/api/packages/woggioni/pypi/simple --output-file=requirements.txt --strip-extras pyproject.toml
|
||||||
#
|
#
|
||||||
--index-url https://gitea.woggioni.net/api/packages/woggioni/pypi/simple
|
--extra-index-url https://gitea.woggioni.net/api/packages/woggioni/pypi/simple
|
||||||
--extra-index-url https://pypi.org/simple
|
|
||||||
|
|
||||||
|
aiodns==3.2.0
|
||||||
|
# via aiohttp
|
||||||
aiofiles==24.1.0
|
aiofiles==24.1.0
|
||||||
# via bugis (pyproject.toml)
|
# via bugis (pyproject.toml)
|
||||||
|
aiohappyeyeballs==2.4.3
|
||||||
|
# via aiohttp
|
||||||
|
aiohttp==3.10.10
|
||||||
|
# via bugis (pyproject.toml)
|
||||||
|
aiosignal==1.3.1
|
||||||
|
# via aiohttp
|
||||||
|
async-timeout==4.0.3
|
||||||
|
# via aiohttp
|
||||||
|
attrs==24.2.0
|
||||||
|
# via aiohttp
|
||||||
|
brotli==1.1.0
|
||||||
|
# via aiohttp
|
||||||
|
cffi==1.17.1
|
||||||
|
# via pycares
|
||||||
|
frozenlist==1.4.1
|
||||||
|
# via
|
||||||
|
# aiohttp
|
||||||
|
# aiosignal
|
||||||
|
idna==3.10
|
||||||
|
# via yarl
|
||||||
markdown==3.7
|
markdown==3.7
|
||||||
# via bugis (pyproject.toml)
|
# via bugis (pyproject.toml)
|
||||||
|
multidict==6.1.0
|
||||||
|
# via
|
||||||
|
# aiohttp
|
||||||
|
# yarl
|
||||||
|
propcache==0.2.0
|
||||||
|
# via yarl
|
||||||
pwo==0.0.3
|
pwo==0.0.3
|
||||||
# via bugis (pyproject.toml)
|
# via bugis (pyproject.toml)
|
||||||
|
pycares==4.4.0
|
||||||
|
# via aiodns
|
||||||
|
pycparser==2.22
|
||||||
|
# via cffi
|
||||||
pygments==2.18.0
|
pygments==2.18.0
|
||||||
# via bugis (pyproject.toml)
|
# via bugis (pyproject.toml)
|
||||||
pygraphviz==1.14
|
pygraphviz==1.14
|
||||||
@@ -20,6 +51,10 @@ pygraphviz==1.14
|
|||||||
pyyaml==6.0.2
|
pyyaml==6.0.2
|
||||||
# via bugis (pyproject.toml)
|
# via bugis (pyproject.toml)
|
||||||
typing-extensions==4.7.1
|
typing-extensions==4.7.1
|
||||||
# via pwo
|
# via
|
||||||
|
# multidict
|
||||||
|
# pwo
|
||||||
watchdog==5.0.3
|
watchdog==5.0.3
|
||||||
# via bugis (pyproject.toml)
|
# via bugis (pyproject.toml)
|
||||||
|
yarl==1.16.0
|
||||||
|
# via aiohttp
|
||||||
|
@@ -1,11 +1,11 @@
|
|||||||
import logging
|
import logging
|
||||||
from logging.config import dictConfig as configure_logging
|
from logging.config import dictConfig as configure_logging
|
||||||
from os import environ
|
|
||||||
from yaml import safe_load
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
logging_configuration_file = environ.get("LOGGING_CONFIGURATION_FILE", Path(__file__).parent / 'default-conf' / 'logging.yaml')
|
from yaml import safe_load
|
||||||
with open(logging_configuration_file, 'r') as input_file:
|
|
||||||
|
from .configuration import Configuration
|
||||||
|
|
||||||
|
with open(Configuration.instance.logging_configuration_file, 'r') as input_file:
|
||||||
conf = safe_load(input_file)
|
conf = safe_load(input_file)
|
||||||
configure_logging(conf)
|
configure_logging(conf)
|
||||||
|
|
||||||
@@ -13,55 +13,51 @@ with open(logging_configuration_file, 'r') as input_file:
|
|||||||
from pwo import Maybe
|
from pwo import Maybe
|
||||||
from .server import Server
|
from .server import Server
|
||||||
from asyncio import get_running_loop
|
from asyncio import get_running_loop
|
||||||
|
from .asgi_utils import decode_headers
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
log = logging.getLogger('access')
|
log = logging.getLogger('access')
|
||||||
log.propagate = False
|
log.propagate = False
|
||||||
|
|
||||||
_server = None
|
_server : Optional[Server] = None
|
||||||
|
|
||||||
def decode_headers(headers):
|
async def application(scope, receive, send):
|
||||||
result = dict()
|
|
||||||
for key, value in headers:
|
|
||||||
if isinstance(key, bytes):
|
|
||||||
key = key.decode()
|
|
||||||
if isinstance(value, bytes):
|
|
||||||
value = value.decode()
|
|
||||||
l = result.setdefault(key, list())
|
|
||||||
l.append(value)
|
|
||||||
return {
|
|
||||||
k: tuple(v) for k, v in result.items()
|
|
||||||
}
|
|
||||||
|
|
||||||
async def application(ctx, receive, send):
|
|
||||||
global _server
|
global _server
|
||||||
if _server is None:
|
if scope['type'] == 'lifespan':
|
||||||
_server = Server(loop=get_running_loop(), prefix=None)
|
while True:
|
||||||
|
message = await receive()
|
||||||
def maybe_log(evt):
|
if message['type'] == 'lifespan.startup':
|
||||||
d = {
|
_server = Server(loop=get_running_loop(), prefix=None)
|
||||||
'response_headers': (Maybe.of_nullable(evt.get('headers'))
|
await send({'type': 'lifespan.startup.complete'})
|
||||||
.map(decode_headers)
|
elif message['type'] == 'lifespan.shutdown':
|
||||||
.or_none()),
|
await _server.stop()
|
||||||
'status': evt['status']
|
await send({'type': 'lifespan.shutdown.complete'})
|
||||||
}
|
else:
|
||||||
log.info(None, extra=dict(**{k : v for k, v in d.items() if k is not None}, **ctx))
|
def maybe_log(evt):
|
||||||
def wrapped_send(*args, **kwargs):
|
d = {
|
||||||
result = send(*args, **kwargs)
|
'response_headers': (Maybe.of_nullable(evt.get('headers'))
|
||||||
(Maybe.of(args)
|
.map(decode_headers)
|
||||||
.filter(lambda it: len(it) > 0)
|
.or_none()),
|
||||||
.map(lambda it: it[0])
|
'status': evt['status']
|
||||||
.filter(lambda it: it.get('type') == 'http.response.start')
|
}
|
||||||
.if_present(maybe_log))
|
log.info(None, extra=dict(**{k : v for k, v in d.items() if k is not None}, **scope))
|
||||||
return result
|
def wrapped_send(*args, **kwargs):
|
||||||
await _server.handle_request(
|
result = send(*args, **kwargs)
|
||||||
ctx['method'],
|
(Maybe.of(args)
|
||||||
ctx['path'],
|
.filter(lambda it: len(it) > 0)
|
||||||
Maybe.of([header[1] for header in ctx['headers'] if header[0].decode().lower() == 'if-none-match'])
|
.map(lambda it: it[0])
|
||||||
.filter(lambda it: len(it) > 0)
|
.filter(lambda it: it.get('type') == 'http.response.start')
|
||||||
.map(lambda it: it[0])
|
.if_present(maybe_log))
|
||||||
.map(lambda it: it.decode())
|
return result
|
||||||
.or_else(None),
|
await _server.handle_request(
|
||||||
Maybe.of_nullable(ctx.get('query_string', None)).map(lambda it: it.decode()).or_else(None),
|
scope['method'],
|
||||||
wrapped_send
|
scope['path'],
|
||||||
)
|
Maybe.of([header[1] for header in scope['headers'] if header[0].decode().lower() == 'if-none-match'])
|
||||||
|
.filter(lambda it: len(it) > 0)
|
||||||
|
.map(lambda it: it[0])
|
||||||
|
.map(lambda it: it.decode())
|
||||||
|
.or_else(None),
|
||||||
|
Maybe.of_nullable(scope.get('query_string', None)).map(lambda it: it.decode()).or_else(None),
|
||||||
|
wrapped_send
|
||||||
|
)
|
||||||
|
|
||||||
|
26
src/bugis/asgi_utils.py
Normal file
26
src/bugis/asgi_utils.py
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
from typing import Tuple, Dict, Sequence
|
||||||
|
|
||||||
|
type StrOrStrings = (str, Sequence[str])
|
||||||
|
|
||||||
|
def decode_headers(headers: Sequence[Tuple[bytes, bytes]]) -> Dict[str, Sequence[str]]:
|
||||||
|
result = dict()
|
||||||
|
for key, value in headers:
|
||||||
|
if isinstance(key, bytes):
|
||||||
|
key = key.decode()
|
||||||
|
if isinstance(value, bytes):
|
||||||
|
value = value.decode()
|
||||||
|
l = result.setdefault(key, list())
|
||||||
|
l.append(value)
|
||||||
|
return {
|
||||||
|
k: tuple(v) for k, v in result.items()
|
||||||
|
}
|
||||||
|
|
||||||
|
def encode_headers(headers: Dict[str, StrOrStrings]) -> Tuple[Tuple[bytes, bytes], ...]:
|
||||||
|
result = []
|
||||||
|
for key, value in headers.items():
|
||||||
|
if isinstance(value, str):
|
||||||
|
result.append((key.encode(), value.encode()))
|
||||||
|
elif isinstance(value, Sequence):
|
||||||
|
for single_value in value:
|
||||||
|
result.append((key.encode(), single_value.encode()))
|
||||||
|
return tuple(result)
|
@@ -4,33 +4,38 @@ from watchdog.events import FileSystemEventHandler, FileSystemEvent, PatternMatc
|
|||||||
from watchdog.observers import Observer
|
from watchdog.observers import Observer
|
||||||
from watchdog.events import FileMovedEvent, FileClosedEvent, FileCreatedEvent, FileModifiedEvent
|
from watchdog.events import FileMovedEvent, FileClosedEvent, FileCreatedEvent, FileModifiedEvent
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from asyncio import Queue, AbstractEventLoop, Future, CancelledError, Task
|
from asyncio import Queue, AbstractEventLoop, Future, CancelledError, Task, gather
|
||||||
from typing import Callable
|
from typing import Callable, Optional
|
||||||
from logging import getLogger, Logger
|
from logging import getLogger, Logger
|
||||||
|
|
||||||
log: Logger = getLogger(__name__)
|
log: Logger = getLogger(__name__)
|
||||||
|
|
||||||
class Subscription:
|
class Subscription:
|
||||||
_unsubscribe_callback: Callable[['Subscription'], None]
|
_unsubscribe_callback: Callable[['Subscription'], None]
|
||||||
_event: Future
|
_event: Optional[Future]
|
||||||
_loop: AbstractEventLoop
|
_loop: AbstractEventLoop
|
||||||
|
|
||||||
def __init__(self, unsubscribe: Callable[['Subscription'], None], loop: AbstractEventLoop):
|
def __init__(self, unsubscribe: Callable[['Subscription'], None], loop: AbstractEventLoop):
|
||||||
self._unsubscribe_callback = unsubscribe
|
self._unsubscribe_callback = unsubscribe
|
||||||
self._event: Future = loop.create_future()
|
self._event: Optional[Future] = None
|
||||||
self._loop = loop
|
self._loop = loop
|
||||||
|
|
||||||
|
|
||||||
def unsubscribe(self) -> None:
|
def unsubscribe(self) -> None:
|
||||||
|
self._event.cancel()
|
||||||
self._unsubscribe_callback(self)
|
self._unsubscribe_callback(self)
|
||||||
log.debug('Deleted subscription %s', id(self))
|
log.debug('Deleted subscription %s', id(self))
|
||||||
|
|
||||||
async def wait(self, tout: float) -> bool:
|
async def wait(self, tout: float) -> bool:
|
||||||
handle = self._loop.call_later(tout, lambda: self._event.cancel())
|
self._event = self._loop.create_future()
|
||||||
|
|
||||||
|
def callback():
|
||||||
|
if not self._event.done():
|
||||||
|
self._event.set_result(False)
|
||||||
|
handle = self._loop.call_later(tout, callback)
|
||||||
try:
|
try:
|
||||||
log.debug('Subscription %s is waiting for an event', id(self))
|
log.debug('Subscription %s is waiting for an event', id(self))
|
||||||
await self._event
|
return await self._event
|
||||||
return True
|
|
||||||
except CancelledError:
|
except CancelledError:
|
||||||
return False
|
return False
|
||||||
finally:
|
finally:
|
||||||
@@ -38,7 +43,8 @@ class Subscription:
|
|||||||
|
|
||||||
def notify(self) -> None:
|
def notify(self) -> None:
|
||||||
log.debug('Subscription %s notified', id(self))
|
log.debug('Subscription %s notified', id(self))
|
||||||
self._event.set_result(None)
|
if not self._event.done():
|
||||||
|
self._event.set_result(True)
|
||||||
|
|
||||||
def reset(self) -> None:
|
def reset(self) -> None:
|
||||||
self._event = self._loop.create_future()
|
self._event = self._loop.create_future()
|
||||||
@@ -106,6 +112,7 @@ class SubscriptionManager:
|
|||||||
|
|
||||||
def unsubscribe_callback(subscription):
|
def unsubscribe_callback(subscription):
|
||||||
subscriptions_per_path.remove(subscription)
|
subscriptions_per_path.remove(subscription)
|
||||||
|
log.debug('Removed subscription %s to path %s', id(result), path)
|
||||||
|
|
||||||
result = Subscription(unsubscribe_callback, self._loop)
|
result = Subscription(unsubscribe_callback, self._loop)
|
||||||
log.debug('Created subscription %s to path %s', id(result), path)
|
log.debug('Created subscription %s to path %s', id(result), path)
|
||||||
@@ -116,21 +123,29 @@ class SubscriptionManager:
|
|||||||
subscriptions = self._subscriptions
|
subscriptions = self._subscriptions
|
||||||
subscriptions_per_path = subscriptions.get(path, None)
|
subscriptions_per_path = subscriptions.get(path, None)
|
||||||
if subscriptions_per_path:
|
if subscriptions_per_path:
|
||||||
|
log.debug(f"Subscriptions on '{path}': {len(subscriptions_per_path)}")
|
||||||
for s in subscriptions_per_path:
|
for s in subscriptions_per_path:
|
||||||
s.notify()
|
s.notify()
|
||||||
|
|
||||||
async def process_events(self):
|
async def process_events(self):
|
||||||
async for evt in AsyncQueueIterator(self._queue):
|
async for evt in AsyncQueueIterator(self._queue):
|
||||||
|
log.debug(f"Processed event for path '{evt}'")
|
||||||
self._notify_subscriptions(evt)
|
self._notify_subscriptions(evt)
|
||||||
|
log.debug(f"Event processor has completed")
|
||||||
|
|
||||||
|
|
||||||
def post_event(self, path):
|
def post_event(self, path):
|
||||||
self._loop.call_soon_threadsafe(self._queue.put_nowait, path)
|
def callback():
|
||||||
|
self._queue.put_nowait(path)
|
||||||
|
log.debug(f"Posted event for path '{path}', queue size: {self._queue.qsize()}")
|
||||||
|
self._loop.call_soon_threadsafe(callback)
|
||||||
|
|
||||||
|
|
||||||
class FileWatcher(PatternMatchingEventHandler):
|
class FileWatcher(PatternMatchingEventHandler):
|
||||||
_subscription_manager: SubscriptionManager
|
_subscription_manager: SubscriptionManager
|
||||||
_loop: AbstractEventLoop
|
_loop: AbstractEventLoop
|
||||||
_subscription_manager_loop: Task
|
_subscription_manager_loop: Task
|
||||||
|
_running_tasks : Future
|
||||||
|
|
||||||
def __init__(self, path):
|
def __init__(self, path):
|
||||||
super().__init__(patterns=['*.md'],
|
super().__init__(patterns=['*.md'],
|
||||||
@@ -141,8 +156,10 @@ class FileWatcher(PatternMatchingEventHandler):
|
|||||||
self._observer.schedule(self, path=path, recursive=True)
|
self._observer.schedule(self, path=path, recursive=True)
|
||||||
self._loop = asyncio.get_running_loop()
|
self._loop = asyncio.get_running_loop()
|
||||||
self._subscription_manager = SubscriptionManager(self._loop)
|
self._subscription_manager = SubscriptionManager(self._loop)
|
||||||
self._loop.run_in_executor(None, self._observer.start)
|
self._running_tasks = gather(
|
||||||
self._subscription_manager_loop = self._loop.create_task(self._subscription_manager.process_events())
|
self._loop.run_in_executor(None, self._observer.start),
|
||||||
|
self._loop.create_task(self._subscription_manager.process_events())
|
||||||
|
)
|
||||||
|
|
||||||
async def stop(self) -> None:
|
async def stop(self) -> None:
|
||||||
def _observer_stop():
|
def _observer_stop():
|
||||||
@@ -150,8 +167,8 @@ class FileWatcher(PatternMatchingEventHandler):
|
|||||||
self._observer.join()
|
self._observer.join()
|
||||||
self._subscription_manager.post_event(None)
|
self._subscription_manager.post_event(None)
|
||||||
|
|
||||||
self._loop.run_in_executor(None, _observer_stop)
|
await self._loop.run_in_executor(None, _observer_stop)
|
||||||
await self._subscription_manager_loop
|
await self._running_tasks
|
||||||
|
|
||||||
def subscribe(self, path: str) -> Subscription:
|
def subscribe(self, path: str) -> Subscription:
|
||||||
return self._subscription_manager.subscribe(path)
|
return self._subscription_manager.subscribe(path)
|
||||||
|
45
src/bugis/configuration.py
Normal file
45
src/bugis/configuration.py
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import os
|
||||||
|
from os import environ
|
||||||
|
from pathlib import Path
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
class ClassPropertyDescriptor(object):
|
||||||
|
|
||||||
|
def __init__(self, fget, fset=None):
|
||||||
|
self.fget = fget
|
||||||
|
self.fset = fset
|
||||||
|
|
||||||
|
def __get__(self, obj, klass=None):
|
||||||
|
if klass is None:
|
||||||
|
klass = type(obj)
|
||||||
|
return self.fget.__get__(obj, klass)()
|
||||||
|
|
||||||
|
def __set__(self, obj, value):
|
||||||
|
if not self.fset:
|
||||||
|
raise AttributeError("can't set attribute")
|
||||||
|
type_ = type(obj)
|
||||||
|
return self.fset.__get__(obj, type_)(value)
|
||||||
|
|
||||||
|
def setter(self, func):
|
||||||
|
if not isinstance(func, (classmethod, staticmethod)):
|
||||||
|
func = classmethod(func)
|
||||||
|
self.fset = func
|
||||||
|
return self
|
||||||
|
|
||||||
|
def classproperty(func):
|
||||||
|
if not isinstance(func, (classmethod, staticmethod)):
|
||||||
|
func = classmethod(func)
|
||||||
|
return ClassPropertyDescriptor(func)
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class Configuration:
|
||||||
|
logging_configuration_file : str = environ.get("LOGGING_CONFIGURATION_FILE", Path(__file__).parent / 'default-conf' / 'logging.yaml')
|
||||||
|
plant_uml_server_address : str = environ.get('PLANT_UML_SERVER_ADDRESS', None)
|
||||||
|
|
||||||
|
|
||||||
|
@classproperty
|
||||||
|
def instance(cls) -> 'Configuration':
|
||||||
|
return Configuration()
|
||||||
|
|
||||||
|
|
||||||
|
|
17
src/bugis/plantuml.py
Normal file
17
src/bugis/plantuml.py
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
from aiofiles import open as async_open
|
||||||
|
from aiohttp import ClientSession
|
||||||
|
from .configuration import Configuration
|
||||||
|
from yarl import URL
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from _typeshed import StrOrBytesPath
|
||||||
|
|
||||||
|
async def render_plant_uml(path: 'StrOrBytesPath') -> bytes:
|
||||||
|
async with ClientSession() as session:
|
||||||
|
url = URL(Configuration.instance.plant_uml_server_address) / 'svg'
|
||||||
|
async with async_open(path, 'rb') as file:
|
||||||
|
source = await file.read()
|
||||||
|
async with session.post(url, data=source) as response:
|
||||||
|
response.raise_for_status()
|
||||||
|
return await response.read()
|
@@ -1,25 +1,26 @@
|
|||||||
import logging
|
|
||||||
from os import getcwd
|
|
||||||
from mimetypes import init as mimeinit, guess_type
|
|
||||||
import hashlib
|
import hashlib
|
||||||
|
import logging
|
||||||
from os.path import join, normpath, splitext, relpath, basename
|
from asyncio import AbstractEventLoop
|
||||||
from asyncio import Future
|
from asyncio import Future
|
||||||
from aiofiles.os import listdir
|
from io import BytesIO
|
||||||
from aiofiles.ospath import exists, isdir, isfile, getmtime
|
from mimetypes import init as mimeinit, guess_type
|
||||||
|
from os import getcwd
|
||||||
|
from os.path import join, normpath, splitext, relpath, basename
|
||||||
|
from typing import Callable, TYPE_CHECKING, Optional, Awaitable, AsyncGenerator, Any
|
||||||
|
|
||||||
|
import pygraphviz as pgv
|
||||||
from aiofiles import open as async_open
|
from aiofiles import open as async_open
|
||||||
from aiofiles.base import AiofilesContextManager
|
from aiofiles.base import AiofilesContextManager
|
||||||
|
from aiofiles.os import listdir
|
||||||
|
from aiofiles.ospath import exists, isdir, isfile, getmtime
|
||||||
from aiofiles.threadpool.binary import AsyncBufferedReader
|
from aiofiles.threadpool.binary import AsyncBufferedReader
|
||||||
from asyncio import AbstractEventLoop
|
|
||||||
|
|
||||||
from .md2html import compile_html, load_from_cache, STATIC_RESOURCES, MARDOWN_EXTENSIONS
|
|
||||||
from shutil import which
|
|
||||||
import pygraphviz as pgv
|
|
||||||
from io import BytesIO
|
|
||||||
from typing import Callable, TYPE_CHECKING, Optional, Awaitable, AsyncGenerator, Any
|
|
||||||
from .async_watchdog import FileWatcher
|
|
||||||
from pwo import Maybe
|
from pwo import Maybe
|
||||||
|
|
||||||
|
from .asgi_utils import encode_headers
|
||||||
|
from .async_watchdog import FileWatcher
|
||||||
|
from .md2html import compile_html, load_from_cache, STATIC_RESOURCES, MARDOWN_EXTENSIONS
|
||||||
|
from .plantuml import render_plant_uml
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from _typeshed import StrOrBytesPath
|
from _typeshed import StrOrBytesPath
|
||||||
|
|
||||||
@@ -45,6 +46,9 @@ def is_markdown(filepath):
|
|||||||
def is_dotfile(filepath):
|
def is_dotfile(filepath):
|
||||||
return has_extension(filepath, ".dot")
|
return has_extension(filepath, ".dot")
|
||||||
|
|
||||||
|
def is_plant_uml(filepath):
|
||||||
|
return has_extension(filepath, ".puml")
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
class Server:
|
class Server:
|
||||||
@@ -91,11 +95,11 @@ class Server:
|
|||||||
await send({
|
await send({
|
||||||
'type': 'http.response.start',
|
'type': 'http.response.start',
|
||||||
'status': 200,
|
'status': 200,
|
||||||
'headers': [
|
'headers': encode_headers({
|
||||||
('content-type', f'{mime_type}; charset=UTF-8'),
|
'content-type': f'{mime_type}; charset=UTF-8',
|
||||||
('etag', f'W/"{digest}"'.encode()),
|
'etag': f'W/{digest}',
|
||||||
('Cache-Control', 'must-revalidate, max-age=86400'),
|
'Cache-Control': 'must-revalidate, max-age=86400',
|
||||||
]
|
})
|
||||||
})
|
})
|
||||||
await send({
|
await send({
|
||||||
'type': 'http.response.body',
|
'type': 'http.response.body',
|
||||||
@@ -124,7 +128,7 @@ class Server:
|
|||||||
lambda: getmtime(path)
|
lambda: getmtime(path)
|
||||||
)
|
)
|
||||||
if etag != digest:
|
if etag != digest:
|
||||||
if exists(path) and await isfile(path):
|
if await exists(path) and await isfile(path):
|
||||||
await self.render_markdown(url_path, path, True, digest, send)
|
await self.render_markdown(url_path, path, True, digest, send)
|
||||||
return
|
return
|
||||||
else:
|
else:
|
||||||
@@ -136,7 +140,7 @@ class Server:
|
|||||||
elif is_markdown(path):
|
elif is_markdown(path):
|
||||||
raw = query_string == 'reload'
|
raw = query_string == 'reload'
|
||||||
await self.render_markdown(url_path, path, raw, digest, send)
|
await self.render_markdown(url_path, path, raw, digest, send)
|
||||||
elif is_dotfile(path) and which("dot"):
|
elif is_dotfile(path):
|
||||||
def render_graphviz(filepath: StrOrBytesPath) -> bytes:
|
def render_graphviz(filepath: StrOrBytesPath) -> bytes:
|
||||||
logger.debug("Starting Graphviz rendering for file '%s'", filepath)
|
logger.debug("Starting Graphviz rendering for file '%s'", filepath)
|
||||||
graph = pgv.AGraph(filepath)
|
graph = pgv.AGraph(filepath)
|
||||||
@@ -147,11 +151,28 @@ class Server:
|
|||||||
await send({
|
await send({
|
||||||
'type': 'http.response.start',
|
'type': 'http.response.start',
|
||||||
'status': 200,
|
'status': 200,
|
||||||
'headers': (
|
'headers': encode_headers({
|
||||||
('Content-Type', 'image/svg+xml; charset=UTF-8'),
|
'Content-Type': 'image/svg+xml; charset=UTF-8',
|
||||||
('Etag', f'W/"{digest}"'),
|
'Etag': f'W/{digest}',
|
||||||
('Cache-Control', 'no-cache'),
|
'Cache-Control': 'no-cache',
|
||||||
)
|
})
|
||||||
|
})
|
||||||
|
await send({
|
||||||
|
'type': 'http.response.body',
|
||||||
|
'body': body
|
||||||
|
})
|
||||||
|
elif is_plant_uml(path):
|
||||||
|
logger.debug("Starting PlantUML rendering for file '%s'", path)
|
||||||
|
body = await render_plant_uml(path)
|
||||||
|
logger.debug("Completed PlantUML rendering for file '%s'", path)
|
||||||
|
await send({
|
||||||
|
'type': 'http.response.start',
|
||||||
|
'status': 200,
|
||||||
|
'headers': encode_headers({
|
||||||
|
'Content-Type': 'image/svg+xml; charset=UTF-8',
|
||||||
|
'Etag': f'W/{digest}',
|
||||||
|
'Cache-Control': 'no-cache'
|
||||||
|
})
|
||||||
})
|
})
|
||||||
await send({
|
await send({
|
||||||
'type': 'http.response.body',
|
'type': 'http.response.body',
|
||||||
@@ -170,11 +191,11 @@ class Server:
|
|||||||
await send({
|
await send({
|
||||||
'type': 'http.response.start',
|
'type': 'http.response.start',
|
||||||
'status': 200,
|
'status': 200,
|
||||||
'headers': (
|
'headers': encode_headers({
|
||||||
('Content-Type', guess_type(basename(path))[0] or 'application/octet-stream'),
|
'Content-Type': guess_type(basename(path))[0] or 'application/octet-stream',
|
||||||
('Etag', f'W/"{digest}"'),
|
'Etag': f'W/{digest}',
|
||||||
('Cache-Control', 'no-cache')
|
'Cache-Control': 'no-cache'
|
||||||
)
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
async for chunk in read_file(path):
|
async for chunk in read_file(path):
|
||||||
@@ -194,9 +215,9 @@ class Server:
|
|||||||
await send({
|
await send({
|
||||||
'type': 'http.response.start',
|
'type': 'http.response.start',
|
||||||
'status': 200,
|
'status': 200,
|
||||||
'headers': (
|
'headers': encode_headers({
|
||||||
('Content-Type', 'text/html; charset=UTF-8'),
|
'Content-Type': 'text/html; charset=UTF-8',
|
||||||
)
|
})
|
||||||
})
|
})
|
||||||
await send({
|
await send({
|
||||||
'type': 'http.response.body',
|
'type': 'http.response.body',
|
||||||
@@ -288,11 +309,11 @@ class Server:
|
|||||||
await send({
|
await send({
|
||||||
'type': 'http.response.start',
|
'type': 'http.response.start',
|
||||||
'status': 200,
|
'status': 200,
|
||||||
'headers': (
|
'headers': encode_headers({
|
||||||
('Content-Type', 'text/html; charset=UTF-8'),
|
'Content-Type': 'text/html; charset=UTF-8',
|
||||||
('Etag', f'W/{digest}'),
|
'Etag': f'W/{digest}',
|
||||||
('Cache-Control', 'no-cache'),
|
'Cache-Control': 'no-cache',
|
||||||
)
|
})
|
||||||
})
|
})
|
||||||
await send({
|
await send({
|
||||||
'type': 'http.response.body',
|
'type': 'http.response.body',
|
||||||
@@ -300,14 +321,14 @@ class Server:
|
|||||||
})
|
})
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def not_modified(send, digest: str, cache_control=('Cache-Control', 'no-cache')) -> []:
|
async def not_modified(send, digest: str, cache_control: str ='no-cache') -> []:
|
||||||
await send({
|
await send({
|
||||||
'type': 'http.response.start',
|
'type': 'http.response.start',
|
||||||
'status': 304,
|
'status': 304,
|
||||||
'headers': (
|
'headers': encode_headers({
|
||||||
(b'Etag', f'W/{digest}'.encode()),
|
'Etag': f'W/{digest}',
|
||||||
cache_control
|
'Cache-Control': cache_control
|
||||||
)
|
})
|
||||||
})
|
})
|
||||||
await send({
|
await send({
|
||||||
'type': 'http.response.body',
|
'type': 'http.response.body',
|
||||||
@@ -350,3 +371,6 @@ class Server:
|
|||||||
async for entry in await ls(file_filter):
|
async for entry in await ls(file_filter):
|
||||||
result += '<li><a href="' + entry + '"/>' + entry + '</li>'
|
result += '<li><a href="' + entry + '"/>' + entry + '</li>'
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
async def stop(self):
|
||||||
|
await self.file_watcher.stop()
|
@@ -1,21 +1,21 @@
|
|||||||
function req(first) {
|
|
||||||
|
function req(first, previousETag) {
|
||||||
|
const minInterval = 2000;
|
||||||
const start = new Date().getTime();
|
const start = new Date().getTime();
|
||||||
const xmlhttp = new XMLHttpRequest();
|
const xmlhttp = new XMLHttpRequest();
|
||||||
xmlhttp.onload = function() {
|
xmlhttp.onload = function() {
|
||||||
if (xmlhttp.status == 200) {
|
const eTag = xmlhttp.getResponseHeader("Etag")
|
||||||
|
if (xmlhttp.status == 200 && eTag !== previousETag) {
|
||||||
document.querySelector("article.markdown-body").innerHTML = xmlhttp.responseText;
|
document.querySelector("article.markdown-body").innerHTML = xmlhttp.responseText;
|
||||||
} else if(xmlhttp.status == 304) {
|
|
||||||
} else {
|
|
||||||
console.log(xmlhttp.status, xmlhttp.statusText);
|
|
||||||
}
|
}
|
||||||
const nextCall = Math.min(1000, Math.max(0, 1000 - (new Date().getTime() - start)));
|
const nextCall = Math.min(minInterval, Math.max(0, minInterval - (new Date().getTime() - start)));
|
||||||
setTimeout(req, nextCall, false);
|
setTimeout(req, nextCall, false, eTag);
|
||||||
};
|
};
|
||||||
xmlhttp.onerror = function() {
|
xmlhttp.onerror = function() {
|
||||||
console.log(xmlhttp.status, xmlhttp.statusText);
|
console.log(xmlhttp.status, xmlhttp.statusText);
|
||||||
setTimeout(req, 1000, false);
|
setTimeout(req, minInterval, false, previousETag);
|
||||||
};
|
};
|
||||||
xmlhttp.open("GET", location.pathname + "?reload", true);
|
xmlhttp.open("GET", location.pathname + "?reload", true);
|
||||||
xmlhttp.send();
|
xmlhttp.send();
|
||||||
}
|
}
|
||||||
req(true);
|
req(true, null);
|
||||||
|
Reference in New Issue
Block a user