Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP: Websockets #7724

Draft
wants to merge 12 commits into
base: master
Choose a base branch
from
2 changes: 1 addition & 1 deletion Procfile
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Web process: gunicorn
web: env/bin/gunicorn --chdir $APP_HOME/src/backend/InvenTree -c src/backend/InvenTree/gunicorn.conf.py InvenTree.wsgi -b 0.0.0.0:$PORT
web: env/bin/gunicorn --chdir $APP_HOME/src/backend/InvenTree -c src/backend/InvenTree/gunicorn.conf.py InvenTree.asgi -b 0.0.0.0:$PORT -k uvicorn_worker.UvicornWorker
# Worker process: qcluster
worker: env/bin/python src/backend/InvenTree/manage.py qcluster
# Invoke commands
Expand Down
2 changes: 1 addition & 1 deletion contrib/container/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ COPY src/backend/requirements.txt ${INVENTREE_BACKEND_DIR}/requirements.txt
COPY --from=frontend ${INVENTREE_BACKEND_DIR}/InvenTree/web/static/web ${INVENTREE_BACKEND_DIR}/InvenTree/web/static/web

# Launch the production server
CMD gunicorn -c ./gunicorn.conf.py InvenTree.wsgi -b 0.0.0.0:8000 --chdir ${INVENTREE_BACKEND_DIR}/InvenTree
CMD gunicorn -c ./gunicorn.conf.py InvenTree.asgi -b 0.0.0.0:8000 --chdir ${INVENTREE_BACKEND_DIR}/InvenTree -k uvicorn_worker.UvicornWorker

FROM inventree_base AS dev

Expand Down
1 change: 1 addition & 0 deletions contrib/container/requirements.in
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ mariadb>=1.1.8

# gunicorn web server
gunicorn>=22.0.0
uvicorn-worker # ASGI worker for gunicorn

# LDAP required packages
django-auth-ldap # Django integration for ldap auth
Expand Down
20 changes: 19 additions & 1 deletion contrib/container/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ asgiref==3.8.1 \
--hash=sha256:3e1e3ecc849832fe52ccf2cb6686b7a55f82bb1d6aee72a58826471390335e47 \
--hash=sha256:c343bd80a0bec947a9860adb4c432ffa7db769836c64238fc34bdc3fec84d590
# via django
click==8.1.7 \
--hash=sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28 \
--hash=sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de
# via uvicorn
django==4.2.15 \
--hash=sha256:61ee4a130efb8c451ef3467c67ca99fdce400fedd768634efc86a68c18d80d30 \
--hash=sha256:c77f926b81129493961e19c0e02188f8d07c112a1162df69bfab178ae447f94a
Expand All @@ -17,7 +21,13 @@ django-auth-ldap==4.8.0 \
gunicorn==23.0.0 \
--hash=sha256:ec400d38950de4dfd418cff8328b2c8faed0edb0d517d3394e457c317908ca4d \
--hash=sha256:f014447a0101dc57e294f6c18ca6b40227a4c90e9bdb586042628030cba004ec
# via -r contrib/container/requirements.in
# via
# -r contrib/container/requirements.in
# uvicorn-worker
h11==0.14.0 \
--hash=sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d \
--hash=sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761
# via uvicorn
invoke==2.2.0 \
--hash=sha256:6ea924cc53d4f78e3d98bc436b08069a03077e6f85ad1ddaa8a116d7dad15820 \
--hash=sha256:ee6cbb101af1a859c7fe84f2a264c059020b0cb7fe3535f9424300ab568f6bd5
Expand Down Expand Up @@ -219,6 +229,14 @@ uv==0.3.0 \
--hash=sha256:d3da56b87ec5aa4f2ae572127c754655bad3820dd41a4d37ed4d5e2f67035990 \
--hash=sha256:d87ff76da5128036c05db0291db7510a85cb8efb86538e8f49adc8074bb292f0
# via -r contrib/container/requirements.in
uvicorn==0.30.6 \
--hash=sha256:4b15decdda1e72be08209e860a1e10e92439ad5b97cf44cc945fcbee66fc5788 \
--hash=sha256:65fd46fe3fda5bdc1b03b94eb634923ff18cd35b2f084813ea79d1f103f711b5
# via uvicorn-worker
uvicorn-worker==0.2.0 \
--hash=sha256:65dcef25ab80a62e0919640f9582216ee05b3bb1dc2f0e58b354ca0511c398fb \
--hash=sha256:f6894544391796be6eeed37d48cae9d7739e5a105f7e37061eccef2eac5a0295
# via -r contrib/container/requirements.in
wheel==0.44.0 \
--hash=sha256:2376a90c98cc337d18623527a97c31797bd02bad0033d41547043a1cbfbe448f \
--hash=sha256:a29c3f2817e95ab89aa4660681ad547c0e9547f20e75b0562fe7723c9a2a9d49
Expand Down
2 changes: 1 addition & 1 deletion contrib/deploy/supervisord.conf
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ port = 127.0.0.1:9001
[program:inventree-server]
user=inventree
directory=/home/inventree/src/InvenTree
command=/home/inventree/env/bin/gunicorn -c gunicorn.conf.py InvenTree.wsgi
command=/home/inventree/env/bin/gunicorn -c gunicorn.conf.py InvenTree.asgi -k uvicorn_worker.UvicornWorker
startsecs=10
autostart=true
autorestart=true
Expand Down
28 changes: 28 additions & 0 deletions src/backend/InvenTree/InvenTree/asgi.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
"""ASGI config for InvenTree project.

It exposes the ASGI callable as a module-level variable named ``application``.

For more information on this file, see
https://docs.djangoproject.com/en/4.1/howto/deployment/asgi/
"""

