9737c787f9
Инфраструктура M2M (живой обмен с НРД через ИШ): - обработка M2MTransferResponse: ERROR(M2Mxx) → заявка Отклонена, сохранение ответа; INFO → ждём Decision; идемпотентность поллера - fallback-корреляция ответов с нулевым GUID (M2M14/M2M17) по FIFO - сырой XML ответа НРД в карточке заявки (для пересылки в ТП) - тестовый пакет роботу приведён к эталону m2m_robot_samples (CostInfo=Yes, 4 бумаги, IsolationStatus, DocumentSeries=сценарий); override паспорта - редирект из теста сразу в карточку заявки Мастер установки ключа Валидаты на флешку (admin/setup/keywizard): - пошаговый: загрузка .7z+пароль → выбор флешки → запись → справочник сертификатов (CRL) → перезапуск+проверка ИШ → готово - привилегированный воркер (bj-keymedia) в host-namespace через файл-обмен, bj-server остаётся в песочнице - сохранение структуры профиля архива (spr<N>), перечисление съёмных USB Прочее: - пакет-доказательство для ТП НРД + форма регистрации участника M2M - эталонные образцы робота (DOC/m2m_robot_samples) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
87 lines
2.1 KiB
Go
87 lines
2.1 KiB
Go
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"net/http"
|
|
"sync"
|
|
)
|
|
|
|
// event — одно событие, отдаваемое подписчикам через SSE.
|
|
// Type становится `event:` строкой, Data — `data:`.
|
|
type event struct {
|
|
Type string
|
|
Data string
|
|
}
|
|
|
|
// eventBus — простой fan-out для SSE. Подписчик создаётся в момент
|
|
// открытия GET /api/events и живёт до закрытия соединения.
|
|
type eventBus struct {
|
|
mu sync.Mutex
|
|
subscribers map[chan event]struct{}
|
|
}
|
|
|
|
func newEventBus() *eventBus {
|
|
return &eventBus{subscribers: make(map[chan event]struct{})}
|
|
}
|
|
|
|
func (b *eventBus) subscribe() chan event {
|
|
ch := make(chan event, 64)
|
|
b.mu.Lock()
|
|
b.subscribers[ch] = struct{}{}
|
|
b.mu.Unlock()
|
|
return ch
|
|
}
|
|
|
|
func (b *eventBus) unsubscribe(ch chan event) {
|
|
b.mu.Lock()
|
|
delete(b.subscribers, ch)
|
|
close(ch)
|
|
b.mu.Unlock()
|
|
}
|
|
|
|
func (b *eventBus) publish(e event) {
|
|
b.mu.Lock()
|
|
defer b.mu.Unlock()
|
|
for ch := range b.subscribers {
|
|
select {
|
|
case ch <- e:
|
|
default:
|
|
// Подписчик отстаёт — пропускаем (UI догонится снапшотом по GET /api/state)
|
|
}
|
|
}
|
|
}
|
|
|
|
// handleSSE — GET /api/events. Держит соединение, в каждом событии
|
|
// отдаёт event: <Type>\ndata: <Data>\n\n.
|
|
func (s *server) handleSSE(w http.ResponseWriter, r *http.Request) {
|
|
flusher, ok := w.(http.Flusher)
|
|
if !ok {
|
|
http.Error(w, "streaming not supported", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
w.Header().Set("Content-Type", "text/event-stream")
|
|
w.Header().Set("Cache-Control", "no-cache")
|
|
w.Header().Set("Connection", "keep-alive")
|
|
w.WriteHeader(http.StatusOK)
|
|
|
|
ch := s.state.bus.subscribe()
|
|
defer s.state.bus.unsubscribe(ch)
|
|
|
|
// сразу шлём snapshot, чтобы UI догнал состояние
|
|
snap := s.state.Snapshot()
|
|
fmt.Fprintf(w, "event: snapshot\ndata: %s\n\n", mustJSON(snap))
|
|
flusher.Flush()
|
|
|
|
for {
|
|
select {
|
|
case <-r.Context().Done():
|
|
return
|
|
case e := <-ch:
|
|
if _, err := fmt.Fprintf(w, "event: %s\ndata: %s\n\n", e.Type, e.Data); err != nil {
|
|
return
|
|
}
|
|
flusher.Flush()
|
|
}
|
|
}
|
|
}
|