fix(batch1): safe fixes — saveOffsets ordering, pid atomic, error handlers, go mutex, frontend cleanup

This commit is contained in:
Xen
2026-03-09 19:43:30 +00:00
parent cc65f9b5ce
commit 0b39b39f3b
4 changed files with 81 additions and 10 deletions

View File

@@ -122,6 +122,16 @@ func (p *Plugin) handleCreateSession(w http.ResponseWriter, r *http.Request) {
return
}
// Validate KV key length — Mattermost enforces a 50-char limit.
// Encoded key = kvPrefix (11 chars) + url.PathEscape(sessionKey).
// Exceeding the limit causes KVSet to silently succeed but never store data.
if len(encodeKey(req.SessionKey)) > 50 {
writeJSON(w, http.StatusBadRequest, map[string]string{
"error": fmt.Sprintf("session_key too long: encoded key length %d exceeds 50-char KV limit", len(encodeKey(req.SessionKey))),
})
return
}
// Check max active sessions
config := p.getConfiguration()
allSessions, _ := p.store.ListAllSessions()

View File

@@ -21,6 +21,9 @@ type Plugin struct {
// store wraps KV store operations for session persistence.
store *Store
// botUserIDLock synchronizes access to botUserID.
botUserIDLock sync.RWMutex
// botUserID is the plugin's bot user ID (created on activation).
botUserID string
@@ -41,7 +44,9 @@ func (p *Plugin) OnActivate() error {
if appErr != nil {
p.API.LogWarn("Failed to ensure bot user", "error", appErr.Error())
} else {
p.botUserIDLock.Lock()
p.botUserID = botID
p.botUserIDLock.Unlock()
p.API.LogInfo("Plugin bot user ensured", "botUserID", botID)
}
@@ -75,8 +80,10 @@ func (p *Plugin) sessionCleanupLoop() {
}
}
// getBotUserID returns the plugin's bot user ID.
// getBotUserID returns the plugin's bot user ID (thread-safe).
func (p *Plugin) getBotUserID() string {
p.botUserIDLock.RLock()
defer p.botUserIDLock.RUnlock()
return p.botUserID
}
@@ -93,6 +100,10 @@ func (p *Plugin) OnDeactivate() error {
p.API.LogWarn("Failed to list sessions on deactivate", "error", err.Error())
} else {
for _, s := range sessions {
// Skip sessions already in a terminal state — do not overwrite done/error
if s.Status == "done" || s.Status == "error" {
continue
}
s.Status = "interrupted"
_ = p.store.SaveSession(s.SessionKey, s)
p.broadcastUpdate(s.ChannelID, s)

View File

@@ -61,6 +61,13 @@ class LiveStatusPlugin {
window.__livestatus_updates[data.post_id] = update;
// Evict completed sessions from the update cache after 60s to prevent unbounded growth
if (data.status === 'done' || data.status === 'error' || data.status === 'interrupted') {
setTimeout(() => {
delete window.__livestatus_updates[data.post_id];
}, 60000);
}
// Notify post-specific listeners
const listeners = window.__livestatus_listeners[data.post_id];
if (listeners) {
@@ -82,7 +89,16 @@ class LiveStatusPlugin {
}
uninitialize(): void {
// Cleanup handled by Mattermost plugin framework
// Clear global listener and update stores to prevent accumulation across reloads
window.__livestatus_listeners = {};
window.__livestatus_updates = {};
// Unregister the custom post type component if it was registered
if (this.postTypeComponentId) {
// registry is not available here — Mattermost framework cleans up on deactivate.
// Clearing postTypeComponentId prevents stale references.
this.postTypeComponentId = null;
}
}
}