diff --git a/server/pkg/handlers.go b/server/pkg/handlers.go index 22504be..30a5247 100644 --- a/server/pkg/handlers.go +++ b/server/pkg/handlers.go @@ -5,6 +5,9 @@ import ( "fmt" "net/http" "os/exec" + + "github.com/go-chi/chi/v5" + "github.com/google/uuid" ) type Handler struct { @@ -92,41 +95,129 @@ func (h *Handler) StartHandler() http.HandlerFunc { } } -// func launchHandler(w http.ResponseWriter, r *http.Request) { -// model := chi.URLParam(r, "model") -// if model == "" { -// http.Error(w, "Model parameter is required", http.StatusBadRequest) -// return -// } +func (h *Handler) GetInstance() http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + id := chi.URLParam(r, "id") + uuid, err := uuid.Parse(id) + if err != nil { + http.Error(w, "Invalid UUID format", http.StatusBadRequest) + return + } -// cmd := execLLama(model) -// if err := cmd.Start(); err != nil { -// http.Error(w, "Failed to start llama server: "+err.Error(), http.StatusInternalServerError) -// return -// } + instance, err := h.InstanceManager.GetInstance(uuid) + if err != nil { + http.Error(w, "Failed to get instance: "+err.Error(), http.StatusInternalServerError) + return + } -// instances[model] = cmd -// w.Write([]byte("Llama server started for model: " + model)) -// } + w.Header().Set("Content-Type", "application/json") + if err := json.NewEncoder(w).Encode(instance); err != nil { + http.Error(w, "Failed to encode instance: "+err.Error(), http.StatusInternalServerError) + return + } + } +} -// func stopHandler(w http.ResponseWriter, r *http.Request) { -// model := chi.URLParam(r, "model") -// if model == "" { -// http.Error(w, "Model parameter is required", http.StatusBadRequest) -// return -// } +func (h *Handler) UpdateInstance() http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + id := chi.URLParam(r, "id") + uuid, err := uuid.Parse(id) + if err != nil { + http.Error(w, "Invalid UUID format", http.StatusBadRequest) + return + } -// cmd, exists := instances[model] -// if !exists { -// http.Error(w, "No running instance for model: "+model, http.StatusNotFound) -// return -// } + var options InstanceOptions + if err := json.NewDecoder(r.Body).Decode(&options); err != nil { + http.Error(w, "Invalid request body", http.StatusBadRequest) + return + } -// if err := cmd.Process.Signal(os.Interrupt); err != nil { -// http.Error(w, "Failed to stop llama server: "+err.Error(), http.StatusInternalServerError) -// return -// } + instance, err := h.InstanceManager.UpdateInstance(uuid, &options) + if err != nil { + http.Error(w, "Failed to update instance: "+err.Error(), http.StatusInternalServerError) + return + } -// delete(instances, model) -// w.Write([]byte("Llama server stopped for model: " + model)) -// } + instance, err = h.InstanceManager.RestartInstance(uuid) + if err != nil { + http.Error(w, "Failed to restart instance: "+err.Error(), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + if err := json.NewEncoder(w).Encode(instance); err != nil { + http.Error(w, "Failed to encode instance: "+err.Error(), http.StatusInternalServerError) + return + } + } +} + +func (h *Handler) StartInstance() http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + id := chi.URLParam(r, "id") + uuid, err := uuid.Parse(id) + if err != nil { + http.Error(w, "Invalid UUID format", http.StatusBadRequest) + return + } + + instance, err := h.InstanceManager.StartInstance(uuid) + if err != nil { + http.Error(w, "Failed to start instance: "+err.Error(), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + if err := json.NewEncoder(w).Encode(instance); err != nil { + http.Error(w, "Failed to encode instance: "+err.Error(), http.StatusInternalServerError) + return + } + } +} + +func (h *Handler) StopInstance() http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + id := chi.URLParam(r, "id") + uuid, err := uuid.Parse(id) + if err != nil { + http.Error(w, "Invalid UUID format", http.StatusBadRequest) + return + } + + instance, err := h.InstanceManager.StopInstance(uuid) + if err != nil { + http.Error(w, "Failed to stop instance: "+err.Error(), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + if err := json.NewEncoder(w).Encode(instance); err != nil { + http.Error(w, "Failed to encode instance: "+err.Error(), http.StatusInternalServerError) + return + } + } +} + +func (h *Handler) RestartInstance() http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + id := chi.URLParam(r, "id") + uuid, err := uuid.Parse(id) + if err != nil { + http.Error(w, "Invalid UUID format", http.StatusBadRequest) + return + } + + instance, err := h.InstanceManager.RestartInstance(uuid) + if err != nil { + http.Error(w, "Failed to restart instance: "+err.Error(), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + if err := json.NewEncoder(w).Encode(instance); err != nil { + http.Error(w, "Failed to encode instance: "+err.Error(), http.StatusInternalServerError) + return + } + } +} diff --git a/server/pkg/instance.go b/server/pkg/instance.go index d1bbd71..060d8ff 100644 --- a/server/pkg/instance.go +++ b/server/pkg/instance.go @@ -15,7 +15,7 @@ import ( type Instance struct { ID uuid.UUID - Options *InstanceOptions + options *InstanceOptions // Status Running bool @@ -37,7 +37,7 @@ type Instance struct { func NewInstance(id uuid.UUID, options *InstanceOptions) *Instance { return &Instance{ ID: id, - Options: options, + options: options, Running: false, @@ -46,6 +46,22 @@ func NewInstance(id uuid.UUID, options *InstanceOptions) *Instance { } } +func (i *Instance) GetOptions() *InstanceOptions { + i.mu.Lock() + defer i.mu.Unlock() + return i.options +} + +func (i *Instance) SetOptions(options *InstanceOptions) { + i.mu.Lock() + defer i.mu.Unlock() + if options == nil { + log.Println("Warning: Attempted to set nil options on instance", i.ID) + return + } + i.options = options +} + func (i *Instance) Start() error { i.mu.Lock() defer i.mu.Unlock() @@ -54,7 +70,7 @@ func (i *Instance) Start() error { return fmt.Errorf("instance %s is already running", i.ID) } - args := i.Options.BuildCommandArgs() + args := i.options.BuildCommandArgs() i.ctx, i.cancel = context.WithCancel(context.Background()) i.cmd = exec.CommandContext(i.ctx, "llama-server", args...) @@ -156,14 +172,14 @@ func (i *Instance) monitorProcess() { } // Handle restart if process crashed and auto-restart is enabled - if err != nil && i.Options.AutoRestart && i.restarts < i.Options.MaxRestarts { + if err != nil && i.options.AutoRestart && i.restarts < i.options.MaxRestarts { i.restarts++ log.Printf("Auto-restarting instance %s (attempt %d/%d) in %v", - i.ID, i.restarts, i.Options.MaxRestarts, i.Options.RestartDelay) + i.ID, i.restarts, i.options.MaxRestarts, i.options.RestartDelay) // Unlock mutex during sleep to avoid blocking other operations i.mu.Unlock() - time.Sleep(i.Options.RestartDelay) + time.Sleep(i.options.RestartDelay) i.mu.Lock() // Attempt restart @@ -173,7 +189,7 @@ func (i *Instance) monitorProcess() { log.Printf("Successfully restarted instance %s", i.ID) i.restarts = 0 // Reset restart count on successful restart } - } else if i.restarts >= i.Options.MaxRestarts { - log.Printf("Instance %s exceeded max restart attempts (%d)", i.ID, i.Options.MaxRestarts) + } else if i.restarts >= i.options.MaxRestarts { + log.Printf("Instance %s exceeded max restart attempts (%d)", i.ID, i.options.MaxRestarts) } } diff --git a/server/pkg/manager.go b/server/pkg/manager.go index 22451de..a675eb1 100644 --- a/server/pkg/manager.go +++ b/server/pkg/manager.go @@ -87,7 +87,8 @@ func (im *instanceManager) UpdateInstance(id uuid.UUID, options *InstanceOptions if !exists { return nil, fmt.Errorf("instance with ID %s not found", id) } - instance.Options = options + + instance.SetOptions(options) return instance, nil } @@ -117,7 +118,10 @@ func (im *instanceManager) StartInstance(id uuid.UUID) (*Instance, error) { return instance, fmt.Errorf("instance with ID %s is already running", id) } - //TODO: + if err := instance.Start(); err != nil { + return nil, fmt.Errorf("failed to start instance %s: %w", id, err) + } + return instance, nil } @@ -131,7 +135,10 @@ func (im *instanceManager) StopInstance(id uuid.UUID) (*Instance, error) { return instance, fmt.Errorf("instance with ID %s is already stopped", id) } - // TODO: + if err := instance.Stop(); err != nil { + return nil, fmt.Errorf("failed to stop instance %s: %w", id, err) + } + return instance, nil } diff --git a/server/pkg/routes.go b/server/pkg/routes.go index 81dfb33..77c3ff5 100644 --- a/server/pkg/routes.go +++ b/server/pkg/routes.go @@ -31,12 +31,12 @@ func SetupRouter(handler *Handler) *chi.Mux { r.Route("/{id}", func(r chi.Router) { // Instance management - // r.Get("/", handler.GetInstance()) // Get instance details - // r.Put("/", handler.UpdateInstance()) // Update instance configuration + r.Get("/", handler.GetInstance()) // Get instance details + r.Put("/", handler.UpdateInstance()) // Update instance configuration // r.Delete("/", handler.DeleteInstance()) // Stop and remove instance - // r.Post("/start", handler.StartInstance()) // Start stopped instance - // r.Post("/stop", handler.StopInstance()) // Stop running instance - // r.Post("/restart", handler.RestartInstance()) // Restart instance + r.Post("/start", handler.StartInstance()) // Start stopped instance + r.Post("/stop", handler.StopInstance()) // Stop running instance + r.Post("/restart", handler.RestartInstance()) // Restart instance // r.Get("/logs", handler.GetInstanceLogs()) // Get instance logs // Llama.cpp server proxy endpoints (proxied to the actual llama.cpp server)