-
Notifications
You must be signed in to change notification settings - Fork 1
fix: Run exec agent #11
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 4 commits
8223bd0
01bc449
e1a6568
2d98dd4
2d395fb
f01a216
eae19f1
df80fe2
a29b566
ab92d45
6d1fda9
6e6e77c
840e166
3660019
f51d8d4
5160555
c6fdca4
09ccdc6
33ea1ad
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -9,43 +9,68 @@ import ( | |
| "os/exec" | ||
| "runtime" | ||
| "strconv" | ||
| "sync" | ||
| "syscall" | ||
|
|
||
| "github.com/charmbracelet/log" | ||
| "github.com/ctrlplanedev/cli/internal/api" | ||
| "github.com/ctrlplanedev/cli/pkg/jobagent" | ||
| ) | ||
|
|
||
| var _ jobagent.Runner = &ExecRunner{} | ||
|
|
||
| type ExecRunner struct{} | ||
| type ExecRunner struct { | ||
| mu sync.Mutex | ||
| finished map[int]error | ||
| } | ||
|
|
||
| func NewExecRunner() *ExecRunner { | ||
| return &ExecRunner{ | ||
| finished: make(map[int]error), | ||
| } | ||
| } | ||
|
|
||
| type ExecConfig struct { | ||
| WorkingDir string `json:"workingDir,omitempty"` | ||
| Script string `json:"script"` | ||
| } | ||
|
|
||
| func (r *ExecRunner) Status(job api.Job) (api.JobStatus, string) { | ||
|
||
| externalId, err := strconv.Atoi(*job.ExternalId) | ||
| if job.ExternalId == nil { | ||
| return api.JobStatusExternalRunNotFound, fmt.Sprintf("external ID is nil: %v", job.ExternalId) | ||
| } | ||
|
|
||
| pid, err := strconv.Atoi(*job.ExternalId) | ||
| if err != nil { | ||
| return api.JobStatusExternalRunNotFound, fmt.Sprintf("invalid process id: %v", err) | ||
| } | ||
|
|
||
| process, err := os.FindProcess(externalId) | ||
| // Check if we've recorded a finished result for this process | ||
| r.mu.Lock() | ||
| finishedErr, exists := r.finished[pid] | ||
| r.mu.Unlock() | ||
| if exists { | ||
| if finishedErr != nil { | ||
| return api.JobStatusFailure, fmt.Sprintf("process exited with error: %v", finishedErr) | ||
| } | ||
| return api.JobStatusSuccessful, "process completed successfully" | ||
| } | ||
|
|
||
| // If not finished yet, try to check if the process is still running. | ||
| process, err := os.FindProcess(pid) | ||
| if err != nil { | ||
| return api.JobStatusExternalRunNotFound, fmt.Sprintf("failed to find process: %v", err) | ||
| } | ||
|
|
||
| // On Unix systems, FindProcess always succeeds, so we need to send signal 0 | ||
| // to check if process exists | ||
| // On Unix, Signal 0 will error if the process is not running. | ||
| err = process.Signal(syscall.Signal(0)) | ||
| if err != nil { | ||
| return api.JobStatusSuccessful, fmt.Sprintf("process not running: %v", err) | ||
| // Process is not running but we haven't recorded its result. | ||
| return api.JobStatusFailure, fmt.Sprintf("process not running: %v", err) | ||
| } | ||
|
|
||
| return api.JobStatusInProgress, fmt.Sprintf("process running with pid %d", externalId) | ||
| return api.JobStatusInProgress, fmt.Sprintf("process running with pid %d", pid) | ||
| } | ||
|
|
||
| func (r *ExecRunner) Start(job api.Job) (string, error) { | ||
| func (r *ExecRunner) Start(job api.Job, jobDetails map[string]interface{}) (string, error) { | ||
| // Create temp script file | ||
| ext := ".sh" | ||
| if runtime.GOOS == "windows" { | ||
|
|
@@ -56,7 +81,6 @@ func (r *ExecRunner) Start(job api.Job) (string, error) { | |
| if err != nil { | ||
| return "", fmt.Errorf("failed to create temp script file: %w", err) | ||
| } | ||
| defer os.Remove(tmpFile.Name()) | ||
|
|
||
| config := ExecConfig{} | ||
| jsonBytes, err := json.Marshal(job.JobAgentConfig) | ||
|
|
@@ -73,7 +97,7 @@ func (r *ExecRunner) Start(job api.Job) (string, error) { | |
| } | ||
|
|
||
| buf := new(bytes.Buffer) | ||
| if err := templatedScript.Execute(buf, job); err != nil { | ||
| if err := templatedScript.Execute(buf, jobDetails); err != nil { | ||
|
||
| return "", fmt.Errorf("failed to execute script template: %w", err) | ||
| } | ||
| script := buf.String() | ||
|
|
@@ -104,9 +128,32 @@ func (r *ExecRunner) Start(job api.Job) (string, error) { | |
| cmd.Stdout = os.Stdout | ||
| cmd.Stderr = os.Stderr | ||
|
|
||
| if err := cmd.Run(); err != nil { | ||
| return "", fmt.Errorf("failed to execute script: %w", err) | ||
| if err := cmd.Start(); err != nil { | ||
| os.Remove(tmpFile.Name()) | ||
| return "", fmt.Errorf("failed to start process: %w", err) | ||
| } | ||
|
|
||
| pid := cmd.Process.Pid | ||
|
|
||
| // Launch a goroutine to wait for process completion and store the result. | ||
| go func(pid int, scriptPath string) { | ||
| err := cmd.Wait() | ||
| // Ensure the map is not nil; if there's any chance ExecRunner is used as a zero-value, initialize it. | ||
| r.mu.Lock() | ||
| if r.finished == nil { | ||
| r.finished = make(map[int]error) | ||
| } | ||
| r.finished[pid] = err | ||
| r.mu.Unlock() | ||
|
|
||
| if err != nil { | ||
| log.Error("Process execution failed", "pid", pid, "error", err) | ||
| } else { | ||
| log.Info("Process execution succeeded", "pid", pid) | ||
| } | ||
|
|
||
| os.Remove(scriptPath) | ||
| }(pid, tmpFile.Name()) | ||
|
|
||
| return strconv.Itoa(cmd.Process.Pid), nil | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,35 @@ | ||
| package jobagent | ||
|
|
||
| import ( | ||
| "context" | ||
| "encoding/json" | ||
| "fmt" | ||
|
|
||
| "github.com/ctrlplanedev/cli/internal/api" | ||
| "github.com/spf13/viper" | ||
| ) | ||
|
|
||
| func fetchJobDetails(ctx context.Context, jobID string) (map[string]interface{}, error) { | ||
| client, err := api.NewAPIKeyClientWithResponses(viper.GetString("url"), viper.GetString("api-key")) | ||
| if err != nil { | ||
| return nil, fmt.Errorf("failed to create API client for job details: %w", err) | ||
| } | ||
|
|
||
| resp, err := client.GetJobWithResponse(ctx, jobID) | ||
| if err != nil { | ||
| return nil, fmt.Errorf("failed to get job details: %w", err) | ||
| } | ||
| if resp.JSON200 == nil { | ||
| return nil, fmt.Errorf("received empty response from job details API") | ||
| } | ||
|
|
||
| var details map[string]interface{} | ||
| detailsBytes, err := json.Marshal(resp.JSON200) | ||
| if err != nil { | ||
| return nil, fmt.Errorf("failed to marshal job response: %w", err) | ||
| } | ||
| if err := json.Unmarshal(detailsBytes, &details); err != nil { | ||
| return nil, fmt.Errorf("failed to unmarshal job details: %w", err) | ||
| } | ||
| return details, nil | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could move input validation above the client creation?