From 4d574d5fdf6143375e869ac3fda3e2df43b39e8c Mon Sep 17 00:00:00 2001 From: Liosha Date: Mon, 11 May 2026 13:10:00 +0300 Subject: [PATCH] adjast wss + docker logs --- build.gradle | 8 ++ jambotron-ui/package-lock.json | 133 +++++++++++++++++- jambotron-ui/package.json | 3 + .../project-details.component.ts | 108 ++++++++------ .../src/app/services/project.service.ts | 45 +++++- .../jambotron/WebSocket/WebSocketConfig.java | 29 ++-- .../jambotron/console/AsyncConfig.java | 9 ++ .../jambotron/console/DockerLogService.java | 38 +++++ .../jambotron/controllers/LogController.java | 21 +++ 9 files changed, 327 insertions(+), 67 deletions(-) create mode 100644 src/main/java/com/jambotronGroup/jambotron/console/AsyncConfig.java create mode 100644 src/main/java/com/jambotronGroup/jambotron/console/DockerLogService.java create mode 100644 src/main/java/com/jambotronGroup/jambotron/controllers/LogController.java diff --git a/build.gradle b/build.gradle index 494e896..ad8d537 100644 --- a/build.gradle +++ b/build.gradle @@ -31,9 +31,17 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-data-mongodb-reactive' // --- НОВОЕ: Docker API для управления контейнерами (Созидание) --- + // Ядро и API + implementation 'com.github.docker-java:docker-java-api:3.3.6' implementation 'com.github.docker-java:docker-java-core:3.3.6' + + // Транспорт (HTTP Client 5) — именно он нужен для DockerConfig.java implementation 'com.github.docker-java:docker-java-transport-httpclient5:3.3.6' + // Spring Boot Starter для STOMP WebSockets + implementation 'org.springframework.boot:spring-boot-starter-websocket' + + // Работа с SQL и PostgreSQL[cite: 3] implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.postgresql:postgresql:42.6.0' diff --git a/jambotron-ui/package-lock.json b/jambotron-ui/package-lock.json index 389630d..1d351d5 100644 --- a/jambotron-ui/package-lock.json +++ b/jambotron-ui/package-lock.json @@ -22,6 +22,7 @@ "@material/material-color-utilities": "^0.3.0", "@ng-bootstrap/ng-bootstrap": "19.0.0", "@popperjs/core": "2.11.8", + "@stomp/stompjs": "^7.3.0", "angular-markdown-editor": "^3.1.1", "bootstrap": "5.3.6", "marked": "^18.0.3", @@ -31,6 +32,7 @@ "ngx-markdown": "^20.0.0", "prismjs": "^1.30.0", "rxjs": "~7.8.0", + "sockjs-client": "^1.6.1", "tslib": "^2.3.0", "viewerjs": "^1.11.7", "zone.js": "~0.15.0" @@ -44,6 +46,7 @@ "@types/jasmine": "~5.1.0", "@types/marked": "^5.0.2", "@types/node": "^20.17.19", + "@types/sockjs-client": "^1.5.4", "jasmine-core": "~5.7.0", "karma": "~6.4.0", "karma-chrome-launcher": "~3.2.0", @@ -3986,6 +3989,12 @@ "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", "license": "MIT" }, + "node_modules/@stomp/stompjs": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/@stomp/stompjs/-/stompjs-7.3.0.tgz", + "integrity": "sha512-nKMLoFfJhrQAqkvvKd1vLq/cVBGCMwPRCD0LqW7UT1fecRx9C3GoKEIR2CYwVuErGeZu8w0kFkl2rlhPlqHVgQ==", + "license": "Apache-2.0" + }, "node_modules/@tufjs/canonical-json": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@tufjs/canonical-json/-/canonical-json-2.0.0.tgz", @@ -4487,6 +4496,13 @@ "@types/node": "*" } }, + "node_modules/@types/sockjs-client": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/@types/sockjs-client/-/sockjs-client-1.5.4.tgz", + "integrity": "sha512-zk+uFZeWyvJ5ZFkLIwoGA/DfJ+pYzcZ8eH4H/EILCm2OBZyHH6Hkdna1/UWL/CFruh5wj6ES7g75SvUB0VsH5w==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/trusted-types": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", @@ -6692,6 +6708,18 @@ ], "license": "BSD-3-Clause" }, + "node_modules/faye-websocket": { + "version": "0.11.4", + "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz", + "integrity": "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==", + "license": "Apache-2.0", + "dependencies": { + "websocket-driver": ">=0.5.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, "node_modules/fdir": { "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", @@ -7166,6 +7194,12 @@ "url": "https://opencollective.com/express" } }, + "node_modules/http-parser-js": { + "version": "0.5.10", + "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.10.tgz", + "integrity": "sha512-Pysuw9XpUq5dVc/2SMHpuTY01RFl8fttgcyunjL7eEMhGM3cI4eOmiCycJDVCo/7O7ClfQD3SaI6ftDzqOXYMA==", + "license": "MIT" + }, "node_modules/http-proxy": { "version": "1.18.1", "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz", @@ -7301,7 +7335,6 @@ "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true, "license": "ISC" }, "node_modules/ini": { @@ -8774,7 +8807,6 @@ "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, "license": "MIT" }, "node_modules/msgpackr": { @@ -9657,6 +9689,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", + "license": "MIT" + }, "node_modules/range-parser": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", @@ -9728,7 +9766,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", - "dev": true, "license": "MIT" }, "node_modules/restore-cursor": { @@ -9912,6 +9949,26 @@ "tslib": "^2.1.0" } }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/safe-regex-test": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", @@ -10311,6 +10368,43 @@ "node": ">= 0.6" } }, + "node_modules/sockjs-client": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/sockjs-client/-/sockjs-client-1.6.1.tgz", + "integrity": "sha512-2g0tjOR+fRs0amxENLi/q5TiJTqY+WXFOzb5UwXndlK6TO3U/mirZznpx6w34HVMoc3g7cY24yC/ZMIYnDlfkw==", + "license": "MIT", + "dependencies": { + "debug": "^3.2.7", + "eventsource": "^2.0.2", + "faye-websocket": "^0.11.4", + "inherits": "^2.0.4", + "url-parse": "^1.5.10" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://tidelift.com/funding/github/npm/sockjs-client" + } + }, + "node_modules/sockjs-client/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/sockjs-client/node_modules/eventsource": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-2.0.2.tgz", + "integrity": "sha512-IzUmBGPR3+oUG9dUeXynyNmf91/3zUSJg1lCktzKw47OXuhco54U3r9B7O4XX+Rb1Itm9OZ2b0RkTs10bICOxA==", + "license": "MIT", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/socks": { "version": "2.8.8", "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.8.tgz", @@ -10777,6 +10871,16 @@ "browserslist": ">= 4.21.0" } }, + "node_modules/url-parse": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", + "license": "MIT", + "dependencies": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, "node_modules/utils-merge": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", @@ -10989,6 +11093,29 @@ "license": "MIT", "optional": true }, + "node_modules/websocket-driver": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz", + "integrity": "sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==", + "license": "Apache-2.0", + "dependencies": { + "http-parser-js": ">=0.5.1", + "safe-buffer": ">=5.1.0", + "websocket-extensions": ">=0.1.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/websocket-extensions": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz", + "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8.0" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", diff --git a/jambotron-ui/package.json b/jambotron-ui/package.json index e107d4e..42443ab 100644 --- a/jambotron-ui/package.json +++ b/jambotron-ui/package.json @@ -28,6 +28,7 @@ "@material/material-color-utilities": "^0.3.0", "@ng-bootstrap/ng-bootstrap": "19.0.0", "@popperjs/core": "2.11.8", + "@stomp/stompjs": "^7.3.0", "angular-markdown-editor": "^3.1.1", "bootstrap": "5.3.6", "marked": "^18.0.3", @@ -37,6 +38,7 @@ "ngx-markdown": "^20.0.0", "prismjs": "^1.30.0", "rxjs": "~7.8.0", + "sockjs-client": "^1.6.1", "tslib": "^2.3.0", "viewerjs": "^1.11.7", "zone.js": "~0.15.0" @@ -50,6 +52,7 @@ "@types/jasmine": "~5.1.0", "@types/marked": "^5.0.2", "@types/node": "^20.17.19", + "@types/sockjs-client": "^1.5.4", "jasmine-core": "~5.7.0", "karma": "~6.4.0", "karma-chrome-launcher": "~3.2.0", diff --git a/jambotron-ui/src/app/modules/user-module/project-details.component/project-details.component.ts b/jambotron-ui/src/app/modules/user-module/project-details.component/project-details.component.ts index d119dcb..e97183f 100644 --- a/jambotron-ui/src/app/modules/user-module/project-details.component/project-details.component.ts +++ b/jambotron-ui/src/app/modules/user-module/project-details.component/project-details.component.ts @@ -1,13 +1,14 @@ +// project-details.component.ts import { Component, OnInit, OnDestroy, ViewChild, ElementRef, AfterViewChecked } from '@angular/core'; -import {ActivatedRoute, Router} from '@angular/router'; +import { ActivatedRoute, Router } from '@angular/router'; import { CommonModule } from '@angular/common'; -import { Subscription, timer, switchMap } from 'rxjs'; +import { Subscription, timer, switchMap, catchError, of } from 'rxjs'; import { ProjectService, Project, DockerContainer, ProjectDetailResponse } from '../../../services/project.service'; import { MatIconModule } from '@angular/material/icon'; import { MatButtonModule } from '@angular/material/button'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import { MatDividerModule } from '@angular/material/divider'; -import {MatCard, MatCardContent} from '@angular/material/card'; +import { MatCard, MatCardContent } from '@angular/material/card'; @Component({ selector: 'app-project-details', @@ -25,16 +26,16 @@ import {MatCard, MatCardContent} from '@angular/material/card'; styleUrls: ['./project-details.component.scss'] }) export class ProjectDetailsComponent implements OnInit, OnDestroy, AfterViewChecked { - @ViewChild('consoleScroll') private consoleContainer!: ElementRef; // Привязка к #consoleScroll + @ViewChild('consoleScroll') private consoleContainer!: ElementRef; // Прив'язка до вікна терміналу project?: Project; config?: any; containers: DockerContainer[] = []; - logs: string[] = []; + logs: string[] = []; // Масив для зберігання рядків логів loading = true; - private pollingSub?: Subscription; - private logSub?: Subscription; + private pollingSub?: Subscription; // Підписка на моніторинг контейнерів + private logSub?: Subscription; // Підписка на потік логів через STOMP constructor( private route: ActivatedRoute, @@ -50,46 +51,90 @@ export class ProjectDetailsComponent implements OnInit, OnDestroy, AfterViewChec } } + // Завантаження основних даних проекту private loadData(id: string): void { this.projectService.getProjectDetails(id).subscribe({ next: (data: ProjectDetailResponse) => { this.project = data.project; this.config = data.config; this.loading = false; - this.connectLogs(id); }, error: (err) => { - console.error('Ошибка загрузки данных проекта:', err); + console.error('Помилка завантаження проекту:', err); this.loading = false; } }); } + // Опитування стану контейнерів кожні 5 секунд private startMonitoring(id: string): void { - // Опрос состояния контейнеров каждые 5 секунд[cite: 5, 6] this.pollingSub = timer(0, 5000).pipe( - switchMap(() => this.projectService.getProjectContainers(id)) + switchMap(() => this.projectService.getProjectContainers(id).pipe( + catchError(() => of([])) // У разі помилки повертаємо порожній список + )) ).subscribe({ - next: (data) => this.containers = data, - error: (err) => console.error('Ошибка мониторинга контейнеров:', err) + next: (data: DockerContainer[]) => { + this.containers = data; + + // Якщо з'явилися контейнери і ми ще не підключені до логів — підключаємося + if (this.containers.length > 0 && !this.logSub) { + const mainContainer = this.containers.find(c => c.state === 'running') || this.containers[0]; + this.connectLogs(mainContainer.id); + } + }, + error: (err) => console.error('Помилка моніторингу:', err) }); } - private connectLogs(id: string): void { + // Підключення до трансляції логів + connectLogs(id: string): void { + console.log(`Підключення до логів контейнера: ${id}`); + + // Очищаємо попередню підписку перед створенням нової + if (this.logSub) { + this.logSub.unsubscribe(); + this.projectService.disconnectLogs(); + } + + this.logs = []; // Очищаємо консоль при зміні контейнера + this.logSub = this.projectService.connectToDockerLogs(id).subscribe({ - next: (msg) => this.logs.push(msg), - error: (err) => console.error('WS Error:', err) + next: (msg) => { + this.logs.push(msg); + // Обмежуємо кількість рядків у пам'яті (останні 200) + if (this.logs.length > 200) this.logs.shift(); + }, + error: (err) => console.error('STOMP Error:', err) }); } deploy(): void { - if (this.project) this.projectService.deployProject(this.project.id).subscribe(); + if (this.project) { + this.loading = true; + this.projectService.deployProject(this.project.id).subscribe({ + next: () => this.loading = false, + error: () => this.loading = false + }); + } } stop(): void { if (this.project) this.projectService.stopProject(this.project.id).subscribe(); } + confirmDelete() { + if (confirm("Знищити проект назавжди?")) { + this.pollingSub?.unsubscribe(); // Зупиняємо оновлення інтерфейсу + + if (this.project) { + this.projectService.deleteProject(this.project.id).subscribe({ + next: () => this.router.navigate(['/main/user/projects']), + error: () => this.router.navigate(['/main/user/projects']) + }); + } + } + } + ngAfterViewChecked(): void { this.scrollToBottom(); } @@ -103,44 +148,23 @@ export class ProjectDetailsComponent implements OnInit, OnDestroy, AfterViewChec ngOnDestroy(): void { this.pollingSub?.unsubscribe(); this.logSub?.unsubscribe(); + this.projectService.disconnectLogs(); // Закриваємо з'єднання з брокером } - confirmDelete() { - if (confirm("Уничтожить проект навсегда?")) { - // 1. Сначала «ослепляем» компонент, чтобы он перестал слать запросы - this.pollingSub?.unsubscribe(); // Твой метод, который делает subscription.unsubscribe() - - // 2. Только теперь отправляем команду на ликвидацию - if (this.project) this.projectService.deleteProject(this.project.id).subscribe({ - next: () => { - console.log("Проект стерт, уходим на дашборд"); - this.router.navigate(['/main/user/projects']); - }, - error: (err) => { - console.error("Ошибка при удалении, но проект мог уже исчезнуть", err); - // Всё равно уходим, так как оставаться на странице 404 нет смысла - this.router.navigate(['/main/user/projects']); - } - }); - } - } - // Метод для окрашивания логов в терминале + // Форматування для відображення кольорів у терміналі formatLogLine(line: string): string { if (!line) return ''; - - // Окрашиваем ключевые слова для лучшей читаемости let formatted = line; if (line.includes('ERROR') || line.includes('Error') || line.includes('failed')) { formatted = `${line}`; - } else if (line.includes('WARN') || line.includes('Warning') || line.includes('Conflict')) { + } else if (line.includes('WARN') || line.includes('Warning')) { formatted = `${line}`; - } else if (line.includes('INFO') || line.includes('success') || line.includes('Started')) { + } else if (line.includes('INFO') || line.includes('success')) { formatted = `${line}`; } else if (line.includes('DEBUG')) { formatted = `${line}`; } - return formatted; } } diff --git a/jambotron-ui/src/app/services/project.service.ts b/jambotron-ui/src/app/services/project.service.ts index 7029213..911c69c 100644 --- a/jambotron-ui/src/app/services/project.service.ts +++ b/jambotron-ui/src/app/services/project.service.ts @@ -1,8 +1,10 @@ import { Injectable } from '@angular/core'; import { HttpClient } from '@angular/common/http'; -import { Observable } from 'rxjs'; +import {Observable, Subject} from 'rxjs'; import { GlobalConstants } from '../global-constants'; -import { webSocket, WebSocketSubject } from 'rxjs/webSocket'; + +import SockJS from 'sockjs-client'; +import { Client, IMessage } from '@stomp/stompjs'; // Унифицируем название интерфейса для списка контейнеров export interface DockerContainer { @@ -44,6 +46,7 @@ export interface ProjectDetailResponse { export class ProjectService { // Используем baseUrl, как определено в начале класса private baseUrl = GlobalConstants.API_URL; + private stompClient: Client | null = null; constructor(private http: HttpClient) {} @@ -80,12 +83,40 @@ export class ProjectService { } // WebSocket для трансляции логов в реальном времени[cite: 3, 5] - public connectToDockerLogs(projectId: string): WebSocketSubject { - const socketUrl = this.baseUrl.replace('http', 'ws') + '/ws-jambotron/' + projectId; - return webSocket({ - url: socketUrl, - deserializer: (msg) => msg.data + public connectToDockerLogs(containerId: string): Observable { + const logSubject = new Subject(); + + // Твой эндпоинт из WebSocketConfig.java + const socket = new SockJS(`${this.baseUrl}/ws-jambotron`); + + this.stompClient = new Client({ + webSocketFactory: () => socket, + debug: (str) => console.log(str), + onConnect: () => { + // 1. Подписываемся на логи конкретного контейнера + this.stompClient?.subscribe(`/topic/logs/${containerId}`, (message: IMessage) => { + logSubject.next(message.body); + }); + + // 2. Отправляем сигнал бэкенду начать стриминг (согласно LogController) + this.stompClient?.publish({ + destination: '/app/start-logs', + body: containerId + }); + }, + onStompError: (frame) => { + logSubject.error('Broker error: ' + frame.headers['message']); + } }); + + this.stompClient.activate(); + return logSubject.asObservable(); + } + + public disconnectLogs(): void { + if (this.stompClient) { + this.stompClient.deactivate(); + } } // ИСПРАВЛЕНО: возвращаем DockerContainer и используем правильный baseUrl[cite: 5] diff --git a/src/main/java/com/jambotronGroup/jambotron/WebSocket/WebSocketConfig.java b/src/main/java/com/jambotronGroup/jambotron/WebSocket/WebSocketConfig.java index 91ecac6..96f021c 100644 --- a/src/main/java/com/jambotronGroup/jambotron/WebSocket/WebSocketConfig.java +++ b/src/main/java/com/jambotronGroup/jambotron/WebSocket/WebSocketConfig.java @@ -1,27 +1,26 @@ package com.jambotronGroup.jambotron.WebSocket; import org.springframework.context.annotation.Configuration; -import org.springframework.web.socket.config.annotation.EnableWebSocket; -import org.springframework.web.socket.config.annotation.WebSocketConfigurer; -import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry; +import org.springframework.messaging.simp.config.MessageBrokerRegistry; +import org.springframework.web.socket.config.annotation.*; @Configuration -@EnableWebSocket -public class WebSocketConfig implements WebSocketConfigurer { +@EnableWebSocketMessageBroker +public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { - private final DockerLogsHandler dockerLogsHandler; - - public WebSocketConfig(DockerLogsHandler dockerLogsHandler) { - this.dockerLogsHandler = dockerLogsHandler; + @Override + public void configureMessageBroker(MessageBrokerRegistry config) { + // Брокер для сообщений клиенту + config.enableSimpleBroker("/topic"); + // Префикс для запросов от клиента к серверу + config.setApplicationDestinationPrefixes("/app"); } @Override - public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { - registry.addHandler(dockerLogsHandler, "/api/ws-jambotron/{projectId}") - // Поскольку есть proxy.conf.json, запросы приходят как Same-Origin. - // Но для стабильности разработки лучше явно указать localhost. + public void registerStompEndpoints(StompEndpointRegistry registry) { + registry.addEndpoint("/api/ws-jambotron") .setAllowedOriginPatterns("https://*.jambotron.com", "https://jambotron.com") - - .setAllowedOrigins("http://localhost:8082", "http://localhost:4200"); + .setAllowedOrigins("http://localhost:8082", "http://localhost:4200") + .withSockJS(); // Для стабильности соединения } } \ No newline at end of file diff --git a/src/main/java/com/jambotronGroup/jambotron/console/AsyncConfig.java b/src/main/java/com/jambotronGroup/jambotron/console/AsyncConfig.java new file mode 100644 index 0000000..d7a9aa3 --- /dev/null +++ b/src/main/java/com/jambotronGroup/jambotron/console/AsyncConfig.java @@ -0,0 +1,9 @@ +package com.jambotronGroup.jambotron.console; + +import org.springframework.context.annotation.Configuration; +import org.springframework.scheduling.annotation.EnableAsync; + +@Configuration +@EnableAsync // <--- Обязательно добавь это +public class AsyncConfig { +} \ No newline at end of file diff --git a/src/main/java/com/jambotronGroup/jambotron/console/DockerLogService.java b/src/main/java/com/jambotronGroup/jambotron/console/DockerLogService.java new file mode 100644 index 0000000..f468cda --- /dev/null +++ b/src/main/java/com/jambotronGroup/jambotron/console/DockerLogService.java @@ -0,0 +1,38 @@ +package com.jambotronGroup.jambotron.console; + +import com.github.dockerjava.api.DockerClient; +import com.github.dockerjava.api.async.ResultCallback; +import com.github.dockerjava.api.model.Frame; +import org.springframework.messaging.simp.SimpMessagingTemplate; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Service; + +@Service +public class DockerLogService { + + private final SimpMessagingTemplate messagingTemplate; + private final DockerClient dockerClient; + + // Spring сам подставит сюда dockerClient из твоего DockerConfig + public DockerLogService(SimpMessagingTemplate messagingTemplate, DockerClient dockerClient) { + this.messagingTemplate = messagingTemplate; + this.dockerClient = dockerClient; + } + + @Async + public void streamLogs(String containerId) { + dockerClient.logContainerCmd(containerId) + .withStdOut(true) + .withStdErr(true) + .withFollowStream(true) + .withTail(100) + .exec(new ResultCallback.Adapter() { + @Override + public void onNext(Frame frame) { + String logLine = new String(frame.getPayload()); + // Отправляем в топик, на который подпишется фронтенд + messagingTemplate.convertAndSend("/topic/logs/" + containerId, logLine); + } + }); + } +} \ No newline at end of file diff --git a/src/main/java/com/jambotronGroup/jambotron/controllers/LogController.java b/src/main/java/com/jambotronGroup/jambotron/controllers/LogController.java new file mode 100644 index 0000000..809f5b3 --- /dev/null +++ b/src/main/java/com/jambotronGroup/jambotron/controllers/LogController.java @@ -0,0 +1,21 @@ +package com.jambotronGroup.jambotron.controllers; + +import com.jambotronGroup.jambotron.console.DockerLogService; +import org.springframework.messaging.handler.annotation.MessageMapping; +import org.springframework.stereotype.Controller; + +@Controller +public class LogController { + + private final DockerLogService dockerLogService; + + public LogController(DockerLogService dockerLogService) { + this.dockerLogService = dockerLogService; + } + + // Когда клиент шлет сообщение на /app/start-logs + @MessageMapping("/start-logs") + public void handleStartLogs(String containerId) { + dockerLogService.streamLogs(containerId); + } +} \ No newline at end of file