import os

from django.core.asgi import get_asgi_application

from channels.auth import AuthMiddlewareStack
from channels.routing import ProtocolTypeRouter, URLRouter
from channels.security.websocket import AllowedHostsOriginValidator

os.environ.setdefault(
'DJANGO_SETTINGS_MODULE', 'InvenTree.settings'
) # pragma: no cover

from InvenTree import routing

application = ProtocolTypeRouter({
'http': get_asgi_application(),
'websocket': AllowedHostsOriginValidator(
AuthMiddlewareStack(URLRouter(routing.websocket_urlpatterns))
),
})
11 changes: 11 additions & 0 deletions src/backend/InvenTree/InvenTree/routing.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
"""Async routings for InvenTree."""

from django.urls import path

from channels.routing import URLRouter

from web.consumers import InvenTreeConsumer

websocket_urlpatterns = [
path('ws/', URLRouter([path('page/<url_name>/', InvenTreeConsumer.as_asgi())]))
]
21 changes: 20 additions & 1 deletion src/backend/InvenTree/InvenTree/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,12 @@
import pytz
from dotenv import load_dotenv

from InvenTree.cache import get_cache_config, is_global_cache_enabled
from InvenTree.cache import (
cache_host,
cache_port,
get_cache_config,
is_global_cache_enabled,
)
from InvenTree.config import get_boolean_setting, get_custom_file, get_setting
from InvenTree.ready import isInMainThread
from InvenTree.sentry import default_sentry_dsn, init_sentry
Expand Down Expand Up @@ -184,6 +189,7 @@
)

INSTALLED_APPS = [
'daphne', # ASGI enabled dev server
# Admin site integration
'django.contrib.admin',
# InvenTree apps
Expand Down Expand Up @@ -239,6 +245,7 @@
'dj_rest_auth.registration', # Registration APIs - dj-rest-auth'
'drf_spectacular', # API documentation
'django_ical', # For exporting calendars
'channels', # websockets
]

