Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
202 changes: 118 additions & 84 deletions internal/script/script.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
"slices"
"strconv"
"strings"
"text/template"

"perfspect/internal/cpus"
"perfspect/internal/target"
Expand Down Expand Up @@ -93,7 +94,11 @@ func RunScripts(myTarget target.Target, scripts []ScriptDefinition, ignoreScript
if len(parallelScripts) > 0 {
// form one master script that calls all the parallel scripts in the background
masterScriptName := "parallel_master.sh"
masterScript, needsElevatedPrivileges := formMasterScript(myTarget.GetTempDirectory(), parallelScripts)
masterScript, needsElevatedPrivileges, err := formMasterScript(myTarget.GetTempDirectory(), parallelScripts)
if err != nil {
err = fmt.Errorf("error forming master script: %v", err)
return nil, err
}
// write master script to local file
masterScriptPath := path.Join(localTempDir, myTarget.GetName(), masterScriptName)
err = os.WriteFile(masterScriptPath, []byte(masterScript), 0600)
Expand Down Expand Up @@ -277,97 +282,126 @@ func scriptNameToFilename(name string) string {

// 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.
// Return values are the master script and a boolean indicating whether the master script requires elevated privileges.
func formMasterScript(targetTempDirectory string, parallelScripts []ScriptDefinition) (string, bool) {
// we write the stdout and stderr from each command to temporary files and save the PID of each command
// in a variable named after the script
var masterScript strings.Builder
func formMasterScript(targetTempDirectory string, parallelScripts []ScriptDefinition) (string, bool, error) {
// data model for template
type tplScript struct {
Name string
Sanitized string
NeedsKill bool
Superuser bool
}
data := struct {
TargetTempDir string
Scripts []tplScript
}{}
data.TargetTempDir = targetTempDirectory
needsElevated := false
for _, s := range parallelScripts {
if s.Superuser {
needsElevated = true
}
data.Scripts = append(data.Scripts, tplScript{
Name: s.Name, Sanitized: sanitizeScriptName(s.Name), NeedsKill: s.NeedsKill, Superuser: s.Superuser,
})
}
const masterScriptTemplate = `#!/usr/bin/env bash
set -o errexit
set -o pipefail

masterScript.WriteString("#!/bin/bash\n")
script_dir={{.TargetTempDir}}
cd "$script_dir"

// set dir var and change working directory to dir in case any of the scripts write out temporary files
masterScript.WriteString(fmt.Sprintf("script_dir=%s\n", targetTempDirectory))
masterScript.WriteString("cd $script_dir\n")
declare -a scripts=()
declare -A needs_kill=()
declare -A pids=()
declare -A exitcodes=()
declare -A orig_names=()

// function to print the output of each script
masterScript.WriteString("\nprint_output() {\n")
for _, script := range parallelScripts {
masterScript.WriteString("\techo \"<---------------------->\"\n")
masterScript.WriteString(fmt.Sprintf("\techo SCRIPT NAME: %s\n", script.Name))
masterScript.WriteString(fmt.Sprintf("\techo STDOUT:\n\tcat %s\n", path.Join("$script_dir", sanitizeScriptName(script.Name)+".stdout")))
masterScript.WriteString(fmt.Sprintf("\techo STDERR:\n\tcat %s\n", path.Join("$script_dir", sanitizeScriptName(script.Name)+".stderr")))
masterScript.WriteString(fmt.Sprintf("\techo EXIT CODE: $%s_exitcode\n", sanitizeScriptName(script.Name)))
}
masterScript.WriteString("}\n")
{{- range .Scripts}}
scripts+=({{ .Sanitized }})
needs_kill[{{ .Sanitized }}]={{ if .NeedsKill }}1{{ else }}0{{ end }}
orig_names[{{ .Sanitized }}]="{{ .Name }}"
{{ end }}

// function to handle SIGINT
masterScript.WriteString("\nhandle_sigint() {\n")
for _, script := range parallelScripts {
// send SIGINT to the child script, if it is still running
masterScript.WriteString(fmt.Sprintf("\tif ps -p \"$%s_pid\" > /dev/null; then\n", sanitizeScriptName(script.Name)))
masterScript.WriteString(fmt.Sprintf("\t\tkill -SIGINT $%s_pid\n", sanitizeScriptName(script.Name)))
masterScript.WriteString("\tfi\n")
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
// if the *cmd.pid file exists, check if the process is still running
masterScript.WriteString(fmt.Sprintf("\tif [ -f %s_cmd.pid ]; then\n", sanitizeScriptName(script.Name)))
masterScript.WriteString(fmt.Sprintf("\t\tif ps -p $(cat %s_cmd.pid) > /dev/null; then\n", sanitizeScriptName(script.Name)))
// send SIGINT to the background process first, then SIGKILL if it doesn't respond to SIGINT
masterScript.WriteString(fmt.Sprintf("\t\t\tkill -SIGINT $(cat %s_cmd.pid)\n", sanitizeScriptName(script.Name)))
// give the process a chance to respond to SIGINT
masterScript.WriteString("\t\t\tsleep 0.5\n")
// if the background process is still running, send SIGKILL
masterScript.WriteString(fmt.Sprintf("\t\t\tif ps -p $(cat %s_cmd.pid) > /dev/null; then\n", sanitizeScriptName(script.Name)))
masterScript.WriteString(fmt.Sprintf("\t\t\t\tkill -SIGKILL $(cat %s_cmd.pid)\n", sanitizeScriptName(script.Name)))
masterScript.WriteString(fmt.Sprintf("\t\t\t\t%s_exitcode=137\n", sanitizeScriptName(script.Name))) // 137 is the exit code for SIGKILL
masterScript.WriteString("\t\t\telse\n")
// if the background process has exited, set the exit code to 0
masterScript.WriteString(fmt.Sprintf("\t\t\t\t%s_exitcode=0\n", sanitizeScriptName(script.Name)))
masterScript.WriteString("\t\t\tfi\n")
masterScript.WriteString("\t\telse\n")
// if the script itself has exited, set the exit code to 0
masterScript.WriteString(fmt.Sprintf("\t\t\t%s_exitcode=0\n", sanitizeScriptName(script.Name)))
masterScript.WriteString("\t\tfi\n")
masterScript.WriteString("\telse\n")
// if the *cmd.pid file doesn't exist, set the exit code to 1
masterScript.WriteString(fmt.Sprintf("\t\t%s_exitcode=0\n", sanitizeScriptName(script.Name)))
masterScript.WriteString("\tfi\n")
} else {
masterScript.WriteString(fmt.Sprintf("\twait \"$%s_pid\"\n", sanitizeScriptName(script.Name)))
masterScript.WriteString(fmt.Sprintf("\t%s_exitcode=$?\n", sanitizeScriptName(script.Name)))
}
}
masterScript.WriteString("\tprint_output\n")
masterScript.WriteString("\texit 0\n")
masterScript.WriteString("}\n")
start_scripts() {
for s in "${scripts[@]}"; do
bash "$script_dir/${s}.sh" > "$script_dir/${s}.stdout" 2> "$script_dir/${s}.stderr" &
pids[$s]=$!
done
}

kill_script() {
local s="$1"
local pid="${pids[$s]:-}"
[[ -z "$pid" ]] && return 0
if ! ps -p "$pid" > /dev/null 2>&1; then return 0; fi
if [[ "${needs_kill[$s]}" == "1" && -f "${s}_cmd.pid" ]]; then
local bgpid
bgpid="$(cat "${s}_cmd.pid" 2>/dev/null || true)"
if [[ -n "$bgpid" && $(ps -p "$bgpid" -o pid= 2>/dev/null) ]]; then
kill -SIGINT "$bgpid" 2>/dev/null || true
sleep 0.5
if ps -p "$bgpid" > /dev/null 2>&1; then
kill -SIGKILL "$bgpid" 2>/dev/null || true
exitcodes[$s]=137
else
exitcodes[$s]=0
fi
fi
else
kill -SIGINT "$pid" 2>/dev/null || true
wait "$pid" 2>/dev/null || true
if [[ -z "${exitcodes[$s]:-}" ]]; then exitcodes[$s]=130; fi
fi
}

// call handle_sigint func when SIGINT is received
masterScript.WriteString("\ntrap handle_sigint SIGINT\n")
wait_for_scripts() {
for s in "${scripts[@]}"; do
if wait "${pids[$s]}"; then
exitcodes[$s]=0
else
ec=$?
exitcodes[$s]=$ec
fi
done
}

// run all parallel scripts in the background
masterScript.WriteString("\n")
needsElevatedPrivileges := false
for _, script := range parallelScripts {
if script.Superuser {
needsElevatedPrivileges = true
}
masterScript.WriteString(
fmt.Sprintf("bash %s > %s 2>%s &\n",
path.Join("$script_dir", scriptNameToFilename(script.Name)),
path.Join("$script_dir", sanitizeScriptName(script.Name)+".stdout"),
path.Join("$script_dir", sanitizeScriptName(script.Name)+".stderr"),
),
)
masterScript.WriteString(fmt.Sprintf("%s_pid=$!\n", sanitizeScriptName(script.Name)))
}
print_summary() {
for s in "${scripts[@]}"; do
echo "<---------------------->"
echo "SCRIPT NAME: ${orig_names[$s]}"
echo "STDOUT:"; cat "$script_dir/${s}.stdout" || true
echo "STDERR:"; cat "$script_dir/${s}.stderr" || true
echo "EXIT CODE: ${exitcodes[$s]:-1}"
done
}

// wait for all parallel scripts to finish then print their output
masterScript.WriteString("\n")
for _, script := range parallelScripts {
masterScript.WriteString(fmt.Sprintf("wait \"$%s_pid\"\n", sanitizeScriptName(script.Name)))
masterScript.WriteString(fmt.Sprintf("%s_exitcode=$?\n", sanitizeScriptName(script.Name)))
}
masterScript.WriteString("\nprint_output\n")
handle_sigint() {
echo "Received SIGINT; attempting graceful shutdown" >&2
for s in "${scripts[@]}"; do
kill_script "$s"
done
print_summary
exit 0
}

trap handle_sigint SIGINT

return masterScript.String(), needsElevatedPrivileges
start_scripts
wait_for_scripts
print_summary
`
tmpl, err := template.New("master").Parse(masterScriptTemplate)
if err != nil {
slog.Error("failed to parse master script template", slog.String("error", err.Error()))
return "", needsElevated, err
}
var out strings.Builder
if err = tmpl.Execute(&out, data); err != nil {
slog.Error("failed to execute master script template", slog.String("error", err.Error()))
return "", needsElevated, err
}
return out.String(), needsElevated, nil
}

// parseMasterScriptOutput parses the output of the master script that runs all parallel scripts in the background.
Expand Down
150 changes: 150 additions & 0 deletions internal/script/script_master_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
// Copyright (C) 2021-2025 Intel Corporation
// SPDX-License-Identifier: BSD-3-Clause
//
// Unit tests for formMasterScript and parseMasterScriptOutput.
// These tests validate:
// 1. Template structure contains expected sections and sanitized names.
// 2. Elevated privilege flag logic (returns true if any script is superuser).
// 3. Behavior with empty script slice.
// 4. Integration: executing generated master script with stub child scripts and parsing output.

package script

import (
"os"
"os/exec"
"path/filepath"
"strings"
"testing"
)

// minimal ScriptDefinition stub (using existing type) – fields used: Name, Superuser, NeedsKill.

func TestFormMasterScriptTemplateStructure(t *testing.T) {
scripts := []ScriptDefinition{
{Name: "alpha script", Superuser: false},
{Name: "beta-script", Superuser: true},
}
master, elevated, err := formMasterScript("/tmp/targetdir", scripts)
if err != nil {
t.Fatalf("error forming master script: %v", err)
}
if !elevated {
t.Fatalf("expected elevated=true when at least one script is superuser")
}
// Shebang
if !strings.HasPrefix(master, "#!/usr/bin/env bash") {
t.Errorf("master script missing shebang")
}
// Functions present
for _, fn := range []string{"start_scripts()", "kill_script()", "wait_for_scripts()", "print_summary()", "handle_sigint()"} {
if !strings.Contains(master, fn) {
t.Errorf("expected function %s in master script", fn)
}
}
// Sanitized names appear (spaces and hyphens replaced with underscores)
if !strings.Contains(master, "alpha_script") || !strings.Contains(master, "beta_script") {
t.Errorf("sanitized script names not found in template output")
}
// Mapping of original names present (orig_names associative array entries)
for _, mapping := range []string{"orig_names[alpha_script]=\"alpha script\"", "orig_names[beta_script]=\"beta-script\""} {
if !strings.Contains(master, mapping) {
t.Errorf("expected original name mapping %q in master script", mapping)
}
}
// Delimiter used for parsing
if !strings.Contains(master, "<---------------------->") {
t.Errorf("expected delimiter for parsing in master script")
}
}

func TestFormMasterScriptNeedsElevatedFlag(t *testing.T) {
scripts := []ScriptDefinition{{Name: "user", Superuser: false}, {Name: "also user", Superuser: false}}
_, elevated, err := formMasterScript("/tmp/dir", scripts)
if err != nil {
t.Fatalf("error forming master script: %v", err)
}
if elevated {
t.Fatalf("expected elevated=false when no scripts require superuser")
}
}

func TestFormMasterScriptEmptyScripts(t *testing.T) {
master, elevated, err := formMasterScript("/tmp/dir", nil)
if err != nil {
t.Fatalf("error forming master script: %v", err)
}
if elevated {
t.Fatalf("expected elevated=false with empty slice")
}
// Should still contain core function definitions even if no scripts.
if !strings.Contains(master, "start_scripts()") || !strings.Contains(master, "print_summary()") {
t.Errorf("template missing expected functions for empty slice")
}
t.Logf("MASTER SCRIPT EMPTY:\n%s", master)
// No orig_names assignments lines for empty slice.
if strings.Count(master, "orig_names[") > 0 {
for line := range strings.SplitSeq(master, "\n") {
if strings.Contains(line, "orig_names[") && strings.Contains(line, "]=") {
// assignment line detected
t.Errorf("no orig_names mappings should appear for empty slice")
}
}
}
}

func TestFormMasterScriptExecutionIntegration(t *testing.T) {
// Integration test: create temp directory, stub two child scripts, run master script, parse output.
tmp := t.TempDir()
scripts := []ScriptDefinition{{Name: "alpha script"}, {Name: "beta-script"}}
master, elevated, err := formMasterScript(tmp, scripts)
if err != nil {
t.Fatalf("error forming master script: %v", err)
}
if elevated { // none marked superuser
t.Fatalf("did not expect elevated=true for non-superuser scripts")
}
// Create child scripts.
for _, s := range scripts {
sanitized := sanitizeScriptName(s.Name)
childPath := filepath.Join(tmp, sanitized+".sh")
content := "#!/usr/bin/env bash\n" + "echo STDOUT-" + sanitized + "\n" + "echo STDERR-" + sanitized + " 1>&2\n"
if err := os.WriteFile(childPath, []byte(content), 0o700); err != nil {
t.Fatalf("failed writing child script %s: %v", childPath, err)
}
}
// Write master script.
masterPath := filepath.Join(tmp, "parallel_master.sh")
if err := os.WriteFile(masterPath, []byte(master), 0o700); err != nil {
t.Fatalf("failed writing master script: %v", err)
}
// Run master script.
out, err := runLocalBash(masterPath)
if err != nil {
// Read master script content for debugging
content, _ := os.ReadFile(masterPath)
t.Fatalf("error executing master script: %v\nstdout+stderr: %s\nMASTER SCRIPT:\n%s", err, out, string(content))
}
parsed := parseMasterScriptOutput(out)
if len(parsed) != 2 {
t.Fatalf("expected 2 parsed script outputs, got %d", len(parsed))
}
// Validate each output.
for _, p := range parsed {
if p.Exitcode != 0 { // child scripts exit 0
t.Errorf("expected exit code 0 for %s, got %d", p.Name, p.Exitcode)
}
if !strings.Contains(p.Stdout, "STDOUT-"+sanitizeScriptName(p.Name)) {
t.Errorf("stdout mismatch for %s: %q", p.Name, p.Stdout)
}
if !strings.Contains(p.Stderr, "STDERR-"+sanitizeScriptName(p.Name)) {
t.Errorf("stderr mismatch for %s: %q", p.Name, p.Stderr)
}
}
}

// runLocalBash executes a bash script locally and returns combined stdout.
func runLocalBash(scriptPath string) (string, error) {
outBytes, err := exec.Command("bash", scriptPath).CombinedOutput() // #nosec G204
return string(outBytes), err
}