mirror of
https://github.com/ets-cfuhrman-pfe/EvalueTonSavoir.git
synced 2025-08-11 21:23:54 -04:00
Merge branch 'stress-test-socket' into dev-it3-it4-PFEA2024
This commit is contained in:
commit
e7eede36be
35 changed files with 2957 additions and 62 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -131,3 +131,5 @@ dist
|
|||
db-backup/
|
||||
|
||||
.venv
|
||||
deployments
|
||||
/test/stressTest/output
|
||||
|
|
|
|||
14
.vscode/launch.json
vendored
14
.vscode/launch.json
vendored
|
|
@ -20,6 +20,20 @@
|
|||
"name": "Debug frontend",
|
||||
"url": "http://localhost:5173",
|
||||
"webRoot": "${workspaceFolder}/client/"
|
||||
},
|
||||
{
|
||||
"name": "Docker: Attach to Node",
|
||||
"type": "node",
|
||||
"request": "attach",
|
||||
"restart": true,
|
||||
"port": 9229,
|
||||
"address": "localhost",
|
||||
"localRoot": "${workspaceFolder}",
|
||||
"remoteRoot": "/app",
|
||||
"protocol": "inspector",
|
||||
"skipFiles": [
|
||||
"<node_internals>/**"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -12,6 +12,10 @@ RUN npm install
|
|||
|
||||
RUN npm run build
|
||||
|
||||
EXPOSE 5173
|
||||
ENV PORT=5173
|
||||
EXPOSE ${PORT}
|
||||
|
||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=30s --retries=3 \
|
||||
CMD curl -f http://localhost:${PORT} || exit 1
|
||||
|
||||
CMD [ "npm", "run", "preview" ]
|
||||
74
create-branch-image.bat
Normal file
74
create-branch-image.bat
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
@echo off
|
||||
setlocal EnableDelayedExpansion
|
||||
|
||||
:: Check if gh is installed
|
||||
where gh >nul 2>&1
|
||||
if %errorlevel% neq 0 (
|
||||
echo GitHub CLI not found. Installing...
|
||||
winget install --id GitHub.cli
|
||||
if %errorlevel% neq 0 (
|
||||
echo Failed to install GitHub CLI. Exiting...
|
||||
exit /b 1
|
||||
)
|
||||
echo GitHub CLI installed successfully.
|
||||
)
|
||||
|
||||
:: Check if user is authenticated
|
||||
gh auth status >nul 2>&1
|
||||
if %errorlevel% neq 0 (
|
||||
echo GitHub CLI not authenticated. Please authenticate...
|
||||
gh auth login
|
||||
if %errorlevel% neq 0 (
|
||||
echo Failed to authenticate. Exiting...
|
||||
exit /b 1
|
||||
)
|
||||
echo Authentication successful.
|
||||
)
|
||||
|
||||
:: Get the current branch name
|
||||
for /f "tokens=*" %%i in ('git rev-parse --abbrev-ref HEAD') do set BRANCH_NAME=%%i
|
||||
|
||||
:: Run the GitHub workflow with the current branch name
|
||||
echo Running GitHub workflow with branch %BRANCH_NAME%...
|
||||
gh workflow run 119194149 --ref %BRANCH_NAME%
|
||||
|
||||
:: Wait and validate workflow launch
|
||||
set /a attempts=0
|
||||
set /a max_attempts=12
|
||||
echo Waiting for workflow to start...
|
||||
|
||||
:wait_for_workflow
|
||||
timeout /t 15 >nul
|
||||
set /a attempts+=1
|
||||
|
||||
:: Get recent workflow run matching our criteria with in_progress status
|
||||
for /f "tokens=*" %%i in ('gh run list --branch %BRANCH_NAME% --status in_progress --limit 1 --json databaseId --jq ".[0].databaseId"') do set WORKFLOW_RUN_ID=%%i
|
||||
|
||||
if "%WORKFLOW_RUN_ID%"=="" (
|
||||
if !attempts! lss !max_attempts! (
|
||||
echo Attempt !attempts! of !max_attempts!: No running workflow found yet...
|
||||
goto wait_for_workflow
|
||||
) else (
|
||||
echo Timeout waiting for workflow to start running.
|
||||
exit /b 1
|
||||
)
|
||||
)
|
||||
|
||||
echo Found running workflow ID: %WORKFLOW_RUN_ID%
|
||||
|
||||
:monitor_progress
|
||||
cls
|
||||
echo Workflow Progress:
|
||||
echo ----------------
|
||||
gh run view %WORKFLOW_RUN_ID% --json jobs --jq ".jobs[] | \"Job: \" + .name + \" - Status: \" + .status + if .conclusion != null then \" (\" + .conclusion + \")\" else \"\" end"
|
||||
echo.
|
||||
|
||||
:: Check if workflow is still running
|
||||
for /f "tokens=*" %%i in ('gh run view %WORKFLOW_RUN_ID% --json status --jq ".status"') do set CURRENT_STATUS=%%i
|
||||
if "%CURRENT_STATUS%" == "completed" (
|
||||
echo Workflow completed.
|
||||
exit /b 0
|
||||
)
|
||||
|
||||
timeout /t 5 >nul
|
||||
goto monitor_progress
|
||||
|
|
@ -3,21 +3,29 @@ version: '3'
|
|||
services:
|
||||
|
||||
frontend:
|
||||
container_name: frontend
|
||||
build:
|
||||
context: ./client
|
||||
dockerfile: Dockerfile
|
||||
container_name: frontend
|
||||
ports:
|
||||
- "5173:5173"
|
||||
networks:
|
||||
- quiz_network
|
||||
restart: always
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "curl -f http://localhost:$${PORT} || exit 1"]
|
||||
interval: 5s
|
||||
timeout: 10s
|
||||
start_period: 5s
|
||||
retries: 6
|
||||
|
||||
backend:
|
||||
build:
|
||||
context: ./server
|
||||
dockerfile: Dockerfile
|
||||
container_name: backend
|
||||
networks:
|
||||
- quiz_network
|
||||
ports:
|
||||
- "3000:3000"
|
||||
volumes:
|
||||
|
|
@ -30,12 +38,15 @@ services:
|
|||
SENDER_EMAIL: infoevaluetonsavoir@gmail.com
|
||||
EMAIL_PSW: 'vvml wmfr dkzb vjzb'
|
||||
JWT_SECRET: haQdgd2jp09qb897GeBZyJetC8ECSpbFJe
|
||||
FRONTEND_URL: "http://localhost:5173"
|
||||
depends_on:
|
||||
- mongo
|
||||
networks:
|
||||
- quiz_network
|
||||
restart: always
|
||||
mongo:
|
||||
condition: service_healthy
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "curl -f http://localhost:$${PORT}/health || exit 1"]
|
||||
interval: 5s
|
||||
timeout: 10s
|
||||
start_period: 5s
|
||||
retries: 6
|
||||
|
||||
quizroom: # Forces image to update
|
||||
build:
|
||||
|
|
@ -44,11 +55,17 @@ services:
|
|||
container_name: quizroom
|
||||
ports:
|
||||
- "4500:4500"
|
||||
depends_on:
|
||||
- backend
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
networks:
|
||||
- quiz_network
|
||||
restart: always
|
||||
healthcheck:
|
||||
test: ["CMD", "/usr/src/app/healthcheck.sh"]
|
||||
interval: 5s
|
||||
timeout: 10s
|
||||
start_period: 5s
|
||||
retries: 6
|
||||
|
||||
nginx:
|
||||
build:
|
||||
|
|
@ -58,11 +75,25 @@ services:
|
|||
ports:
|
||||
- "80:80"
|
||||
depends_on:
|
||||
- backend
|
||||
- frontend
|
||||
frontend:
|
||||
condition: service_healthy
|
||||
backend:
|
||||
condition: service_healthy
|
||||
networks:
|
||||
- quiz_network
|
||||
restart: always
|
||||
#environment:
|
||||
# - PORT=8000
|
||||
# - FRONTEND_HOST=frontend
|
||||
# - FRONTEND_PORT=5173
|
||||
# - BACKEND_HOST=backend
|
||||
# - BACKEND_PORT=3000
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "wget --spider http://0.0.0.0:$${PORT}/health || exit 1"]
|
||||
interval: 5s
|
||||
timeout: 10s
|
||||
start_period: 5s
|
||||
retries: 6
|
||||
|
||||
mongo:
|
||||
image: mongo
|
||||
|
|
@ -75,6 +106,12 @@ services:
|
|||
networks:
|
||||
- quiz_network
|
||||
restart: always
|
||||
healthcheck:
|
||||
test: ["CMD", "mongosh", "--eval", "db.adminCommand('ping')"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
start_period: 20s
|
||||
|
||||
watchtower:
|
||||
image: containrrr/watchtower
|
||||
|
|
|
|||
5
nginx/.env.example
Normal file
5
nginx/.env.example
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
PORT=80
|
||||
FRONTEND_HOST=frontend
|
||||
FRONTEND_PORT=5173
|
||||
BACKEND_HOST=backend
|
||||
BACKEND_PORT=3000
|
||||
|
|
@ -1,20 +1,17 @@
|
|||
# Stage 1: Build stage
|
||||
FROM nginx:1.27-alpine AS builder
|
||||
|
||||
# Install required packages
|
||||
RUN apk add --no-cache nginx-mod-http-js nginx-mod-http-keyval
|
||||
|
||||
# Stage 2: Final stage
|
||||
FROM alpine:3.19
|
||||
|
||||
# Copy Nginx and NJS modules from builder
|
||||
COPY --from=builder /usr/sbin/nginx /usr/sbin/
|
||||
COPY --from=builder /usr/lib/nginx/modules/ /usr/lib/nginx/modules/
|
||||
COPY --from=builder /etc/nginx/ /etc/nginx/
|
||||
COPY --from=builder /usr/lib/nginx/ /usr/lib/nginx/
|
||||
|
||||
# Install required runtime dependencies
|
||||
# Install gettext for envsubst and other dependencies
|
||||
RUN apk add --no-cache \
|
||||
gettext \
|
||||
curl \
|
||||
nginx-mod-http-js \
|
||||
nginx-mod-http-keyval \
|
||||
pcre2 \
|
||||
ca-certificates \
|
||||
pcre \
|
||||
|
|
@ -24,15 +21,30 @@ RUN apk add --no-cache \
|
|||
libxml2 \
|
||||
libedit \
|
||||
geoip \
|
||||
libxslt \
|
||||
&& mkdir -p /var/cache/nginx \
|
||||
libxslt
|
||||
|
||||
# Create base nginx directory
|
||||
RUN mkdir -p /etc/nginx
|
||||
|
||||
# Copy Nginx and NJS modules from builder
|
||||
COPY --from=builder /usr/sbin/nginx /usr/sbin/
|
||||
COPY --from=builder /usr/lib/nginx/modules/ /usr/lib/nginx/modules/
|
||||
RUN rm -rf /etc/nginx/*
|
||||
COPY --from=builder /etc/nginx/ /etc/nginx/
|
||||
COPY --from=builder /usr/lib/nginx/ /usr/lib/nginx/
|
||||
|
||||
# Setup directories and permissions
|
||||
RUN mkdir -p /var/cache/nginx \
|
||||
&& mkdir -p /var/log/nginx \
|
||||
&& mkdir -p /etc/nginx/conf.d \
|
||||
&& mkdir -p /etc/nginx/njs \
|
||||
&& ln -sf /dev/stdout /var/log/nginx/access.log \
|
||||
&& ln -sf /dev/stderr /var/log/nginx/error.log \
|
||||
&& addgroup -S nginx \
|
||||
&& adduser -D -S -h /var/cache/nginx -s /sbin/nologin -G nginx nginx
|
||||
&& mkdir -p /etc/nginx/templates \
|
||||
&& chown -R nginx:nginx /var/cache/nginx \
|
||||
&& chown -R nginx:nginx /var/log/nginx \
|
||||
&& chown -R nginx:nginx /etc/nginx \
|
||||
&& touch /var/run/nginx.pid \
|
||||
&& chown nginx:nginx /var/run/nginx.pid \
|
||||
&& chmod 777 /var/log/nginx
|
||||
|
||||
# Copy necessary libraries from builder
|
||||
COPY --from=builder /usr/lib/libxml2.so* /usr/lib/
|
||||
|
|
@ -45,25 +57,34 @@ RUN echo 'load_module modules/ngx_http_js_module.so;' > /tmp/nginx.conf && \
|
|||
cat /etc/nginx/nginx.conf >> /tmp/nginx.conf && \
|
||||
mv /tmp/nginx.conf /etc/nginx/nginx.conf
|
||||
|
||||
# Copy our configuration
|
||||
COPY conf.d/default.conf /etc/nginx/conf.d/
|
||||
# Copy configurations
|
||||
COPY templates/default.conf /etc/nginx/templates/
|
||||
COPY njs/main.js /etc/nginx/njs/
|
||||
COPY entrypoint.sh /entrypoint.sh
|
||||
RUN dos2unix /entrypoint.sh
|
||||
|
||||
# Set proper permissions
|
||||
RUN chown -R nginx:nginx /var/cache/nginx \
|
||||
&& chown -R nginx:nginx /var/log/nginx \
|
||||
&& chown -R nginx:nginx /etc/nginx/conf.d \
|
||||
&& touch /var/run/nginx.pid \
|
||||
&& chown -R nginx:nginx /var/run/nginx.pid
|
||||
ENV PORT=80 \
|
||||
FRONTEND_HOST=frontend \
|
||||
FRONTEND_PORT=5173 \
|
||||
BACKEND_HOST=backend \
|
||||
BACKEND_PORT=3000
|
||||
|
||||
# Verify the configuration
|
||||
# RUN nginx -t --dry-run
|
||||
# Set final permissions
|
||||
RUN chmod +x /entrypoint.sh && \
|
||||
chown -R nginx:nginx /etc/nginx && \
|
||||
chown -R nginx:nginx /var/log/nginx && \
|
||||
chown -R nginx:nginx /var/cache/nginx && \
|
||||
chmod 755 /etc/nginx && \
|
||||
chmod 777 /etc/nginx/conf.d && \
|
||||
chmod 644 /etc/nginx/templates/default.conf && \
|
||||
chmod 644 /etc/nginx/conf.d/default.conf
|
||||
|
||||
# Switch to non-root user
|
||||
# Switch to nginx user
|
||||
USER nginx
|
||||
|
||||
# Expose HTTP port
|
||||
EXPOSE 80
|
||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
|
||||
CMD wget -q --spider http://0.0.0.0:${PORT}/health || exit 1
|
||||
|
||||
# Start Nginx
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
# Start Nginx using entrypoint script
|
||||
# CMD [ "/bin/sh","-c","sleep 3600" ] # For debugging
|
||||
ENTRYPOINT [ "/entrypoint.sh" ]
|
||||
10
nginx/entrypoint.sh
Normal file
10
nginx/entrypoint.sh
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
#!/bin/sh
|
||||
# entrypoint.sh
|
||||
|
||||
# We are already running as nginx user
|
||||
envsubst '${PORT} ${FRONTEND_HOST} ${FRONTEND_PORT} ${BACKEND_HOST} ${BACKEND_PORT}' \
|
||||
< /etc/nginx/templates/default.conf \
|
||||
> /etc/nginx/conf.d/default.conf
|
||||
|
||||
# Start nginx
|
||||
exec nginx -g "daemon off;"
|
||||
|
|
@ -8,18 +8,38 @@ map $http_upgrade $connection_upgrade {
|
|||
}
|
||||
|
||||
upstream frontend {
|
||||
server frontend:5173;
|
||||
server ${FRONTEND_HOST}:${FRONTEND_PORT};
|
||||
}
|
||||
|
||||
upstream backend {
|
||||
server backend:3000;
|
||||
server ${BACKEND_HOST}:${BACKEND_PORT};
|
||||
}
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
listen ${PORT};
|
||||
|
||||
set $proxy_target "";
|
||||
|
||||
location /health {
|
||||
access_log off;
|
||||
add_header Content-Type text/plain;
|
||||
return 200 'healthy';
|
||||
}
|
||||
|
||||
location /backend-health {
|
||||
proxy_pass http://backend/health;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
access_log off;
|
||||
}
|
||||
|
||||
location /frontend-health {
|
||||
proxy_pass http://frontend;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
access_log off;
|
||||
}
|
||||
|
||||
location /api {
|
||||
proxy_pass http://backend;
|
||||
proxy_http_version 1.1;
|
||||
|
|
@ -1,9 +1,8 @@
|
|||
# Use the Node base image
|
||||
FROM node:18 AS quizroom
|
||||
|
||||
ARG PORT=4500
|
||||
ENV PORT=${PORT}
|
||||
ENV ROOM_ID=${ROOM_ID}
|
||||
ENV PORT=4500
|
||||
ENV ROOM_ID=000000
|
||||
|
||||
# Create a working directory
|
||||
WORKDIR /usr/src/app
|
||||
|
|
@ -15,6 +14,10 @@ RUN npm install
|
|||
# Copy the rest of the source code to the container
|
||||
COPY . .
|
||||
|
||||
# Ensure healthcheck.sh has execution permissions
|
||||
COPY healthcheck.sh /usr/src/app/healthcheck.sh
|
||||
RUN chmod +x /usr/src/app/healthcheck.sh
|
||||
|
||||
# Build the TypeScript code
|
||||
RUN npm run build
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,8 @@ import http from "http";
|
|||
import { Server, ServerOptions } from "socket.io";
|
||||
import { setupWebsocket } from "./socket/setupWebSocket";
|
||||
import dotenv from "dotenv";
|
||||
import express from 'express';
|
||||
import express from "express";
|
||||
import os from "os"; // Import the os module
|
||||
|
||||
// Load environment variables
|
||||
dotenv.config();
|
||||
|
|
@ -36,6 +37,7 @@ app.get('/health', (_, res) => {
|
|||
}
|
||||
});
|
||||
|
||||
|
||||
const ioOptions: Partial<ServerOptions> = {
|
||||
path: `/api/room/${roomId}/socket`,
|
||||
cors: {
|
||||
|
|
|
|||
|
|
@ -8,6 +8,8 @@ services:
|
|||
- PORT=${PORT:-4500}
|
||||
ports:
|
||||
- "${PORT:-4500}:${PORT:-4500}"
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
environment:
|
||||
- PORT=${PORT:-4500}
|
||||
- ROOM_ID=${ROOM_ID}
|
||||
|
|
|
|||
|
|
@ -1,2 +1,2 @@
|
|||
#!/bin/sh
|
||||
#!/bin/bash
|
||||
curl -f "http://0.0.0.0:${PORT}/health" || exit 1
|
||||
319
quizRoom/package-lock.json
generated
319
quizRoom/package-lock.json
generated
|
|
@ -9,17 +9,24 @@
|
|||
"version": "1.0.0",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"dockerode": "^4.0.2",
|
||||
"dotenv": "^16.4.5",
|
||||
"express": "^4.21.1",
|
||||
"http": "^0.0.1-security",
|
||||
"socket.io": "^4.8.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/dockerode": "^3.3.32",
|
||||
"@types/express": "^5.0.0",
|
||||
"ts-node": "^10.9.2",
|
||||
"typescript": "^5.6.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@balena/dockerignore": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@balena/dockerignore/-/dockerignore-1.0.2.tgz",
|
||||
"integrity": "sha512-wMue2Sy4GAVTk6Ic4tJVcnfdau+gx2EnG7S+uAEe+TWJFqE4YoWN4/H8MSLj4eYJKxGg26lZwboEniNiNwZQ6Q=="
|
||||
},
|
||||
"node_modules/@cspotcode/source-map-support": {
|
||||
"version": "0.8.1",
|
||||
"resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",
|
||||
|
|
@ -118,6 +125,27 @@
|
|||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/docker-modem": {
|
||||
"version": "3.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/docker-modem/-/docker-modem-3.0.6.tgz",
|
||||
"integrity": "sha512-yKpAGEuKRSS8wwx0joknWxsmLha78wNMe9R2S3UNsVOkZded8UqOrV8KoeDXoXsjndxwyF3eIhyClGbO1SEhEg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/node": "*",
|
||||
"@types/ssh2": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/dockerode": {
|
||||
"version": "3.3.32",
|
||||
"resolved": "https://registry.npmjs.org/@types/dockerode/-/dockerode-3.3.32.tgz",
|
||||
"integrity": "sha512-xxcG0g5AWKtNyh7I7wswLdFvym4Mlqks5ZlKzxEUrGHS0r0PUOfxm2T0mspwu10mHQqu3Ck3MI3V2HqvLWE1fg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/docker-modem": "*",
|
||||
"@types/node": "*",
|
||||
"@types/ssh2": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/express": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.0.tgz",
|
||||
|
|
@ -195,6 +223,30 @@
|
|||
"@types/send": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/ssh2": {
|
||||
"version": "1.15.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/ssh2/-/ssh2-1.15.1.tgz",
|
||||
"integrity": "sha512-ZIbEqKAsi5gj35y4P4vkJYly642wIbY6PqoN0xiyQGshKUGXR9WQjF/iF9mXBQ8uBKy3ezfsCkcoHKhd0BzuDA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/node": "^18.11.18"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/ssh2/node_modules/@types/node": {
|
||||
"version": "18.19.67",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.67.tgz",
|
||||
"integrity": "sha512-wI8uHusga+0ZugNp0Ol/3BqQfEcCCNfojtO6Oou9iVNGPTL6QNSdnUdqq85fRgIorLhLMuPIKpsN98QE9Nh+KQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"undici-types": "~5.26.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/ssh2/node_modules/undici-types": {
|
||||
"version": "5.26.5",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
|
||||
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/accepts": {
|
||||
"version": "1.3.8",
|
||||
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
|
||||
|
|
@ -243,6 +295,33 @@
|
|||
"integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/asn1": {
|
||||
"version": "0.2.6",
|
||||
"resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz",
|
||||
"integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==",
|
||||
"dependencies": {
|
||||
"safer-buffer": "~2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/base64-js": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
|
||||
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/feross"
|
||||
},
|
||||
{
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
]
|
||||
},
|
||||
"node_modules/base64id": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz",
|
||||
|
|
@ -251,6 +330,24 @@
|
|||
"node": "^4.5.0 || >= 5.9"
|
||||
}
|
||||
},
|
||||
"node_modules/bcrypt-pbkdf": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz",
|
||||
"integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==",
|
||||
"dependencies": {
|
||||
"tweetnacl": "^0.14.3"
|
||||
}
|
||||
},
|
||||
"node_modules/bl": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz",
|
||||
"integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==",
|
||||
"dependencies": {
|
||||
"buffer": "^5.5.0",
|
||||
"inherits": "^2.0.4",
|
||||
"readable-stream": "^3.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/body-parser": {
|
||||
"version": "1.20.3",
|
||||
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz",
|
||||
|
|
@ -290,6 +387,38 @@
|
|||
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/buffer": {
|
||||
"version": "5.7.1",
|
||||
"resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
|
||||
"integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/feross"
|
||||
},
|
||||
{
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
],
|
||||
"dependencies": {
|
||||
"base64-js": "^1.3.1",
|
||||
"ieee754": "^1.1.13"
|
||||
}
|
||||
},
|
||||
"node_modules/buildcheck": {
|
||||
"version": "0.0.6",
|
||||
"resolved": "https://registry.npmjs.org/buildcheck/-/buildcheck-0.0.6.tgz",
|
||||
"integrity": "sha512-8f9ZJCUXyT1M35Jx7MkBgmBMo3oHTTBIPLiY9xyL0pl3T5RwcPEY8cUHr5LBNfu/fk6c2T4DJZuVM/8ZZT2D2A==",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/bytes": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
|
||||
|
|
@ -318,6 +447,11 @@
|
|||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/chownr": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz",
|
||||
"integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg=="
|
||||
},
|
||||
"node_modules/content-disposition": {
|
||||
"version": "0.5.4",
|
||||
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
|
||||
|
|
@ -365,6 +499,20 @@
|
|||
"node": ">= 0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/cpu-features": {
|
||||
"version": "0.0.10",
|
||||
"resolved": "https://registry.npmjs.org/cpu-features/-/cpu-features-0.0.10.tgz",
|
||||
"integrity": "sha512-9IkYqtX3YHPCzoVg1Py+o9057a3i0fp7S530UWokCSaFVTc7CwXPRiOjRjBQQ18ZCNafx78YfnG+HALxtVmOGA==",
|
||||
"hasInstallScript": true,
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"buildcheck": "~0.0.6",
|
||||
"nan": "^2.19.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/create-require": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz",
|
||||
|
|
@ -432,6 +580,33 @@
|
|||
"node": ">=0.3.1"
|
||||
}
|
||||
},
|
||||
"node_modules/docker-modem": {
|
||||
"version": "5.0.3",
|
||||
"resolved": "https://registry.npmjs.org/docker-modem/-/docker-modem-5.0.3.tgz",
|
||||
"integrity": "sha512-89zhop5YVhcPEt5FpUFGr3cDyceGhq/F9J+ZndQ4KfqNvfbJpPMfgeixFgUj5OjCYAboElqODxY5Z1EBsSa6sg==",
|
||||
"dependencies": {
|
||||
"debug": "^4.1.1",
|
||||
"readable-stream": "^3.5.0",
|
||||
"split-ca": "^1.0.1",
|
||||
"ssh2": "^1.15.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/dockerode": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/dockerode/-/dockerode-4.0.2.tgz",
|
||||
"integrity": "sha512-9wM1BVpVMFr2Pw3eJNXrYYt6DT9k0xMcsSCjtPvyQ+xa1iPg/Mo3T/gUcwI0B2cczqCeCYRPF8yFYDwtFXT0+w==",
|
||||
"dependencies": {
|
||||
"@balena/dockerignore": "^1.0.2",
|
||||
"docker-modem": "^5.0.3",
|
||||
"tar-fs": "~2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/dotenv": {
|
||||
"version": "16.4.5",
|
||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz",
|
||||
|
|
@ -458,6 +633,14 @@
|
|||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/end-of-stream": {
|
||||
"version": "1.4.4",
|
||||
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz",
|
||||
"integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==",
|
||||
"dependencies": {
|
||||
"once": "^1.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/engine.io": {
|
||||
"version": "6.6.2",
|
||||
"resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.2.tgz",
|
||||
|
|
@ -639,6 +822,11 @@
|
|||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/fs-constants": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
|
||||
"integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow=="
|
||||
},
|
||||
"node_modules/function-bind": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
||||
|
|
@ -760,6 +948,25 @@
|
|||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ieee754": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
|
||||
"integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/feross"
|
||||
},
|
||||
{
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
]
|
||||
},
|
||||
"node_modules/inherits": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
||||
|
|
@ -839,11 +1046,22 @@
|
|||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/mkdirp-classic": {
|
||||
"version": "0.5.3",
|
||||
"resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz",
|
||||
"integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A=="
|
||||
},
|
||||
"node_modules/ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
|
||||
},
|
||||
"node_modules/nan": {
|
||||
"version": "2.22.0",
|
||||
"resolved": "https://registry.npmjs.org/nan/-/nan-2.22.0.tgz",
|
||||
"integrity": "sha512-nbajikzWTMwsW+eSsNm3QwlOs7het9gGJU5dDZzRTQGk03vyBOauxgI4VakDzE0PtsGTmXPsXTbbjVhRwR5mpw==",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/negotiator": {
|
||||
"version": "0.6.3",
|
||||
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
|
||||
|
|
@ -884,6 +1102,14 @@
|
|||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/once": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
|
||||
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
|
||||
"dependencies": {
|
||||
"wrappy": "1"
|
||||
}
|
||||
},
|
||||
"node_modules/parseurl": {
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
|
||||
|
|
@ -912,6 +1138,15 @@
|
|||
"node": ">= 0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/pump": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz",
|
||||
"integrity": "sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==",
|
||||
"dependencies": {
|
||||
"end-of-stream": "^1.1.0",
|
||||
"once": "^1.3.1"
|
||||
}
|
||||
},
|
||||
"node_modules/qs": {
|
||||
"version": "6.13.0",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz",
|
||||
|
|
@ -951,6 +1186,19 @@
|
|||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/readable-stream": {
|
||||
"version": "3.6.2",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
|
||||
"integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
|
||||
"dependencies": {
|
||||
"inherits": "^2.0.3",
|
||||
"string_decoder": "^1.1.1",
|
||||
"util-deprecate": "^1.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/safe-buffer": {
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
|
||||
|
|
@ -1119,6 +1367,28 @@
|
|||
"node": ">=10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/split-ca": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/split-ca/-/split-ca-1.0.1.tgz",
|
||||
"integrity": "sha512-Q5thBSxp5t8WPTTJQS59LrGqOZqOsrhDGDVm8azCqIBjSBd7nd9o2PM+mDulQQkh8h//4U6hFZnc/mul8t5pWQ=="
|
||||
},
|
||||
"node_modules/ssh2": {
|
||||
"version": "1.16.0",
|
||||
"resolved": "https://registry.npmjs.org/ssh2/-/ssh2-1.16.0.tgz",
|
||||
"integrity": "sha512-r1X4KsBGedJqo7h8F5c4Ybpcr5RjyP+aWIG007uBPRjmdQWfEiVLzSK71Zji1B9sKxwaCvD8y8cwSkYrlLiRRg==",
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"asn1": "^0.2.6",
|
||||
"bcrypt-pbkdf": "^1.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.16.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"cpu-features": "~0.0.10",
|
||||
"nan": "^2.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/statuses": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
|
||||
|
|
@ -1128,6 +1398,40 @@
|
|||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/string_decoder": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
|
||||
"integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
|
||||
"dependencies": {
|
||||
"safe-buffer": "~5.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/tar-fs": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.0.1.tgz",
|
||||
"integrity": "sha512-6tzWDMeroL87uF/+lin46k+Q+46rAJ0SyPGz7OW7wTgblI273hsBqk2C1j0/xNadNLKDTUL9BukSjB7cwgmlPA==",
|
||||
"dependencies": {
|
||||
"chownr": "^1.1.1",
|
||||
"mkdirp-classic": "^0.5.2",
|
||||
"pump": "^3.0.0",
|
||||
"tar-stream": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/tar-stream": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz",
|
||||
"integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==",
|
||||
"dependencies": {
|
||||
"bl": "^4.0.3",
|
||||
"end-of-stream": "^1.4.1",
|
||||
"fs-constants": "^1.0.0",
|
||||
"inherits": "^2.0.3",
|
||||
"readable-stream": "^3.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/toidentifier": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
|
||||
|
|
@ -1180,6 +1484,11 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"node_modules/tweetnacl": {
|
||||
"version": "0.14.5",
|
||||
"resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz",
|
||||
"integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA=="
|
||||
},
|
||||
"node_modules/type-is": {
|
||||
"version": "1.6.18",
|
||||
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
|
||||
|
|
@ -1220,6 +1529,11 @@
|
|||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/util-deprecate": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="
|
||||
},
|
||||
"node_modules/utils-merge": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
|
||||
|
|
@ -1243,6 +1557,11 @@
|
|||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/wrappy": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
||||
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="
|
||||
},
|
||||
"node_modules/ws": {
|
||||
"version": "8.17.1",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz",
|
||||
|
|
|
|||
|
|
@ -12,11 +12,13 @@
|
|||
"license": "ISC",
|
||||
"description": "",
|
||||
"devDependencies": {
|
||||
"@types/dockerode": "^3.3.32",
|
||||
"@types/express": "^5.0.0",
|
||||
"ts-node": "^10.9.2",
|
||||
"typescript": "^5.6.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"dockerode": "^4.0.2",
|
||||
"dotenv": "^16.4.5",
|
||||
"express": "^4.21.1",
|
||||
"http": "^0.0.1-security",
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
import { Server, Socket } from "socket.io";
|
||||
import Docker from 'dockerode';
|
||||
import fs from 'fs';
|
||||
|
||||
const MAX_USERS_PER_ROOM = 60;
|
||||
const MAX_TOTAL_CONNECTIONS = 2000;
|
||||
|
|
@ -23,6 +25,7 @@ export const setupWebsocket = (io: Server): void => {
|
|||
? sentRoomName.toUpperCase()
|
||||
: generateRoomName();
|
||||
|
||||
console.log(`Created room with name: ${roomName}`);
|
||||
if (!io.sockets.adapter.rooms.get(roomName)) {
|
||||
socket.join(roomName);
|
||||
socket.emit("create-success", roomName);
|
||||
|
|
@ -96,9 +99,137 @@ export const setupWebsocket = (io: Server): void => {
|
|||
socket.on("error", (error) => {
|
||||
console.error("WebSocket server error:", error);
|
||||
});
|
||||
|
||||
|
||||
// Stress Testing
|
||||
|
||||
socket.on("message-from-teacher", ({ roomName, message }: { roomName: string; message: string }) => {
|
||||
console.log(`Message reçu dans la salle ${roomName} : ${message}`);
|
||||
socket.to(roomName).emit("message-sent-teacher", { message });
|
||||
});
|
||||
|
||||
socket.on("message-from-student", ({ roomName, message }: { roomName: string; message: string }) => {
|
||||
console.log(`Message reçu dans la salle ${roomName} : ${message}`);
|
||||
socket.to(roomName).emit("message-sent-student", { message });
|
||||
});
|
||||
|
||||
interface ContainerStats {
|
||||
containerId: string;
|
||||
containerName: string;
|
||||
memoryUsedMB: number | null;
|
||||
memoryUsedPercentage: number | null;
|
||||
cpuUsedPercentage: number | null;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
class ContainerMetrics {
|
||||
private docker: Docker;
|
||||
private containerName: string;
|
||||
|
||||
private bytesToMB(bytes: number): number {
|
||||
return Math.round(bytes / (1024 * 1024));
|
||||
}
|
||||
|
||||
constructor() {
|
||||
this.docker = new Docker({
|
||||
socketPath: process.platform === 'win32' ? '//./pipe/docker_engine' : '/var/run/docker.sock'
|
||||
});
|
||||
this.containerName = `room_${process.env.ROOM_ID}`;
|
||||
}
|
||||
|
||||
private async getContainerNetworks(containerId: string): Promise<string[]> {
|
||||
const container = this.docker.getContainer(containerId);
|
||||
const info = await container.inspect();
|
||||
return Object.keys(info.NetworkSettings.Networks);
|
||||
}
|
||||
|
||||
public async getAllContainerStats(): Promise<ContainerStats[]> {
|
||||
try {
|
||||
// First get our container to find its networks
|
||||
const ourContainer = await this.docker.listContainers({
|
||||
all: true,
|
||||
filters: { name: [this.containerName] }
|
||||
});
|
||||
|
||||
if (!ourContainer.length) {
|
||||
throw new Error(`Container ${this.containerName} not found`);
|
||||
}
|
||||
|
||||
const ourNetworks = await this.getContainerNetworks(ourContainer[0].Id);
|
||||
|
||||
// Get all containers
|
||||
const allContainers = await this.docker.listContainers();
|
||||
|
||||
// Get stats for containers on the same networks
|
||||
const containerStats = await Promise.all(
|
||||
allContainers.map(async (container): Promise<ContainerStats | null> => {
|
||||
try {
|
||||
const containerNetworks = await this.getContainerNetworks(container.Id);
|
||||
// Check if container shares any network with our container
|
||||
if (!containerNetworks.some(network => ourNetworks.includes(network))) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const stats = await this.docker.getContainer(container.Id).stats({ stream: false });
|
||||
|
||||
const memoryStats = {
|
||||
usage: stats.memory_stats.usage,
|
||||
limit: stats.memory_stats.limit || 0,
|
||||
percent: stats.memory_stats.limit ? (stats.memory_stats.usage / stats.memory_stats.limit) * 100 : 0
|
||||
};
|
||||
|
||||
const cpuDelta = stats.cpu_stats?.cpu_usage?.total_usage - (stats.precpu_stats?.cpu_usage?.total_usage || 0);
|
||||
const systemDelta = stats.cpu_stats?.system_cpu_usage - (stats.precpu_stats?.system_cpu_usage || 0);
|
||||
const cpuPercent = systemDelta > 0 ? (cpuDelta / systemDelta) * (stats.cpu_stats?.online_cpus || 1) * 100 : 0;
|
||||
|
||||
return {
|
||||
containerId: container.Id,
|
||||
containerName: container.Names[0].replace(/^\//, ''),
|
||||
memoryUsedMB: this.bytesToMB(memoryStats.usage),
|
||||
memoryUsedPercentage: memoryStats.percent,
|
||||
cpuUsedPercentage: cpuPercent
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
containerId: container.Id,
|
||||
containerName: container.Names[0].replace(/^\//, ''),
|
||||
memoryUsedMB: null,
|
||||
memoryUsedPercentage: null,
|
||||
cpuUsedPercentage: null,
|
||||
error: error instanceof Error ? error.message : String(error)
|
||||
};
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
// Change the filter to use proper type predicate
|
||||
return containerStats.filter((stats): stats is ContainerStats => stats !== null);
|
||||
} catch (error) {
|
||||
console.error('Stats error:', error);
|
||||
return [{
|
||||
containerId: 'unknown',
|
||||
containerName: 'unknown',
|
||||
memoryUsedMB: null,
|
||||
memoryUsedPercentage: null,
|
||||
cpuUsedPercentage: null,
|
||||
error: error instanceof Error ? error.message : String(error)
|
||||
}];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const containerMetrics = new ContainerMetrics();
|
||||
|
||||
socket.on("get-usage", async () => {
|
||||
try {
|
||||
const usageData = await containerMetrics.getAllContainerStats();
|
||||
socket.emit("usage-data", usageData);
|
||||
} catch (error) {
|
||||
socket.emit("error", { message: "Failed to retrieve usage data" });
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
const generateRoomName = (length = 6): string => {
|
||||
const characters = "0123456789";
|
||||
|
|
|
|||
|
|
@ -8,6 +8,10 @@ RUN npm install
|
|||
|
||||
COPY ./ .
|
||||
|
||||
EXPOSE 4400
|
||||
ENV PORT=3000
|
||||
EXPOSE ${PORT}
|
||||
|
||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
|
||||
CMD curl -f http://localhost:${PORT}/health || exit 1
|
||||
|
||||
CMD ["npm", "run", "start"]
|
||||
|
|
@ -47,6 +47,7 @@ const folderRouter = require('./routers/folders.js');
|
|||
const quizRouter = require('./routers/quiz.js');
|
||||
const imagesRouter = require('./routers/images.js');
|
||||
const roomRouter = require('./routers/rooms.js');
|
||||
const healthRouter = require('./routers/health.js');
|
||||
|
||||
// Setup environment
|
||||
dotenv.config();
|
||||
|
|
@ -71,6 +72,7 @@ app.use('/api/folder', folderRouter);
|
|||
app.use('/api/quiz', quizRouter);
|
||||
app.use('/api/image', imagesRouter);
|
||||
app.use('/api/room', roomRouter);
|
||||
app.use('/health', healthRouter);
|
||||
|
||||
app.use(errorHandler);
|
||||
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ class DockerRoomProvider extends BaseRoomProvider {
|
|||
const dockerSocket = process.env.DOCKER_SOCKET || "/var/run/docker.sock";
|
||||
|
||||
this.docker = new Docker({ socketPath: dockerSocket });
|
||||
this.docker_network = 'evaluetonsavoir_quiz_network';
|
||||
this.docker_network = process.env.QUIZ_NETWORK_NAME || 'evaluetonsavoir_quiz_network';
|
||||
}
|
||||
|
||||
async syncInstantiatedRooms() {
|
||||
|
|
@ -46,10 +46,52 @@ class DockerRoomProvider extends BaseRoomProvider {
|
|||
}
|
||||
}
|
||||
|
||||
async checkAndPullImage(imageName) {
|
||||
try {
|
||||
const images = await this.docker.listImages({ all: true });
|
||||
//console.log('Images disponibles:', images.map(img => ({
|
||||
// RepoTags: img.RepoTags || [],
|
||||
// Id: img.Id
|
||||
//})));
|
||||
|
||||
const imageExists = images.some(img => {
|
||||
const tags = img.RepoTags || [];
|
||||
return tags.includes(imageName) ||
|
||||
tags.includes(`${imageName}:latest`) ||
|
||||
img.Id.includes(imageName);
|
||||
});
|
||||
|
||||
if (!imageExists) {
|
||||
console.log(`L'image ${imageName} n'a pas été trouvée localement, tentative de téléchargement...`);
|
||||
try {
|
||||
await this.docker.pull(imageName);
|
||||
console.log(`L'image ${imageName} a été téléchargée avec succès`);
|
||||
} catch (pullError) {
|
||||
const localImages = await this.docker.listImages({ all: true });
|
||||
const foundLocally = localImages.some(img =>
|
||||
(img.RepoTags || []).includes(imageName) ||
|
||||
(img.RepoTags || []).includes(`${imageName}:latest`)
|
||||
);
|
||||
|
||||
if (!foundLocally) {
|
||||
throw new Error(`Impossible de trouver ou de télécharger l'image ${imageName}: ${pullError.message}`);
|
||||
} else {
|
||||
console.log(`L'image ${imageName} a été trouvée localement après vérification supplémentaire`);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
console.log(`L'image ${imageName} a été trouvée localement`);
|
||||
}
|
||||
} catch (error) {
|
||||
throw new Error(`Une erreur est survenue lors de la vérification/téléchargement de l'image ${imageName}: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
async createRoom(roomId, options) {
|
||||
const container_name = `room_${roomId}`;
|
||||
|
||||
try {
|
||||
await this.checkAndPullImage(this.quiz_docker_image);
|
||||
const containerConfig = {
|
||||
Image: this.quiz_docker_image,
|
||||
name: container_name,
|
||||
|
|
@ -57,7 +99,10 @@ class DockerRoomProvider extends BaseRoomProvider {
|
|||
NetworkMode: this.docker_network,
|
||||
RestartPolicy: {
|
||||
Name: 'unless-stopped'
|
||||
}
|
||||
},
|
||||
Binds: [
|
||||
'/var/run/docker.sock:/var/run/docker.sock'
|
||||
]
|
||||
},
|
||||
Env: [
|
||||
`ROOM_ID=${roomId}`,
|
||||
|
|
|
|||
20
server/routers/health.js
Normal file
20
server/routers/health.js
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
const express = require('express');
|
||||
const router = express.Router();
|
||||
|
||||
router.get('/', async (req, res) => {
|
||||
try {
|
||||
const dbStatus = await require('../config/db.js').getConnection() ? 'connected' : 'disconnected';
|
||||
res.json({
|
||||
status: 'healthy',
|
||||
timestamp: new Date(),
|
||||
db: dbStatus
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
status: 'unhealthy',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
1
test/stressTest/.dockerignore
Normal file
1
test/stressTest/.dockerignore
Normal file
|
|
@ -0,0 +1 @@
|
|||
node_modules
|
||||
19
test/stressTest/.env.example
Normal file
19
test/stressTest/.env.example
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
# Target url
|
||||
BASE_URL=http://msevignyl.duckdns.org
|
||||
|
||||
# Connection account
|
||||
USER_EMAIL=admin@admin.com
|
||||
USER_PASSWORD=admin
|
||||
|
||||
# Stress test parameters
|
||||
NUMBER_ROOMS=5
|
||||
USERS_PER_ROOM=60
|
||||
|
||||
# Optionnal
|
||||
|
||||
|
||||
MAX_MESSAGES_ROUND=20
|
||||
CONVERSATION_INTERVAL=1000
|
||||
MESSAGE_RESPONSE_TIMEOUT=5000
|
||||
BATCH_DELAY=1000
|
||||
BATCH_SIZE=10
|
||||
13
test/stressTest/Dockerfile
Normal file
13
test/stressTest/Dockerfile
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
FROM node:18
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY package*.json ./
|
||||
|
||||
RUN npm install
|
||||
|
||||
COPY . .
|
||||
|
||||
VOLUME /app/output
|
||||
|
||||
CMD ["node", "main.js"]
|
||||
51
test/stressTest/README.md
Normal file
51
test/stressTest/README.md
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
# Test de Charge - EvalueTonSavoir
|
||||
|
||||
Ce conteneur permet d'exécuter des tests de charge sur l'application EvalueTonSavoir.
|
||||
|
||||
## Prérequis
|
||||
|
||||
- Docker
|
||||
- Docker Compose
|
||||
|
||||
## Configuration
|
||||
|
||||
1. Créez un fichier `.env` à partir du modèle `.env.example`:
|
||||
|
||||
```bash
|
||||
copy .env.example .env
|
||||
```
|
||||
|
||||
2. Modifiez les variables dans le fichier .env:
|
||||
|
||||
```bash
|
||||
# URL de l'application cible
|
||||
BASE_URL=http://votre-url.com
|
||||
|
||||
# Compte de connexion
|
||||
USER_EMAIL=admin@admin.com
|
||||
USER_PASSWORD=admin
|
||||
|
||||
# Paramètres du test de charge
|
||||
NUMBER_ROOMS=5 # Nombre de salles à créer
|
||||
USERS_PER_ROOM=60 # Nombre d'utilisateurs par salle
|
||||
|
||||
```
|
||||
#### Paramètres optionnels
|
||||
Dans le fichier .env, vous pouvez aussi configurer:
|
||||
|
||||
```bash
|
||||
MAX_MESSAGES_ROUND=20 # Messages maximum par cycle
|
||||
CONVERSATION_INTERVAL=1000 # Intervalle entre les messages (ms)
|
||||
MESSAGE_RESPONSE_TIMEOUT=5000 # Timeout des réponses (ms)
|
||||
BATCH_DELAY=1000 # Délai entre les lots (ms)
|
||||
BATCH_SIZE=10 # Taille des lots d'utilisateurs
|
||||
```
|
||||
|
||||
## Démarrage
|
||||
Pour lancer le test de charge:
|
||||
|
||||
Les résultats seront disponibles dans le dossier output/.
|
||||
|
||||
```bash
|
||||
docker compose up
|
||||
```
|
||||
46
test/stressTest/class/metrics.js
Normal file
46
test/stressTest/class/metrics.js
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
export class TestMetrics {
|
||||
constructor() {
|
||||
this.reset();
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.roomsCreated = 0;
|
||||
this.roomsFailed = 0;
|
||||
this.usersConnected = 0;
|
||||
this.userConnectionsFailed = 0;
|
||||
this.messagesAttempted = 0;
|
||||
this.messagesSent = 0;
|
||||
this.messagesReceived = 0;
|
||||
this.errors = new Map();
|
||||
}
|
||||
|
||||
logError(category, error) {
|
||||
if (!this.errors.has(category)) {
|
||||
this.errors.set(category, []);
|
||||
}
|
||||
this.errors.get(category).push(error);
|
||||
}
|
||||
|
||||
getSummary() {
|
||||
return {
|
||||
rooms: {
|
||||
created: this.roomsCreated,
|
||||
failed: this.roomsFailed,
|
||||
total: this.roomsCreated + this.roomsFailed
|
||||
},
|
||||
users: {
|
||||
connected: this.usersConnected,
|
||||
failed: this.userConnectionsFailed,
|
||||
total: this.usersConnected + this.userConnectionsFailed
|
||||
},
|
||||
messages: {
|
||||
attempted: this.messagesAttempted,
|
||||
sent: this.messagesSent,
|
||||
received: this.messagesReceived
|
||||
},
|
||||
errors: Object.fromEntries(
|
||||
Array.from(this.errors.entries()).map(([k, v]) => [k, v.length])
|
||||
)
|
||||
};
|
||||
}
|
||||
}
|
||||
83
test/stressTest/class/roomParticipant.js
Normal file
83
test/stressTest/class/roomParticipant.js
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
import { io } from "socket.io-client";
|
||||
|
||||
export class RoomParticipant {
|
||||
constructor(username, roomName) {
|
||||
this.username = username;
|
||||
this.roomName = roomName;
|
||||
this.socket = null;
|
||||
this.maxRetries = 3;
|
||||
this.retryDelay = 1000;
|
||||
}
|
||||
|
||||
async connectToRoom(baseUrl) {
|
||||
let retries = 0;
|
||||
const maxRetries = 2;
|
||||
const retryDelay = 2000;
|
||||
|
||||
const cleanup = () => {
|
||||
if (this.socket) {
|
||||
this.socket.removeAllListeners();
|
||||
this.socket.disconnect();
|
||||
this.socket = null;
|
||||
}
|
||||
};
|
||||
|
||||
while (retries < maxRetries) {
|
||||
try {
|
||||
const socket = io(baseUrl, {
|
||||
path: `/api/room/${this.roomName}/socket`,
|
||||
transports: ['websocket'],
|
||||
timeout: 8000,
|
||||
reconnection: false,
|
||||
forceNew: true
|
||||
});
|
||||
|
||||
const result = await new Promise((resolve, reject) => {
|
||||
const timeout = setTimeout(() => {
|
||||
cleanup();
|
||||
reject(new Error('Connection timeout'));
|
||||
}, 8000);
|
||||
|
||||
socket.on('connect', () => {
|
||||
clearTimeout(timeout);
|
||||
this.socket = socket;
|
||||
this.onConnected(); // Add this line
|
||||
resolve(socket);
|
||||
});
|
||||
|
||||
socket.on('connect_error', (error) => {
|
||||
clearTimeout(timeout);
|
||||
cleanup();
|
||||
reject(new Error(`Connection error: ${error.message}`));
|
||||
});
|
||||
|
||||
socket.on('error', (error) => {
|
||||
clearTimeout(timeout);
|
||||
cleanup();
|
||||
reject(new Error(`Socket error: ${error.message}`));
|
||||
});
|
||||
});
|
||||
|
||||
return result;
|
||||
|
||||
} catch (error) {
|
||||
retries++;
|
||||
if (retries === maxRetries) {
|
||||
throw error;
|
||||
}
|
||||
await new Promise(resolve => setTimeout(resolve, retryDelay));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onConnected() {
|
||||
// To be implemented by child classes
|
||||
}
|
||||
|
||||
disconnect() {
|
||||
if (this.socket) {
|
||||
this.socket.disconnect();
|
||||
this.socket = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
48
test/stressTest/class/student.js
Normal file
48
test/stressTest/class/student.js
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
// student.js
|
||||
import { RoomParticipant } from './roomParticipant.js';
|
||||
|
||||
export class Student extends RoomParticipant {
|
||||
|
||||
nbrMessageReceived = 0;
|
||||
|
||||
constructor(username, roomName) {
|
||||
super(username, roomName);
|
||||
}
|
||||
|
||||
connectToRoom(baseUrl) {
|
||||
return super.connectToRoom(baseUrl);
|
||||
}
|
||||
|
||||
onConnected() {
|
||||
this.joinRoom();
|
||||
this.listenForTeacherMessage();
|
||||
}
|
||||
|
||||
joinRoom() {
|
||||
if (this.socket) {
|
||||
this.socket.emit('join-room', {
|
||||
enteredRoomName: this.roomName,
|
||||
username: this.username
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
listenForTeacherMessage() {
|
||||
if (this.socket) {
|
||||
this.socket.on('message-sent-teacher', ({ message }) => {
|
||||
this.nbrMessageReceived++;
|
||||
this.respondToTeacher(message);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
respondToTeacher(message) {
|
||||
const reply = `${this.username} replying to: "${message}"`;
|
||||
if (this.socket) {
|
||||
this.socket.emit('message-from-student', {
|
||||
roomName: this.roomName,
|
||||
message: reply
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
46
test/stressTest/class/teacher.js
Normal file
46
test/stressTest/class/teacher.js
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
import { RoomParticipant } from './roomParticipant.js';
|
||||
|
||||
export class Teacher extends RoomParticipant {
|
||||
|
||||
nbrMessageReceived = 0;
|
||||
|
||||
constructor(username, roomName) {
|
||||
super(username, roomName);
|
||||
this.ready = false;
|
||||
}
|
||||
|
||||
connectToRoom(baseUrl) {
|
||||
return super.connectToRoom(baseUrl);
|
||||
}
|
||||
|
||||
onConnected() {
|
||||
this.createRoom();
|
||||
this.listenForStudentMessage();
|
||||
}
|
||||
|
||||
createRoom() {
|
||||
if (this.socket) {
|
||||
this.socket.emit('create-room', this.roomName);
|
||||
}
|
||||
}
|
||||
|
||||
broadcastMessage(message) {
|
||||
if (this.socket) {
|
||||
this.socket.emit('message-from-teacher', {
|
||||
roomName: this.roomName,
|
||||
message
|
||||
});
|
||||
} else {
|
||||
console.warn(`Teacher ${this.username} not ready to broadcast yet`);
|
||||
}
|
||||
}
|
||||
|
||||
listenForStudentMessage() {
|
||||
if (this.socket) {
|
||||
this.socket.on('message-sent-student', ({ message }) => {
|
||||
//console.log(`Teacher ${this.username} received: "${message}"`);
|
||||
this.nbrMessageReceived++;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
72
test/stressTest/class/watcher.js
Normal file
72
test/stressTest/class/watcher.js
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
import { RoomParticipant } from './roomParticipant.js';
|
||||
|
||||
export class Watcher extends RoomParticipant {
|
||||
|
||||
roomRessourcesData = [];
|
||||
checkRessourceInterval = null;
|
||||
|
||||
constructor(username, roomName) {
|
||||
super(username, roomName);
|
||||
}
|
||||
|
||||
connectToRoom(baseUrl) {
|
||||
return super.connectToRoom(baseUrl);
|
||||
}
|
||||
|
||||
onConnected() {
|
||||
this.startCheckingResources();
|
||||
}
|
||||
|
||||
checkRessource() {
|
||||
if (this.socket?.connected) {
|
||||
try {
|
||||
this.socket.emit("get-usage");
|
||||
this.socket.once("usage-data", (data) => {
|
||||
const timestamp = Date.now();
|
||||
// Store each container's metrics separately with timestamp
|
||||
data.forEach(containerStat => {
|
||||
const existingData = this.roomRessourcesData.find(d => d.containerId === containerStat.containerId);
|
||||
if (existingData) {
|
||||
existingData.metrics.push({
|
||||
timestamp,
|
||||
...containerStat
|
||||
});
|
||||
} else {
|
||||
this.roomRessourcesData.push({
|
||||
containerId: containerStat.containerId,
|
||||
containerName: containerStat.containerName,
|
||||
metrics: [{
|
||||
timestamp,
|
||||
...containerStat
|
||||
}]
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
console.warn(`Error capturing metrics for room ${this.roomName}:`, error.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
startCheckingResources(intervalMs = 500) {
|
||||
if (this.checkRessourceInterval) {
|
||||
console.warn(`Resource checking is already running for room ${this.roomName}.`);
|
||||
return;
|
||||
}
|
||||
|
||||
this.checkRessourceInterval = setInterval(() => this.checkRessource(), intervalMs);
|
||||
}
|
||||
|
||||
stopCheckingResources() {
|
||||
if (this.checkRessourceInterval) {
|
||||
clearInterval(this.checkRessourceInterval);
|
||||
this.checkRessourceInterval = null;
|
||||
}
|
||||
}
|
||||
|
||||
disconnect() {
|
||||
this.stopCheckingResources();
|
||||
super.disconnect();
|
||||
}
|
||||
}
|
||||
16
test/stressTest/docker-compose.yml
Normal file
16
test/stressTest/docker-compose.yml
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
version: '3'
|
||||
|
||||
services:
|
||||
|
||||
stress-test:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
container_name: stress-test
|
||||
network_mode: host
|
||||
env_file:
|
||||
- .env
|
||||
volumes:
|
||||
- ./output:/app/output
|
||||
tty: true
|
||||
stdin_open: true
|
||||
201
test/stressTest/main.js
Normal file
201
test/stressTest/main.js
Normal file
|
|
@ -0,0 +1,201 @@
|
|||
import { attemptLoginOrRegister, createRoomContainer } from './utility/apiServices.js';
|
||||
import { Student } from './class/student.js';
|
||||
import { Teacher } from './class/teacher.js';
|
||||
import { Watcher } from './class/watcher.js';
|
||||
import { TestMetrics } from './class/metrics.js';
|
||||
import dotenv from 'dotenv';
|
||||
import generateMetricsReport from './utility/metrics_generator.js';
|
||||
|
||||
dotenv.config();
|
||||
|
||||
const config = {
|
||||
baseUrl: process.env.BASE_URL || 'http://localhost',
|
||||
auth: {
|
||||
username: process.env.USER_EMAIL || 'admin@admin.com',
|
||||
password: process.env.USER_PASSWORD || 'admin'
|
||||
},
|
||||
rooms: {
|
||||
count: parseInt(process.env.NUMBER_ROOMS || '15'),
|
||||
usersPerRoom: parseInt(process.env.USERS_PER_ROOM || '60'),
|
||||
batchSize: parseInt(process.env.BATCH_SIZE || 5),
|
||||
batchDelay: parseInt(process.env.BATCH_DELAY || 250)
|
||||
},
|
||||
simulation: {
|
||||
maxMessages: parseInt(process.env.MAX_MESSAGES_ROUND || '20'),
|
||||
messageInterval: parseInt(process.env.CONVERSATION_INTERVAL || '1000'),
|
||||
responseTimeout: parseInt(process.env.MESSAGE_RESPONSE_TIMEOUT || 5000)
|
||||
}
|
||||
};
|
||||
|
||||
const rooms = new Map();
|
||||
const metrics = new TestMetrics();
|
||||
|
||||
// Changes to setupRoom function
|
||||
async function setupRoom(token, index) {
|
||||
try {
|
||||
const room = await createRoomContainer(config.baseUrl, token);
|
||||
if (!room?.id) throw new Error('Room creation failed');
|
||||
metrics.roomsCreated++;
|
||||
|
||||
const teacher = new Teacher(`teacher_${index}`, room.id);
|
||||
// Only create watcher for first room (index 0)
|
||||
const watcher = index === 0 ? new Watcher(`watcher_${index}`, room.id) : null;
|
||||
|
||||
await Promise.all([
|
||||
teacher.connectToRoom(config.baseUrl)
|
||||
.then(() => metrics.usersConnected++)
|
||||
.catch(err => {
|
||||
metrics.userConnectionsFailed++;
|
||||
metrics.logError('teacherConnection', err);
|
||||
console.warn(`Teacher ${index} connection failed:`, err.message);
|
||||
}),
|
||||
// Only connect watcher if it exists
|
||||
...(watcher ? [
|
||||
watcher.connectToRoom(config.baseUrl)
|
||||
.then(() => metrics.usersConnected++)
|
||||
.catch(err => {
|
||||
metrics.userConnectionsFailed++;
|
||||
metrics.logError('watcherConnection', err);
|
||||
console.warn(`Watcher ${index} connection failed:`, err.message);
|
||||
})
|
||||
] : [])
|
||||
]);
|
||||
|
||||
// Adjust number of students based on whether room has a watcher
|
||||
const studentCount = watcher ?
|
||||
config.rooms.usersPerRoom - 2 : // Room with watcher: subtract teacher and watcher
|
||||
config.rooms.usersPerRoom - 1; // Rooms without watcher: subtract only teacher
|
||||
|
||||
const students = Array.from({ length: studentCount },
|
||||
(_, i) => new Student(`student_${index}_${i}`, room.id));
|
||||
|
||||
rooms.set(room.id, { teacher, watcher, students });
|
||||
return room.id;
|
||||
} catch (err) {
|
||||
metrics.roomsFailed++;
|
||||
metrics.logError('roomSetup', err);
|
||||
console.warn(`Room ${index} setup failed:`, err.message);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function connectParticipants(roomId) {
|
||||
const { students } = rooms.get(roomId);
|
||||
const participants = [...students];
|
||||
|
||||
for (let i = 0; i < participants.length; i += config.rooms.batchSize) {
|
||||
const batch = participants.slice(i, i + config.rooms.batchSize);
|
||||
await Promise.all(batch.map(p =>
|
||||
Promise.race([
|
||||
p.connectToRoom(config.baseUrl).then(() => {
|
||||
metrics.usersConnected++;
|
||||
}),
|
||||
new Promise((_, reject) => setTimeout(() => reject(new Error('Timeout')), 10000))
|
||||
]).catch(err => {
|
||||
metrics.userConnectionsFailed++;
|
||||
metrics.logError('studentConnection', err);
|
||||
console.warn(`Connection failed for ${p.username}:`, err.message);
|
||||
})
|
||||
));
|
||||
await new Promise(resolve => setTimeout(resolve, config.rooms.batchDelay));
|
||||
}
|
||||
}
|
||||
|
||||
async function simulate() {
|
||||
const simulations = Array.from(rooms.entries()).map(async ([roomId, { teacher, students }]) => {
|
||||
const connectedStudents = students.filter(student => student.socket?.connected);
|
||||
const expectedResponses = connectedStudents.length;
|
||||
|
||||
for (let i = 0; i < config.simulation.maxMessages; i++) {
|
||||
metrics.messagesAttempted++;
|
||||
const initialMessages = teacher.nbrMessageReceived;
|
||||
|
||||
try {
|
||||
teacher.broadcastMessage(`Message ${i + 1} from ${teacher.username}`);
|
||||
metrics.messagesSent++;
|
||||
|
||||
await Promise.race([
|
||||
new Promise(resolve => {
|
||||
const checkResponses = setInterval(() => {
|
||||
const receivedResponses = teacher.nbrMessageReceived - initialMessages;
|
||||
if (receivedResponses >= expectedResponses) {
|
||||
metrics.messagesReceived += receivedResponses;
|
||||
clearInterval(checkResponses);
|
||||
resolve();
|
||||
}
|
||||
}, 100);
|
||||
}),
|
||||
new Promise((_, reject) =>
|
||||
setTimeout(() => reject(new Error('Response timeout')), config.simulation.responseTimeout)
|
||||
)
|
||||
]);
|
||||
} catch (error) {
|
||||
metrics.logError('messaging', error);
|
||||
console.error(`Error in room ${roomId} message ${i + 1}:`, error);
|
||||
}
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, config.simulation.messageInterval));
|
||||
}
|
||||
});
|
||||
|
||||
await Promise.all(simulations);
|
||||
console.log('All room simulations completed');
|
||||
}
|
||||
|
||||
async function generateReport() {
|
||||
const watcherRoom = Array.from(rooms.entries()).find(([_, room]) => room.watcher);
|
||||
if (!watcherRoom) {
|
||||
throw new Error('No watcher found in any room');
|
||||
}
|
||||
const data = {
|
||||
[watcherRoom[0]]: watcherRoom[1].watcher.roomRessourcesData
|
||||
};
|
||||
return generateMetricsReport(data, metrics);
|
||||
}
|
||||
|
||||
function cleanup() {
|
||||
for (const { teacher, watcher, students } of rooms.values()) {
|
||||
[teacher, watcher, ...students].forEach(p => p?.disconnect());
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
try {
|
||||
const token = await attemptLoginOrRegister(config.baseUrl, config.auth.username, config.auth.password);
|
||||
if (!token) throw new Error('Authentication failed');
|
||||
|
||||
console.log('Creating rooms...');
|
||||
const roomIds = await Promise.all(
|
||||
Array.from({ length: config.rooms.count }, (_, i) => setupRoom(token, i))
|
||||
);
|
||||
|
||||
console.log('Connecting participants...');
|
||||
await Promise.all(roomIds.filter(Boolean).map(connectParticipants));
|
||||
|
||||
console.log('Retrieving baseline metrics...');
|
||||
await new Promise(resolve => setTimeout(resolve, 10000));
|
||||
|
||||
console.log('Starting simulation across all rooms...');
|
||||
await simulate();
|
||||
|
||||
console.log('Simulation complete. Waiting for system stabilization...');
|
||||
await new Promise(resolve => setTimeout(resolve, 10000));
|
||||
|
||||
console.log('Generating final report...');
|
||||
const folderName = await generateReport();
|
||||
console.log(`Metrics report generated in ${folderName.outputDir}`);
|
||||
|
||||
console.log('All done!');
|
||||
} catch (error) {
|
||||
metrics.logError('main', error);
|
||||
console.error('Error:', error.message);
|
||||
} finally {
|
||||
cleanup();
|
||||
}
|
||||
}
|
||||
|
||||
['SIGINT', 'exit', 'uncaughtException', 'unhandledRejection'].forEach(event => {
|
||||
process.on(event, cleanup);
|
||||
});
|
||||
|
||||
main();
|
||||
1230
test/stressTest/package-lock.json
generated
Normal file
1230
test/stressTest/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
22
test/stressTest/package.json
Normal file
22
test/stressTest/package.json
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
{
|
||||
"name": "stresstest",
|
||||
"version": "1.0.0",
|
||||
"description": "main.js",
|
||||
"type": "module",
|
||||
"main": "main.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"axios": "^1.7.7",
|
||||
"chart.js": "^3.9.1",
|
||||
"chartjs-node-canvas": "^4.1.6",
|
||||
"dockerode": "^4.0.2",
|
||||
"dotenv": "^16.4.5",
|
||||
"p-limit": "^6.1.0",
|
||||
"socket.io": "^4.8.1",
|
||||
"socket.io-client": "^4.8.1"
|
||||
}
|
||||
}
|
||||
77
test/stressTest/utility/apiServices.js
Normal file
77
test/stressTest/utility/apiServices.js
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
import axios from "axios";
|
||||
|
||||
// Logs in a user.
|
||||
async function login(baseUrl, email, password) {
|
||||
if (!email || !password) throw new Error("Email and password are required.");
|
||||
|
||||
try {
|
||||
const res = await axios.post(`${baseUrl}/api/user/login`, { email, password }, {
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
|
||||
if (res.status === 200 && res.data.token) {
|
||||
console.log(`Login successful for ${email}`);
|
||||
return res.data.token;
|
||||
}
|
||||
throw new Error(`Login failed. Status: ${res.status}`);
|
||||
} catch (error) {
|
||||
console.error(`Login error for ${email}:`, error.message);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Registers a new user.
|
||||
async function register(baseUrl, email, password) {
|
||||
if (!email || !password) throw new Error("Email and password are required.");
|
||||
|
||||
try {
|
||||
const res = await axios.post(`${baseUrl}/api/user/register`, { email, password }, {
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
|
||||
if (res.status === 200) {
|
||||
console.log(`Registration successful for ${email}`);
|
||||
return res.data.message || "Registration completed successfully.";
|
||||
}
|
||||
throw new Error(`Registration failed. Status: ${res.status}`);
|
||||
} catch (error) {
|
||||
console.error(`Registration error for ${email}:`, error.message);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Attempts to log in a user, or registers and logs in if the login fails.
|
||||
export async function attemptLoginOrRegister(baseUrl, username, password) {
|
||||
try {
|
||||
return await login(baseUrl, username, password);
|
||||
} catch (loginError) {
|
||||
console.log(`Login failed for ${username}. Attempting registration...`);
|
||||
try {
|
||||
await register(baseUrl, username, password);
|
||||
return await login(baseUrl, username, password);
|
||||
} catch (registerError) {
|
||||
console.error(`Registration and login failed for ${username}:`, registerError.message);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Creates a new room
|
||||
export async function createRoomContainer(baseUrl, token) {
|
||||
if (!token) throw new Error("Authorization token is required.");
|
||||
|
||||
try {
|
||||
const res = await axios.post(`${baseUrl}/api/room`, {}, {
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (res.status === 200) return res.data;
|
||||
throw new Error(`Room creation failed. Status: ${res.status}`);
|
||||
} catch (error) {
|
||||
console.error("Room creation error:", error.message);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
253
test/stressTest/utility/metrics_generator.js
Normal file
253
test/stressTest/utility/metrics_generator.js
Normal file
|
|
@ -0,0 +1,253 @@
|
|||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { ChartJSNodeCanvas } from 'chartjs-node-canvas';
|
||||
|
||||
async function saveMetricsSummary(metrics, baseOutputDir) {
|
||||
const metricsData = metrics.getSummary();
|
||||
|
||||
// Save as JSON
|
||||
fs.writeFileSync(
|
||||
path.join(baseOutputDir, 'metrics-summary.json'),
|
||||
JSON.stringify(metricsData, null, 2)
|
||||
);
|
||||
|
||||
// Save as formatted text
|
||||
const textSummary = `
|
||||
Load Test Summary
|
||||
================
|
||||
|
||||
Rooms
|
||||
-----
|
||||
Created: ${metricsData.rooms.created}
|
||||
Failed: ${metricsData.rooms.failed}
|
||||
Total: ${metricsData.rooms.total}
|
||||
|
||||
Users
|
||||
-----
|
||||
Connected: ${metricsData.users.connected}
|
||||
Failed: ${metricsData.users.failed}
|
||||
Total: ${metricsData.users.total}
|
||||
|
||||
Messages
|
||||
--------
|
||||
Attempted: ${metricsData.messages.attempted}
|
||||
Sent: ${metricsData.messages.sent}
|
||||
Received: ${metricsData.messages.received}
|
||||
|
||||
Errors by Category
|
||||
----------------
|
||||
${Object.entries(metricsData.errors)
|
||||
.map(([category, count]) => `${category}: ${count}`)
|
||||
.join('\n')}
|
||||
`;
|
||||
|
||||
fs.writeFileSync(
|
||||
path.join(baseOutputDir, 'metrics-summary.txt'),
|
||||
textSummary.trim()
|
||||
);
|
||||
}
|
||||
|
||||
// Common chart configurations
|
||||
const CHART_CONFIG = {
|
||||
width: 800,
|
||||
height: 400,
|
||||
chartStyles: {
|
||||
memory: {
|
||||
borderColor: 'blue',
|
||||
backgroundColor: 'rgba(54, 162, 235, 0.2)'
|
||||
},
|
||||
memoryPercent: {
|
||||
borderColor: 'green',
|
||||
backgroundColor: 'rgba(75, 192, 192, 0.2)'
|
||||
},
|
||||
cpu: {
|
||||
borderColor: 'red',
|
||||
backgroundColor: 'rgba(255, 99, 132, 0.2)'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const createBaseChartConfig = (labels, dataset, xLabel, yLabel) => ({
|
||||
type: 'line',
|
||||
data: {
|
||||
labels,
|
||||
datasets: [dataset]
|
||||
},
|
||||
options: {
|
||||
scales: {
|
||||
x: { title: { display: true, text: xLabel }},
|
||||
y: { title: { display: true, text: yLabel }}
|
||||
},
|
||||
plugins: {
|
||||
legend: { display: true, position: 'top' }
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
function ensureDirectoryExists(directory) {
|
||||
!fs.existsSync(directory) && fs.mkdirSync(directory, { recursive: true });
|
||||
}
|
||||
|
||||
async function generateMetricsChart(chartJSNodeCanvas, data, style, label, timeLabels, metric, outputPath) {
|
||||
const dataset = {
|
||||
label,
|
||||
data: data.map(m => m[metric] || 0),
|
||||
...CHART_CONFIG.chartStyles[style],
|
||||
fill: true,
|
||||
tension: 0.4
|
||||
};
|
||||
|
||||
const buffer = await chartJSNodeCanvas.renderToBuffer(
|
||||
createBaseChartConfig(timeLabels, dataset, 'Time', label)
|
||||
);
|
||||
|
||||
return fs.promises.writeFile(outputPath, buffer);
|
||||
}
|
||||
|
||||
async function generateContainerCharts(chartJSNodeCanvas, containerData, outputDir) {
|
||||
const timeLabels = containerData.metrics.map(m =>
|
||||
new Date(m.timestamp).toLocaleTimeString()
|
||||
);
|
||||
|
||||
const chartPromises = [
|
||||
generateMetricsChart(
|
||||
chartJSNodeCanvas,
|
||||
containerData.metrics,
|
||||
'memory',
|
||||
`${containerData.containerName} Memory (MB)`,
|
||||
timeLabels,
|
||||
'memoryUsedMB',
|
||||
path.join(outputDir, 'memory-usage-mb.png')
|
||||
),
|
||||
generateMetricsChart(
|
||||
chartJSNodeCanvas,
|
||||
containerData.metrics,
|
||||
'memoryPercent',
|
||||
`${containerData.containerName} Memory %`,
|
||||
timeLabels,
|
||||
'memoryUsedPercentage',
|
||||
path.join(outputDir, 'memory-usage-percent.png')
|
||||
),
|
||||
generateMetricsChart(
|
||||
chartJSNodeCanvas,
|
||||
containerData.metrics,
|
||||
'cpu',
|
||||
`${containerData.containerName} CPU %`,
|
||||
timeLabels,
|
||||
'cpuUsedPercentage',
|
||||
path.join(outputDir, 'cpu-usage.png')
|
||||
)
|
||||
];
|
||||
|
||||
await Promise.all(chartPromises);
|
||||
}
|
||||
|
||||
async function processSummaryMetrics(containers, timeLabels) {
|
||||
return timeLabels.map((_, timeIndex) => ({
|
||||
memoryUsedMB: containers.reduce((sum, container) =>
|
||||
sum + (container.metrics[timeIndex]?.memoryUsedMB || 0), 0),
|
||||
memoryUsedPercentage: containers.reduce((sum, container) =>
|
||||
sum + (container.metrics[timeIndex]?.memoryUsedPercentage || 0), 0),
|
||||
cpuUsedPercentage: containers.reduce((sum, container) =>
|
||||
sum + (container.metrics[timeIndex]?.cpuUsedPercentage || 0), 0)
|
||||
}));
|
||||
}
|
||||
|
||||
export default async function generateMetricsReport(allRoomsData, testMetrics) {
|
||||
try {
|
||||
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
||||
const baseOutputDir = `./output/${timestamp}`;
|
||||
const dirs = [
|
||||
baseOutputDir,
|
||||
path.join(baseOutputDir, 'all-containers'),
|
||||
path.join(baseOutputDir, 'all-rooms')
|
||||
];
|
||||
|
||||
dirs.forEach(ensureDirectoryExists);
|
||||
|
||||
if (testMetrics) {
|
||||
await saveMetricsSummary(testMetrics, baseOutputDir);
|
||||
}
|
||||
|
||||
const chartJSNodeCanvas = new ChartJSNodeCanvas(CHART_CONFIG);
|
||||
const allContainers = Object.values(allRoomsData).flat();
|
||||
const roomContainers = allContainers.filter(c =>
|
||||
c.containerName.startsWith('room_')
|
||||
);
|
||||
|
||||
if (allContainers.length > 0) {
|
||||
const timeLabels = allContainers[0].metrics.map(m =>
|
||||
new Date(m.timestamp).toLocaleTimeString()
|
||||
);
|
||||
const summedMetrics = await processSummaryMetrics(allContainers, timeLabels);
|
||||
await generateContainerSummaryCharts(
|
||||
chartJSNodeCanvas,
|
||||
summedMetrics,
|
||||
timeLabels,
|
||||
path.join(baseOutputDir, 'all-containers')
|
||||
);
|
||||
}
|
||||
|
||||
if (roomContainers.length > 0) {
|
||||
const timeLabels = roomContainers[0].metrics.map(m =>
|
||||
new Date(m.timestamp).toLocaleTimeString()
|
||||
);
|
||||
const summedMetrics = await processSummaryMetrics(roomContainers, timeLabels);
|
||||
await generateContainerSummaryCharts(
|
||||
chartJSNodeCanvas,
|
||||
summedMetrics,
|
||||
timeLabels,
|
||||
path.join(baseOutputDir, 'all-rooms')
|
||||
);
|
||||
}
|
||||
|
||||
// Process individual containers
|
||||
const containerPromises = Object.values(allRoomsData)
|
||||
.flat()
|
||||
.filter(container => !container.containerName.startsWith('room_'))
|
||||
.map(async containerData => {
|
||||
const containerDir = path.join(baseOutputDir, containerData.containerName);
|
||||
ensureDirectoryExists(containerDir);
|
||||
await generateContainerCharts(chartJSNodeCanvas, containerData, containerDir);
|
||||
});
|
||||
|
||||
await Promise.all(containerPromises);
|
||||
|
||||
return { outputDir: baseOutputDir };
|
||||
} catch (error) {
|
||||
console.error('Error generating metrics report:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async function generateContainerSummaryCharts(chartJSNodeCanvas, metrics, timeLabels, outputDir) {
|
||||
await Promise.all([
|
||||
generateMetricsChart(
|
||||
chartJSNodeCanvas,
|
||||
metrics,
|
||||
'memory',
|
||||
'Total Memory (MB)',
|
||||
timeLabels,
|
||||
'memoryUsedMB',
|
||||
path.join(outputDir, 'total-memory-usage-mb.png')
|
||||
),
|
||||
generateMetricsChart(
|
||||
chartJSNodeCanvas,
|
||||
metrics,
|
||||
'memoryPercent',
|
||||
'Total Memory %',
|
||||
timeLabels,
|
||||
'memoryUsedPercentage',
|
||||
path.join(outputDir, 'total-memory-usage-percent.png')
|
||||
),
|
||||
generateMetricsChart(
|
||||
chartJSNodeCanvas,
|
||||
metrics,
|
||||
'cpu',
|
||||
'Total CPU %',
|
||||
timeLabels,
|
||||
'cpuUsedPercentage',
|
||||
path.join(outputDir, 'total-cpu-usage.png')
|
||||
)
|
||||
]);
|
||||
}
|
||||
Loading…
Reference in a new issue