MIDDLEWARE = CONFIG.get(
Expand Down Expand Up @@ -516,6 +523,7 @@

# WSGI default setting
WSGI_APPLICATION = 'InvenTree.wsgi.application'
ASGI_APPLICATION = 'InvenTree.asgi.application'

"""
Configure the database backend based on the user-specified values.
Expand Down Expand Up @@ -842,6 +850,17 @@
# as well
Q_CLUSTER['django_redis'] = 'worker'

# Settings for channels
if GLOBAL_CACHE_ENABLED: # pragma: no cover
CHANNEL_LAYERS = {
'default': {
'BACKEND': 'channels_redis.core.RedisChannelLayer',
'CONFIG': {'hosts': [cache_host(), cache_port()]},
}
}
else:
CHANNEL_LAYERS = {'default': {'BACKEND': 'channels.layers.InMemoryChannelLayer'}}

# database user sessions
SESSION_ENGINE = 'user_sessions.backends.db'
LOGOUT_REDIRECT_URL = get_setting(
Expand Down
82 changes: 82 additions & 0 deletions src/backend/InvenTree/web/consumers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
"""Websocket consumers for web app."""

import json

from asgiref.sync import async_to_sync
from channels.generic.websocket import JsonWebsocketConsumer


class InvenTreeConsumer(JsonWebsocketConsumer):
"""This consumer is used to enable page attendance widgets."""

def __init__(self, *args, **kwargs):
"""Set up context."""
super().__init__(args, kwargs)
self.user = None

def connect(self):
"""Join a user to a page."""
user = self.scope.get('user')
if not user or not user.is_authenticated:
self.close()
return
self.user = user
self.accept()

from django.contrib.auth.models import User

from InvenTree.serializers import UserSerializer

self.room_name = self.scope['url_route']['kwargs']['url_name']
self.room_group_name = f'chat_{self.room_name}'
# Join room group
async_to_sync(self.channel_layer.group_add)(
self.room_group_name, self.channel_name
)

# Announce all users
instances = User.objects.all()
self.send_json({
'type': 'users',
'users': UserSerializer(instances, many=True).data,
})

def disconnect(self, code):
"""Remove user from a page."""
async_to_sync(self.channel_layer.group_discard)(
self.room_group_name, self.channel_name
)
return super().disconnect(code)

def receive_json(self, content, **kwargs):
"""Handler for processing incoming messages."""
message_type = content.get('type', None)
message = content.get('message', None)

async_to_sync(self.channel_layer.group_send)(
self.room_group_name,
{
'type': 'chat_message',
'message': message,
'user': self.user.get_username(),
},
)

if message_type == '#TODO':
# TODO: implement this
...
return super().receive_json(content, **kwargs)

# Receive message from room group
def chat_message(self, event):
"""Send message to WebSocket."""
message = event['message']

# Send message to WebSocket
self.send(
text_data=json.dumps({
'type': 'chat_message',
'message': message,
'user': event['user'],
})
)
4 changes: 3 additions & 1 deletion src/backend/requirements-dev.txt
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,9 @@ charset-normalizer==3.3.2 \
click==8.1.7 \
--hash=sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28 \
--hash=sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de
# via pip-tools
# via
# -c src/backend/requirements.txt
# pip-tools
coverage[toml]==7.6.1 \
--hash=sha256:06a737c882bd26d0d6ee7269b20b12f14a8704807a01056c80bb881a4b2ce6ca \
--hash=sha256:07e2ca0ad381b91350c0ed49d52699b625aab2b44b65e1b4e02fa9df0e92ad2d \
Expand Down
5 changes: 4 additions & 1 deletion src/backend/requirements.in
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
# Please keep this list sorted - if you pin a version provide a reason
Django<5.0 # Django package
channels[daphne] # Websockers
channels_redis # Websockets with Redis
coreapi # API documentation for djangorestframework
cryptography>=40.0.0,!=40.0.2 # Core cryptographic functionality
django-allauth[openid,saml] # SSO for external providers via OpenID
django-allauth[openid,saml] # SSO for external providers via OpenID
django-allauth-2fa # MFA / 2FA
django-cleanup # Automated deletion of old / unused uploaded files
django-cors-headers # CORS headers extension for DRF
Expand Down Expand Up @@ -51,6 +53,7 @@ regex # Advanced regular expressions
sentry-sdk # Error reporting (optional)
setuptools # Standard dependency
tablib[xls,xlsx,yaml] # Support for XLS and XLSX formats
uvicorn-worker # ASGI worker for gunicorn
weasyprint # PDF generation
whitenoise # Enhanced static file serving

Expand Down
Loading
Loading