Major changes: logic add to panel, local variables, panel histor, users management...
This commit is contained in:
@@ -508,6 +508,11 @@ func (e *EPICS) ListSignals(_ context.Context) ([]datasource.Metadata, error) {
|
||||
// Write puts a new value onto a CA channel.
|
||||
// If the signal is not currently subscribed (e.g. a button in oneshot mode),
|
||||
// a temporary CA channel is created, used for the put, then torn down.
|
||||
//
|
||||
// NOTE: per-session end-user identity (datasource.WithUser) is NOT honoured in
|
||||
// the CGo/libca build: libca uses a single process-wide CA context whose client
|
||||
// name is fixed at startup. Writes therefore use the server identity here. Use
|
||||
// the default pure-Go build for per-user write attribution.
|
||||
func (e *EPICS) Write(ctx context.Context, signal string, value any) error {
|
||||
e.attachCAContext()
|
||||
|
||||
|
||||
@@ -39,8 +39,29 @@ type EPICS struct {
|
||||
|
||||
mu sync.RWMutex
|
||||
metadata map[string]datasource.Metadata
|
||||
|
||||
// Per-user CA clients for writes. EPICS Channel Access carries the client
|
||||
// username at the circuit (TCP) level, so attributing a write to a specific
|
||||
// end-user requires a dedicated client whose ClientName is that user. These
|
||||
// are created lazily on first write by each user and evicted when idle.
|
||||
ctx context.Context // datasource lifetime; governs per-user clients
|
||||
baseCfg ca.Config // template config for per-user clients
|
||||
selfName string // the server's own CA client name
|
||||
userMu sync.Mutex
|
||||
userClients map[string]*userClient
|
||||
}
|
||||
|
||||
// userClient is a per-end-user CA client and its last-use time for idle eviction.
|
||||
type userClient struct {
|
||||
cli *ca.Client
|
||||
lastUsed time.Time
|
||||
}
|
||||
|
||||
// userClientIdleTTL is how long an idle per-user CA client is kept before being
|
||||
// closed. Writes are bursty, so a few minutes avoids reconnecting on every put
|
||||
// without holding circuits open indefinitely.
|
||||
const userClientIdleTTL = 10 * time.Minute
|
||||
|
||||
// New creates a new EPICS Channel Access data source.
|
||||
//
|
||||
// caAddrList is the EPICS_CA_ADDR_LIST value (space-separated addresses).
|
||||
@@ -79,7 +100,12 @@ func (e *EPICS) Connect(ctx context.Context) error {
|
||||
return fmt.Errorf("epics: %w", err)
|
||||
}
|
||||
e.client = cli
|
||||
slog.Info("epics: CA client started", "addrs", cfg.AddrList)
|
||||
e.ctx = ctx
|
||||
e.baseCfg = cfg
|
||||
e.selfName = cfg.ClientName
|
||||
e.userClients = make(map[string]*userClient)
|
||||
go e.evictIdleUserClients(ctx)
|
||||
slog.Info("epics: CA client started", "addrs", cfg.AddrList, "client_name", cfg.ClientName)
|
||||
|
||||
// Perform background sync from Channel Finder if configured.
|
||||
if e.cfURL != "" && e.autoSyncFilter != "" {
|
||||
@@ -344,13 +370,70 @@ func (e *EPICS) ListSignals(_ context.Context) ([]datasource.Metadata, error) {
|
||||
|
||||
// Write puts a new value onto the named CA channel.
|
||||
// value must be one of: float64, float32, int64, int32, int, int16, string, bool.
|
||||
//
|
||||
// When the request context carries an end-user identity (see datasource.WithUser)
|
||||
// that differs from the server's own CA client name, the put is performed on a
|
||||
// dedicated CA client whose ClientName is that user, so the IOC's access-security
|
||||
// rules see the actual end-user rather than the server process.
|
||||
func (e *EPICS) Write(ctx context.Context, signal string, value any) error {
|
||||
if err := e.client.Put(ctx, signal, value); err != nil {
|
||||
cli := e.client
|
||||
if user, ok := datasource.UserFrom(ctx); ok && user != e.selfName {
|
||||
uc, err := e.clientForUser(user)
|
||||
if err != nil {
|
||||
return fmt.Errorf("epics: write %q as %q: %w", signal, user, err)
|
||||
}
|
||||
cli = uc
|
||||
}
|
||||
if err := cli.Put(ctx, signal, value); err != nil {
|
||||
return fmt.Errorf("epics: write %q: %w", signal, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// clientForUser returns a CA client whose ClientName is user, creating and
|
||||
// caching one on first use. The client is bound to the datasource lifetime ctx,
|
||||
// not the per-request ctx, so it survives across writes until evicted when idle.
|
||||
func (e *EPICS) clientForUser(user string) (*ca.Client, error) {
|
||||
e.userMu.Lock()
|
||||
defer e.userMu.Unlock()
|
||||
if uc, ok := e.userClients[user]; ok {
|
||||
uc.lastUsed = time.Now()
|
||||
return uc.cli, nil
|
||||
}
|
||||
cfg := e.baseCfg
|
||||
cfg.ClientName = user
|
||||
cli, err := ca.NewClient(e.ctx, cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
e.userClients[user] = &userClient{cli: cli, lastUsed: time.Now()}
|
||||
slog.Info("epics: created per-user CA client", "user", user)
|
||||
return cli, nil
|
||||
}
|
||||
|
||||
// evictIdleUserClients closes per-user CA clients that have not been used within
|
||||
// userClientIdleTTL. It runs until ctx is cancelled.
|
||||
func (e *EPICS) evictIdleUserClients(ctx context.Context) {
|
||||
t := time.NewTicker(time.Minute)
|
||||
defer t.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case now := <-t.C:
|
||||
e.userMu.Lock()
|
||||
for user, uc := range e.userClients {
|
||||
if now.Sub(uc.lastUsed) > userClientIdleTTL {
|
||||
uc.cli.Close()
|
||||
delete(e.userClients, user)
|
||||
slog.Info("epics: evicted idle per-user CA client", "user", user)
|
||||
}
|
||||
}
|
||||
e.userMu.Unlock()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------- //
|
||||
// History //
|
||||
// -------------------------------------------------------------------------- //
|
||||
|
||||
Reference in New Issue
Block a user