| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158 |
- package agi
- import (
- "encoding/json"
- "errors"
- "log"
- "net/http"
- "sync"
- "time"
- "imuslab.com/arozos/mod/utils"
- )
- /*
- AGI VM Registry
- Tracks every Otto VM that is actively executing a script so that
- administrators (and users for their own scripts) can inspect running
- VMs and force-stop any that are stuck in an infinite loop or
- otherwise unresponsive.
- Force-stop works by sending a function into the VM's interrupt
- channel; Otto checks that channel between JS operations and, when a
- value is found, calls the function — which panics with errForceStop.
- The panic is caught by a deferred recovery block in each Execute*
- function and results in a 503 response rather than a goroutine crash.
- */
- // errForceStop is the sentinel value panicked inside a forcibly stopped VM.
- var errForceStop = errors.New("errForceStop")
- // VMRecord holds metadata about one live AGI VM instance.
- type VMRecord struct {
- ExecID string
- ScriptFile string
- Username string
- StartTime time.Time
- interruptCh chan func() // alias to vm.Interrupt — never nil after registration
- }
- // VMInfo is the JSON-serialisable view of a VMRecord sent to API callers.
- type VMInfo struct {
- ExecID string `json:"execID"`
- ScriptFile string `json:"scriptFile"`
- Username string `json:"username"`
- StartTime int64 `json:"startTime"` // Unix seconds
- ElapsedSeconds int64 `json:"elapsedSeconds"` // seconds since StartTime
- }
- func toVMInfo(rec *VMRecord) VMInfo {
- return VMInfo{
- ExecID: rec.ExecID,
- ScriptFile: rec.ScriptFile,
- Username: rec.Username,
- StartTime: rec.StartTime.Unix(),
- ElapsedSeconds: int64(time.Since(rec.StartTime).Seconds()),
- }
- }
- // vmRegistry is a goroutine-safe map of execID → *VMRecord.
- type vmRegistry struct {
- mu sync.RWMutex
- records map[string]*VMRecord
- }
- func newVMRegistry() *vmRegistry {
- return &vmRegistry{records: make(map[string]*VMRecord)}
- }
- // register adds a record. Called just before vm.Run() in each Execute* path.
- func (r *vmRegistry) register(rec *VMRecord) {
- r.mu.Lock()
- r.records[rec.ExecID] = rec
- r.mu.Unlock()
- }
- // unregister removes a record. Always called via defer so it fires even on panic.
- func (r *vmRegistry) unregister(execID string) {
- r.mu.Lock()
- delete(r.records, execID)
- r.mu.Unlock()
- }
- // list returns VMInfo for every VM visible to the requester.
- // Admins see all; regular users see only their own records.
- func (r *vmRegistry) list(requesterUsername string, isAdmin bool) []VMInfo {
- r.mu.RLock()
- defer r.mu.RUnlock()
- result := make([]VMInfo, 0, len(r.records))
- for _, rec := range r.records {
- if isAdmin || rec.Username == requesterUsername {
- result = append(result, toVMInfo(rec))
- }
- }
- return result
- }
- // forceStop sends an interrupt to the VM with the given execID.
- // Regular users may only stop their own VMs; admins may stop any.
- func (r *vmRegistry) forceStop(execID, requesterUsername string, isAdmin bool) error {
- r.mu.RLock()
- rec, ok := r.records[execID]
- r.mu.RUnlock()
- if !ok {
- return errors.New("VM not found: " + execID)
- }
- if !isAdmin && rec.Username != requesterUsername {
- return errors.New("permission denied: you can only stop your own VMs")
- }
- select {
- case rec.interruptCh <- func() { panic(errForceStop) }:
- log.Printf("[AGI] VM %s (script: %s, user: %s) force-stopped by %s",
- execID, rec.ScriptFile, rec.Username, requesterUsername)
- return nil
- default:
- return errors.New("interrupt channel full — VM may already be stopping")
- }
- }
- // ── HTTP Handlers ──────────────────────────────────────────────────────────
- // HandleListRuntimes returns the list of running VMs visible to the caller.
- // GET /system/ajgi/runtime/list
- func (g *Gateway) HandleListRuntimes(w http.ResponseWriter, r *http.Request) {
- thisuser, err := g.Option.UserHandler.GetUserInfoFromRequest(w, r)
- if err != nil {
- http.Error(w, "401 Unauthorized", http.StatusUnauthorized)
- return
- }
- infos := g.vmReg.list(thisuser.Username, thisuser.IsAdmin())
- w.Header().Set("Content-Type", "application/json")
- json.NewEncoder(w).Encode(infos)
- }
- // HandleForceStopRuntime terminates a VM identified by the execid POST parameter.
- // POST /system/ajgi/runtime/stop
- func (g *Gateway) HandleForceStopRuntime(w http.ResponseWriter, r *http.Request) {
- thisuser, err := g.Option.UserHandler.GetUserInfoFromRequest(w, r)
- if err != nil {
- http.Error(w, "401 Unauthorized", http.StatusUnauthorized)
- return
- }
- execID, err := utils.PostPara(r, "execid")
- if err != nil {
- utils.SendErrorResponse(w, "missing execid parameter")
- return
- }
- if stopErr := g.vmReg.forceStop(execID, thisuser.Username, thisuser.IsAdmin()); stopErr != nil {
- utils.SendErrorResponse(w, stopErr.Error())
- return
- }
- w.Header().Set("Content-Type", "application/json")
- w.Write([]byte(`{"ok":true}`))
- }
|