@@ -67,6 +67,16 @@ type FirecrestRemoteSessionController struct {
6767
6868 // fakeStart if true, do not start the remote session and print debug information
6969 fakeStart bool
70+
71+ // stdoutPath and stderrPath are the paths to the Slurm stdout/stderr files on the cluster filesystem.
72+ stdoutPath string
73+ stderrPath string
74+ // stdoutOffset and stderrOffset track how many bytes have been consumed from each log file.
75+ stdoutOffset int
76+ stderrOffset int
77+ // stdoutBuf and stderrBuf hold partial lines between fetches.
78+ stdoutBuf bytes.Buffer
79+ stderrBuf bytes.Buffer
7080}
7181
7282func NewFirecrestRemoteSessionController (cfg config.RemoteSessionControllerConfig ) (c * FirecrestRemoteSessionController , err error ) {
@@ -133,7 +143,7 @@ func (c *FirecrestRemoteSessionController) Start(ctx context.Context) error {
133143 // Start a go routine to update the session status
134144 go c .periodicSessionStatus (ctx )
135145
136- if err := c .recoverJobID (); err != nil {
146+ if err := c .recoverState (); err != nil {
137147 return err
138148 }
139149 // We recovered an existing job ID, do nothing
@@ -327,8 +337,38 @@ func (c *FirecrestRemoteSessionController) Start(ctx context.Context) error {
327337
328338 slog .Info ("submitted job" , "jobID" , c .jobID )
329339
330- // Save the job ID for recovery
331- if err := c .saveJobID (); err != nil {
340+ // After submission, determine the log file paths from the job metadata.
341+ c .stdoutPath = path .Join (sessionPath , fmt .Sprintf ("slurm-%s.out" , c .jobID ))
342+ c .stderrPath = path .Join (sessionPath , fmt .Sprintf ("slurm-%s.err" , c .jobID ))
343+
344+ metaRes , err := c .client .GetJobMetadataComputeSystemNameJobsJobIdMetadataGetWithResponse (startCtx , c .systemName , c .jobID )
345+ if err != nil {
346+ slog .Warn ("could not get job metadata, falling back to default log paths" , "error" , err )
347+ } else if metaRes .JSON200 == nil || metaRes .JSON200 .Jobs == nil || len (* metaRes .JSON200 .Jobs ) == 0 {
348+ slog .Warn ("job metadata response empty, falling back to default log paths" )
349+ } else {
350+ meta := (* metaRes .JSON200 .Jobs )[0 ]
351+ if meta .StandardOutput != nil && * meta .StandardOutput != "" {
352+ p := * meta .StandardOutput
353+ if ! path .IsAbs (p ) {
354+ p = path .Join (sessionPath , p )
355+ }
356+ c .stdoutPath = p
357+ }
358+ if meta .StandardError != nil && * meta .StandardError != "" {
359+ p := * meta .StandardError
360+ if ! path .IsAbs (p ) {
361+ p = path .Join (sessionPath , p )
362+ }
363+ c .stderrPath = p
364+ } else {
365+ // Slurm writes stderr to stdout when StandardError is not explicitly set.
366+ c .stderrPath = c .stdoutPath
367+ }
368+ }
369+
370+ // Save the state for recovery
371+ if err := c .saveState (); err != nil {
332372 return err
333373 }
334374
@@ -561,6 +601,14 @@ func (c *FirecrestRemoteSessionController) periodicSessionStatus(ctx context.Con
561601 } else {
562602 slog .Error ("getCurrentStatus failed" , "status" , state , "error" , err )
563603 }
604+ // Fetch any new session logs from the cluster filesystem.
605+ c .fetchSessionLogs (childCtx )
606+ // Persist offsets so we can resume after a restart.
607+ if c .jobID != "" {
608+ if err := c .saveState (); err != nil {
609+ slog .Warn ("failed to save controller state" , "error" , err )
610+ }
611+ }
564612 }()
565613 }
566614 }
@@ -599,6 +647,88 @@ func (c *FirecrestRemoteSessionController) getCurrentStatus(ctx context.Context)
599647 return state , nil
600648}
601649
650+ // fetchSessionLogs retrieves any new lines from the remote Slurm stdout/stderr files
651+ // via FirecREST and writes them to this container's stdout so they appear in kubectl logs.
652+ func (c * FirecrestRemoteSessionController ) fetchSessionLogs (ctx context.Context ) {
653+ if c .jobID == "" || c .stdoutPath == "" {
654+ return
655+ }
656+
657+ for _ , stream := range c .streamsToFetch () {
658+ c .fetchLogStream (ctx , stream )
659+ }
660+ }
661+
662+ func (c * FirecrestRemoteSessionController ) streamsToFetch () []string {
663+ streams := []string {"stdout" }
664+ if c .stderrPath != "" && c .stderrPath != c .stdoutPath {
665+ streams = append (streams , "stderr" )
666+ }
667+ return streams
668+ }
669+
670+ func (c * FirecrestRemoteSessionController ) fetchLogStream (ctx context.Context , stream string ) {
671+ var filePath string
672+ var offset * int
673+ var buf * bytes.Buffer
674+
675+ switch stream {
676+ case "stdout" :
677+ filePath = c .stdoutPath
678+ offset = & c .stdoutOffset
679+ buf = & c .stdoutBuf
680+ case "stderr" :
681+ filePath = c .stderrPath
682+ offset = & c .stderrOffset
683+ buf = & c .stderrBuf
684+ default :
685+ slog .Warn ("unknown stream type" , "stream" , stream )
686+ return
687+ }
688+
689+ size := 524288 // 512KB
690+ apiOffset := * offset + buf .Len ()
691+ params := GetViewFilesystemSystemNameOpsViewGetParams {
692+ Path : filePath ,
693+ Offset : & apiOffset ,
694+ Size : & size ,
695+ }
696+
697+ res , err := c .client .GetViewFilesystemSystemNameOpsViewGetWithResponse (ctx , c .systemName , & params )
698+ if err != nil {
699+ slog .Warn ("failed to fetch session log" , "stream" , stream , "error" , err )
700+ return
701+ }
702+ if res .JSON200 == nil || res .JSON200 .Output == nil {
703+ // File not available yet or empty — this is expected early in the job lifecycle.
704+ return
705+ }
706+
707+ content := * res .JSON200 .Output
708+ if content == "" {
709+ return
710+ }
711+
712+ // Append new content and flush any complete lines.
713+ if _ , err := buf .WriteString (content ); err != nil {
714+ slog .Warn ("failed to buffer session log content" , "stream" , stream , "error" , err )
715+ return
716+ }
717+ for {
718+ data := buf .Bytes ()
719+ idx := bytes .IndexByte (data , '\n' )
720+ if idx == - 1 {
721+ break
722+ }
723+ line := string (data [:idx ])
724+ if _ , err := fmt .Fprintf (os .Stdout , "[session/%s] %s\n " , stream , line ); err != nil {
725+ slog .Warn ("failed to write session log" , "stream" , stream , "error" , err )
726+ }
727+ buf .Next (idx + 1 )
728+ * offset += idx + 1
729+ }
730+ }
731+
602732func (c * FirecrestRemoteSessionController ) renderSessionScript (sessionScript string , fileSystems * []FileSystem , secretsPath string ) string {
603733 return renderSessionScriptStatic (sessionScript , c .partition , fileSystems , secretsPath )
604734}
@@ -673,7 +803,7 @@ func addSessionMountsToScript(sessionScript string, fileSystems *[]FileSystem, s
673803 return strings .Replace (sessionScript , "#{{SESSION_MOUNTS_PLACEHOLDER}}" , mountsStr , 1 )
674804}
675805
676- func (c * FirecrestRemoteSessionController ) saveJobID () error {
806+ func (c * FirecrestRemoteSessionController ) saveState () error {
677807 if c .jobID == "" {
678808 return fmt .Errorf ("cannot save, job ID is not defined" )
679809 }
@@ -684,7 +814,11 @@ func (c *FirecrestRemoteSessionController) saveJobID() error {
684814 savePath := c .getSavePath ()
685815
686816 state := savedState {
687- JobID : c .jobID ,
817+ JobID : c .jobID ,
818+ StdoutPath : c .stdoutPath ,
819+ StderrPath : c .stderrPath ,
820+ StdoutOffset : c .stdoutOffset ,
821+ StderrOffset : c .stderrOffset ,
688822 }
689823 contents , err := json .Marshal (state )
690824 if err != nil {
@@ -699,7 +833,7 @@ func (c *FirecrestRemoteSessionController) deleteSavedState() error {
699833 return os .Remove (savePath )
700834}
701835
702- func (c * FirecrestRemoteSessionController ) recoverJobID () error {
836+ func (c * FirecrestRemoteSessionController ) recoverState () error {
703837 contents , err := os .ReadFile (c .getSavePath ())
704838 if err != nil {
705839 return nil
@@ -714,11 +848,19 @@ func (c *FirecrestRemoteSessionController) recoverJobID() error {
714848 c .jobID = state .JobID
715849 slog .Info ("recovered job ID" , "jobID" , c .jobID )
716850 }
851+ c .stdoutPath = state .StdoutPath
852+ c .stderrPath = state .StderrPath
853+ c .stdoutOffset = state .StdoutOffset
854+ c .stderrOffset = state .StderrOffset
717855 return nil
718856}
719857
720858type savedState struct {
721- JobID string `json:"job_id"`
859+ JobID string `json:"job_id"`
860+ StdoutPath string `json:"stdout_path,omitempty"`
861+ StderrPath string `json:"stderr_path,omitempty"`
862+ StdoutOffset int `json:"stdout_offset,omitempty"`
863+ StderrOffset int `json:"stderr_offset,omitempty"`
722864}
723865
724866func (c * FirecrestRemoteSessionController ) getSaveDirPath () string {
0 commit comments