@@ -26,6 +26,7 @@ import (
2626 "strconv"
2727 "strings"
2828 "time"
29+ "unicode"
2930)
3031
3132const QstatDateFormat = "2006-01-02 03:04:05"
@@ -68,7 +69,9 @@ func parseFixedWidthJobs(input string) ([]ParallelJobTask, error) {
6869 return tasks , nil
6970 }
7071
71- // Correct column positions based on your description
72+ // we have 9.0.0 to 9.0.2 format and 9.0.3 format with 3 more columns
73+ // for the job IDs
74+
7275 columnPositions := []struct {
7376 start int
7477 end int
@@ -942,3 +945,192 @@ func ParseJobArrayTask(out string) ([]JobArrayTask, error) {
942945 }
943946 return jobArrayTasks , nil
944947}
948+
949+ /*
950+ qstat -f
951+ queuename qtype resv/used/tot. load_avg arch states
952+ ---------------------------------------------------------------------------------
953+ all.q@master BIP 0/9/14 0.69 lx-amd64
954+ 2 0.50500 sleep root r 2025-02-15 12:28:22 1
955+ 3 0.50500 sleep root r 2025-02-15 12:28:23 1
956+ 4 0.50500 sleep root r 2025-02-15 12:28:23 1
957+ 5 0.50500 sleep root r 2025-02-15 12:28:24 1
958+ 6 0.50500 sleep root r 2025-02-15 12:28:24 1
959+ 7 0.50500 sleep root r 2025-02-15 12:28:25 1
960+ 8 0.50500 sleep root r 2025-02-15 12:28:25 1
961+ 12 0.60500 sleep root r 2025-02-15 12:29:31 2
962+ ---------------------------------------------------------------------------------
963+ test.q@master BIP 0/6/10 0.69 lx-amd64
964+ 9 0.50500 sleep root r 2025-02-15 12:28:34 1
965+ 10 0.50500 sleep root r 2025-02-15 12:28:38 1
966+ 11 0.50500 sleep root r 2025-02-15 12:29:03 1 1
967+ 11 0.50500 sleep root r 2025-02-15 12:29:03 1 2
968+ 13 0.60500 sleep root r 2025-02-15 12:29:35 2
969+ */
970+
971+ // ParseQstatFullOutput parses the output of the "qstat -f" command and returns
972+ // a slice of FullQueueInfo containing queue details and associated job information.
973+ //
974+ // It expects an output with queue header lines (non-indented) followed by one or more
975+ // job lines (indented) until a separator (a line full of "-" characters) is encountered.
976+ func ParseQstatFullOutput (out string ) ([]FullQueueInfo , error ) {
977+ lines := strings .Split (out , "\n " )
978+ var results []FullQueueInfo
979+ var currentQueue * FullQueueInfo
980+
981+ for _ , line := range lines {
982+ trimmed := strings .TrimSpace (line )
983+ if trimmed == "" {
984+ continue
985+ }
986+ if strings .HasPrefix (trimmed , "####" ) {
987+ break
988+ }
989+
990+ // Skip any known header lines.
991+ lower := strings .ToLower (trimmed )
992+ if strings .HasPrefix (lower , "queuename" ) {
993+ continue
994+ }
995+
996+ // If this is a separator line, then finish the current block.
997+ if isSeparatorLine (trimmed ) {
998+ if currentQueue != nil {
999+ results = append (results , * currentQueue )
1000+ currentQueue = nil
1001+ }
1002+ continue
1003+ }
1004+
1005+ // If the line does not start with whitespace, it is a queue header.
1006+ if ! startsWithWhitespace (line ) {
1007+ // If an active queue exists, push it into results before starting a new block.
1008+ if currentQueue != nil {
1009+ results = append (results , * currentQueue )
1010+ }
1011+
1012+ fields := strings .Fields (line )
1013+ if len (fields ) < 5 {
1014+ return nil , fmt .Errorf ("invalid queue header format: %q" , line )
1015+ }
1016+ queueName := fields [0 ]
1017+ qtype := fields [1 ]
1018+ resvUsedTot := fields [2 ] // Expected format: "resv/used/tot"
1019+ loadAvgStr := fields [3 ]
1020+ arch := fields [4 ]
1021+
1022+ parts := strings .Split (resvUsedTot , "/" )
1023+ if len (parts ) != 3 {
1024+ return nil , fmt .Errorf ("invalid resv/used/tot format in queue header: %q" , line )
1025+ }
1026+ reserved , err := strconv .Atoi (parts [0 ])
1027+ if err != nil {
1028+ return nil , fmt .Errorf ("invalid reserved value in queue header: %v" , err )
1029+ }
1030+ used , err := strconv .Atoi (parts [1 ])
1031+ if err != nil {
1032+ return nil , fmt .Errorf ("invalid used value in queue header: %v" , err )
1033+ }
1034+ total , err := strconv .Atoi (parts [2 ])
1035+ if err != nil {
1036+ return nil , fmt .Errorf ("invalid total value in queue header: %v" , err )
1037+ }
1038+ loadAvg , err := strconv .ParseFloat (loadAvgStr , 64 )
1039+ if err != nil {
1040+ return nil , fmt .Errorf ("invalid load_avg value in queue header: %v" , err )
1041+ }
1042+ currentQueue = & FullQueueInfo {
1043+ QueueName : queueName ,
1044+ QueueType : qtype ,
1045+ Reserved : reserved ,
1046+ Used : used ,
1047+ Total : total ,
1048+ LoadAvg : loadAvg ,
1049+ Arch : arch ,
1050+ Jobs : []JobInfo {},
1051+ }
1052+ } else {
1053+ // This is a job line. It must belong to an already parsed queue header.
1054+ if currentQueue == nil {
1055+ return nil , fmt .Errorf ("job info found without preceding queue header: %q" , line )
1056+ }
1057+ fields := strings .Fields (line )
1058+ if len (fields ) < 8 {
1059+ return nil , fmt .Errorf ("invalid job line format: %q" , line )
1060+ }
1061+ jobID , err := strconv .Atoi (fields [0 ])
1062+ if err != nil {
1063+ return nil , fmt .Errorf ("invalid job id in job line %q: %v" , line , err )
1064+ }
1065+ score , err := strconv .ParseFloat (fields [1 ], 64 )
1066+ if err != nil {
1067+ return nil , fmt .Errorf ("invalid score in job line %q: %v" , line , err )
1068+ }
1069+ taskName := fields [2 ]
1070+ owner := fields [3 ]
1071+ state := fields [4 ]
1072+ datetimeStr := fields [5 ] + " " + fields [6 ]
1073+ startTime , err := time .Parse ("2006-01-02 15:04:05" , datetimeStr )
1074+ if err != nil {
1075+ return nil , fmt .Errorf (
1076+ "failed to parse datetime '%s' in job line %q: %v" ,
1077+ datetimeStr , line , err )
1078+ }
1079+ var submitTime time.Time
1080+ if strings .Contains (state , "q" ) {
1081+ submitTime = startTime
1082+ startTime = time.Time {}
1083+ }
1084+ slots , err := strconv .Atoi (fields [7 ])
1085+ if err != nil {
1086+ return nil , fmt .Errorf ("invalid slots in job line %q: %v" , line , err )
1087+ }
1088+ // optional tasks
1089+ var taskIDs []int64
1090+ if len (fields ) > 8 {
1091+ taskID , err := strconv .Atoi (fields [8 ])
1092+ if err != nil {
1093+ return nil , fmt .Errorf ("invalid task id in job line %q: %v" , line , err )
1094+ }
1095+ taskIDs = []int64 {int64 (taskID )}
1096+ }
1097+ job := JobInfo {
1098+ JobID : jobID ,
1099+ Priority : score ,
1100+ Name : taskName ,
1101+ User : owner ,
1102+ State : state ,
1103+ StartTime : startTime ,
1104+ SubmitTime : submitTime ,
1105+ Queue : currentQueue .QueueName ,
1106+ Slots : slots ,
1107+ JaTaskIDs : taskIDs ,
1108+ }
1109+ currentQueue .Jobs = append (currentQueue .Jobs , job )
1110+ }
1111+ }
1112+
1113+ // Append the last queue block if it exists.
1114+ if currentQueue != nil {
1115+ results = append (results , * currentQueue )
1116+ }
1117+ return results , nil
1118+ }
1119+
1120+ // startsWithWhitespace returns true if the first rune of the string is a whitespace.
1121+ func startsWithWhitespace (s string ) bool {
1122+ for _ , r := range s {
1123+ return unicode .IsSpace (r )
1124+ }
1125+ return false
1126+ }
1127+
1128+ // isSeparatorLine checks if the provided line is made up entirely of '-' characters.
1129+ func isSeparatorLine (s string ) bool {
1130+ for _ , r := range s {
1131+ if r != '-' {
1132+ return false
1133+ }
1134+ }
1135+ return true
1136+ }
0 commit comments