@@ -14,6 +14,7 @@ import (
1414 "slices"
1515 "strconv"
1616 "strings"
17+ "text/template"
1718
1819 "perfspect/internal/cpus"
1920 "perfspect/internal/target"
@@ -93,7 +94,11 @@ func RunScripts(myTarget target.Target, scripts []ScriptDefinition, ignoreScript
9394 if len (parallelScripts ) > 0 {
9495 // form one master script that calls all the parallel scripts in the background
9596 masterScriptName := "parallel_master.sh"
96- masterScript , needsElevatedPrivileges := formMasterScript (myTarget .GetTempDirectory (), parallelScripts )
97+ masterScript , needsElevatedPrivileges , err := formMasterScript (myTarget .GetTempDirectory (), parallelScripts )
98+ if err != nil {
99+ err = fmt .Errorf ("error forming master script: %v" , err )
100+ return nil , err
101+ }
97102 // write master script to local file
98103 masterScriptPath := path .Join (localTempDir , myTarget .GetName (), masterScriptName )
99104 err = os .WriteFile (masterScriptPath , []byte (masterScript ), 0600 )
@@ -277,97 +282,126 @@ func scriptNameToFilename(name string) string {
277282
278283// formMasterScript forms a master script that runs all parallel scripts in the background, waits for them to finish, then prints the output of each script.
279284// Return values are the master script and a boolean indicating whether the master script requires elevated privileges.
280- func formMasterScript (targetTempDirectory string , parallelScripts []ScriptDefinition ) (string , bool ) {
281- // we write the stdout and stderr from each command to temporary files and save the PID of each command
282- // in a variable named after the script
283- var masterScript strings.Builder
285+ func formMasterScript (targetTempDirectory string , parallelScripts []ScriptDefinition ) (string , bool , error ) {
286+ // data model for template
287+ type tplScript struct {
288+ Name string
289+ Sanitized string
290+ NeedsKill bool
291+ Superuser bool
292+ }
293+ data := struct {
294+ TargetTempDir string
295+ Scripts []tplScript
296+ }{}
297+ data .TargetTempDir = targetTempDirectory
298+ needsElevated := false
299+ for _ , s := range parallelScripts {
300+ if s .Superuser {
301+ needsElevated = true
302+ }
303+ data .Scripts = append (data .Scripts , tplScript {
304+ Name : s .Name , Sanitized : sanitizeScriptName (s .Name ), NeedsKill : s .NeedsKill , Superuser : s .Superuser ,
305+ })
306+ }
307+ const masterScriptTemplate = `#!/usr/bin/env bash
308+ set -o errexit
309+ set -o pipefail
284310
285- masterScript .WriteString ("#!/bin/bash\n " )
311+ script_dir={{.TargetTempDir}}
312+ cd "$script_dir"
286313
287- // set dir var and change working directory to dir in case any of the scripts write out temporary files
288- masterScript .WriteString (fmt .Sprintf ("script_dir=%s\n " , targetTempDirectory ))
289- masterScript .WriteString ("cd $script_dir\n " )
314+ declare -a scripts=()
315+ declare -A needs_kill=()
316+ declare -A pids=()
317+ declare -A exitcodes=()
318+ declare -A orig_names=()
290319
291- // function to print the output of each script
292- masterScript .WriteString ("\n print_output() {\n " )
293- for _ , script := range parallelScripts {
294- masterScript .WriteString ("\t echo \" <---------------------->\" \n " )
295- masterScript .WriteString (fmt .Sprintf ("\t echo SCRIPT NAME: %s\n " , script .Name ))
296- masterScript .WriteString (fmt .Sprintf ("\t echo STDOUT:\n \t cat %s\n " , path .Join ("$script_dir" , sanitizeScriptName (script .Name )+ ".stdout" )))
297- masterScript .WriteString (fmt .Sprintf ("\t echo STDERR:\n \t cat %s\n " , path .Join ("$script_dir" , sanitizeScriptName (script .Name )+ ".stderr" )))
298- masterScript .WriteString (fmt .Sprintf ("\t echo EXIT CODE: $%s_exitcode\n " , sanitizeScriptName (script .Name )))
299- }
300- masterScript .WriteString ("}\n " )
320+ {{- range .Scripts}}
321+ scripts+=({{ .Sanitized }})
322+ needs_kill[{{ .Sanitized }}]={{ if .NeedsKill }}1{{ else }}0{{ end }}
323+ orig_names[{{ .Sanitized }}]="{{ .Name }}"
324+ {{ end }}
301325
302- // function to handle SIGINT
303- masterScript .WriteString ("\n handle_sigint() {\n " )
304- for _ , script := range parallelScripts {
305- // send SIGINT to the child script, if it is still running
306- masterScript .WriteString (fmt .Sprintf ("\t if ps -p \" $%s_pid\" > /dev/null; then\n " , sanitizeScriptName (script .Name )))
307- masterScript .WriteString (fmt .Sprintf ("\t \t kill -SIGINT $%s_pid\n " , sanitizeScriptName (script .Name )))
308- masterScript .WriteString ("\t fi\n " )
309- if script .NeedsKill { // this is primarily used for scripts that start commands in the background, some of which (processwatch) doesn't respond to SIGINT as expected
310- // if the *cmd.pid file exists, check if the process is still running
311- masterScript .WriteString (fmt .Sprintf ("\t if [ -f %s_cmd.pid ]; then\n " , sanitizeScriptName (script .Name )))
312- masterScript .WriteString (fmt .Sprintf ("\t \t if ps -p $(cat %s_cmd.pid) > /dev/null; then\n " , sanitizeScriptName (script .Name )))
313- // send SIGINT to the background process first, then SIGKILL if it doesn't respond to SIGINT
314- masterScript .WriteString (fmt .Sprintf ("\t \t \t kill -SIGINT $(cat %s_cmd.pid)\n " , sanitizeScriptName (script .Name )))
315- // give the process a chance to respond to SIGINT
316- masterScript .WriteString ("\t \t \t sleep 0.5\n " )
317- // if the background process is still running, send SIGKILL
318- masterScript .WriteString (fmt .Sprintf ("\t \t \t if ps -p $(cat %s_cmd.pid) > /dev/null; then\n " , sanitizeScriptName (script .Name )))
319- masterScript .WriteString (fmt .Sprintf ("\t \t \t \t kill -SIGKILL $(cat %s_cmd.pid)\n " , sanitizeScriptName (script .Name )))
320- masterScript .WriteString (fmt .Sprintf ("\t \t \t \t %s_exitcode=137\n " , sanitizeScriptName (script .Name ))) // 137 is the exit code for SIGKILL
321- masterScript .WriteString ("\t \t \t else\n " )
322- // if the background process has exited, set the exit code to 0
323- masterScript .WriteString (fmt .Sprintf ("\t \t \t \t %s_exitcode=0\n " , sanitizeScriptName (script .Name )))
324- masterScript .WriteString ("\t \t \t fi\n " )
325- masterScript .WriteString ("\t \t else\n " )
326- // if the script itself has exited, set the exit code to 0
327- masterScript .WriteString (fmt .Sprintf ("\t \t \t %s_exitcode=0\n " , sanitizeScriptName (script .Name )))
328- masterScript .WriteString ("\t \t fi\n " )
329- masterScript .WriteString ("\t else\n " )
330- // if the *cmd.pid file doesn't exist, set the exit code to 1
331- masterScript .WriteString (fmt .Sprintf ("\t \t %s_exitcode=0\n " , sanitizeScriptName (script .Name )))
332- masterScript .WriteString ("\t fi\n " )
333- } else {
334- masterScript .WriteString (fmt .Sprintf ("\t wait \" $%s_pid\" \n " , sanitizeScriptName (script .Name )))
335- masterScript .WriteString (fmt .Sprintf ("\t %s_exitcode=$?\n " , sanitizeScriptName (script .Name )))
336- }
337- }
338- masterScript .WriteString ("\t print_output\n " )
339- masterScript .WriteString ("\t exit 0\n " )
340- masterScript .WriteString ("}\n " )
326+ start_scripts() {
327+ for s in "${scripts[@]}"; do
328+ bash "$script_dir/${s}.sh" > "$script_dir/${s}.stdout" 2> "$script_dir/${s}.stderr" &
329+ pids[$s]=$!
330+ done
331+ }
332+
333+ kill_script() {
334+ local s="$1"
335+ local pid="${pids[$s]:-}"
336+ [[ -z "$pid" ]] && return 0
337+ if ! ps -p "$pid" > /dev/null 2>&1; then return 0; fi
338+ if [[ "${needs_kill[$s]}" == "1" && -f "${s}_cmd.pid" ]]; then
339+ local bgpid
340+ bgpid="$(cat "${s}_cmd.pid" 2>/dev/null || true)"
341+ if [[ -n "$bgpid" && $(ps -p "$bgpid" -o pid= 2>/dev/null) ]]; then
342+ kill -SIGINT "$bgpid" 2>/dev/null || true
343+ sleep 0.5
344+ if ps -p "$bgpid" > /dev/null 2>&1; then
345+ kill -SIGKILL "$bgpid" 2>/dev/null || true
346+ exitcodes[$s]=137
347+ else
348+ exitcodes[$s]=0
349+ fi
350+ fi
351+ else
352+ kill -SIGINT "$pid" 2>/dev/null || true
353+ wait "$pid" 2>/dev/null || true
354+ if [[ -z "${exitcodes[$s]:-}" ]]; then exitcodes[$s]=130; fi
355+ fi
356+ }
341357
342- // call handle_sigint func when SIGINT is received
343- masterScript .WriteString ("\n trap handle_sigint SIGINT\n " )
358+ wait_for_scripts() {
359+ for s in "${scripts[@]}"; do
360+ if wait "${pids[$s]}"; then
361+ exitcodes[$s]=0
362+ else
363+ ec=$?
364+ exitcodes[$s]=$ec
365+ fi
366+ done
367+ }
344368
345- // run all parallel scripts in the background
346- masterScript .WriteString ("\n " )
347- needsElevatedPrivileges := false
348- for _ , script := range parallelScripts {
349- if script .Superuser {
350- needsElevatedPrivileges = true
351- }
352- masterScript .WriteString (
353- fmt .Sprintf ("bash %s > %s 2>%s &\n " ,
354- path .Join ("$script_dir" , scriptNameToFilename (script .Name )),
355- path .Join ("$script_dir" , sanitizeScriptName (script .Name )+ ".stdout" ),
356- path .Join ("$script_dir" , sanitizeScriptName (script .Name )+ ".stderr" ),
357- ),
358- )
359- masterScript .WriteString (fmt .Sprintf ("%s_pid=$!\n " , sanitizeScriptName (script .Name )))
360- }
369+ print_summary() {
370+ for s in "${scripts[@]}"; do
371+ echo "<---------------------->"
372+ echo "SCRIPT NAME: ${orig_names[$s]}"
373+ echo "STDOUT:"; cat "$script_dir/${s}.stdout" || true
374+ echo "STDERR:"; cat "$script_dir/${s}.stderr" || true
375+ echo "EXIT CODE: ${exitcodes[$s]:-1}"
376+ done
377+ }
361378
362- // wait for all parallel scripts to finish then print their output
363- masterScript .WriteString ("\n " )
364- for _ , script := range parallelScripts {
365- masterScript .WriteString (fmt .Sprintf ("wait \" $%s_pid\" \n " , sanitizeScriptName (script .Name )))
366- masterScript .WriteString (fmt .Sprintf ("%s_exitcode=$?\n " , sanitizeScriptName (script .Name )))
367- }
368- masterScript .WriteString ("\n print_output\n " )
379+ handle_sigint() {
380+ echo "Received SIGINT; attempting graceful shutdown" >&2
381+ for s in "${scripts[@]}"; do
382+ kill_script "$s"
383+ done
384+ print_summary
385+ exit 0
386+ }
387+
388+ trap handle_sigint SIGINT
369389
370- return masterScript .String (), needsElevatedPrivileges
390+ start_scripts
391+ wait_for_scripts
392+ print_summary
393+ `
394+ tmpl , err := template .New ("master" ).Parse (masterScriptTemplate )
395+ if err != nil {
396+ slog .Error ("failed to parse master script template" , slog .String ("error" , err .Error ()))
397+ return "" , needsElevated , err
398+ }
399+ var out strings.Builder
400+ if err = tmpl .Execute (& out , data ); err != nil {
401+ slog .Error ("failed to execute master script template" , slog .String ("error" , err .Error ()))
402+ return "" , needsElevated , err
403+ }
404+ return out .String (), needsElevated , nil
371405}
372406
373407// parseMasterScriptOutput parses the output of the master script that runs all parallel scripts in the background.
0 commit comments