feat: base go mkiso
This commit is contained in:
226
pkg/dicom/command.go
Normal file
226
pkg/dicom/command.go
Normal file
@@ -0,0 +1,226 @@
|
||||
package dicom
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// Global port allocator for concurrent safety.
|
||||
var (
|
||||
portMu sync.Mutex
|
||||
portInUse = make(map[int]bool)
|
||||
)
|
||||
|
||||
// allocatePort picks a free port from the range [base, base+range).
|
||||
func allocatePort(base, size int) (int, func()) {
|
||||
portMu.Lock()
|
||||
defer portMu.Unlock()
|
||||
|
||||
for i := 0; i < size; i++ {
|
||||
port := base + i
|
||||
if !portInUse[port] {
|
||||
portInUse[port] = true
|
||||
return port, func() {
|
||||
portMu.Lock()
|
||||
delete(portInUse, port)
|
||||
portMu.Unlock()
|
||||
}
|
||||
}
|
||||
}
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
// StorescpResult holds the outcome of a storescp process.
|
||||
type StorescpResult struct {
|
||||
PID int
|
||||
Port int
|
||||
Stderr string
|
||||
Err error
|
||||
}
|
||||
|
||||
// StartStoresCP launches storescp in the background.
|
||||
// Returns a result channel that will receive the outcome when the process exits,
|
||||
// plus a stop function to kill the process, and an error if startup failed.
|
||||
func StartStoresCP(ctx context.Context, bin, aeTitle string, port int, outputDir string) (resultCh <-chan StorescpResult, stop func(), err error) {
|
||||
args := []string{
|
||||
strconv.Itoa(port),
|
||||
"-aet", aeTitle,
|
||||
"-od", outputDir,
|
||||
"+xa", // accept all transfer syntaxes
|
||||
}
|
||||
|
||||
slog.Info("starting storescp",
|
||||
"bin", bin,
|
||||
"port", port,
|
||||
"ae", aeTitle,
|
||||
"dir", outputDir,
|
||||
)
|
||||
|
||||
cmd := exec.CommandContext(ctx, bin, args...)
|
||||
|
||||
// Redirect stderr to a buffer (storescp logs association info to stderr)
|
||||
var stderrBuf bytes.Buffer
|
||||
cmd.Stderr = &stderrBuf
|
||||
cmd.Stdout = nil // storescp doesn't use stdout
|
||||
|
||||
if err := cmd.Start(); err != nil {
|
||||
return nil, nil, fmt.Errorf("start storescp: %w", err)
|
||||
}
|
||||
|
||||
pid := cmd.Process.Pid
|
||||
ch := make(chan StorescpResult, 1)
|
||||
|
||||
go func() {
|
||||
err := cmd.Wait()
|
||||
ch <- StorescpResult{
|
||||
PID: pid,
|
||||
Port: port,
|
||||
Stderr: stderrBuf.String(),
|
||||
Err: err,
|
||||
}
|
||||
close(ch)
|
||||
}()
|
||||
|
||||
stop = func() {
|
||||
if cmd.Process != nil {
|
||||
slog.Info("stopping storescp", "pid", pid, "port", port)
|
||||
cmd.Process.Signal(os.Interrupt) // SIGINT for clean shutdown
|
||||
go func() {
|
||||
cmd.Wait()
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
return ch, stop, nil
|
||||
}
|
||||
|
||||
// RunMoveSCU executes movescu for the given accession number.
|
||||
// It uses Study Root query/retrieve model (via -S flag).
|
||||
func RunMoveSCU(ctx context.Context, bin, ourAE, pacsAE, pacsHost string, pacsPort, moveDestPort int, accessionNumber string, timeoutSec int) (exitCode int, stdout, stderr string, err error) {
|
||||
args := []string{
|
||||
"-aet", ourAE,
|
||||
"-aec", pacsAE,
|
||||
"-aem", ourAE,
|
||||
pacsHost, strconv.Itoa(pacsPort),
|
||||
"-S", // Study Root query/retrieve
|
||||
"-k", fmt.Sprintf("0008,0050=%s", accessionNumber),
|
||||
"-k", "0010,0020=", // Patient ID wildcard (required for Study Root)
|
||||
"--no-port",
|
||||
}
|
||||
|
||||
if timeoutSec > 0 {
|
||||
args = append(args, "-to", strconv.Itoa(timeoutSec))
|
||||
}
|
||||
|
||||
slog.Info("running movescu",
|
||||
"bin", bin,
|
||||
"accession", accessionNumber,
|
||||
"pacs", fmt.Sprintf("%s@%s:%d", pacsAE, pacsHost, pacsPort),
|
||||
"dest", fmt.Sprintf("%s:%d", ourAE, moveDestPort),
|
||||
)
|
||||
|
||||
cmd := exec.CommandContext(ctx, bin, args...)
|
||||
var outBuf, errBuf bytes.Buffer
|
||||
cmd.Stdout = &outBuf
|
||||
cmd.Stderr = &errBuf
|
||||
|
||||
err = cmd.Run()
|
||||
stdout = outBuf.String()
|
||||
stderr = errBuf.String()
|
||||
|
||||
if err != nil {
|
||||
if exitErr, ok := err.(*exec.ExitError); ok {
|
||||
exitCode = exitErr.ExitCode()
|
||||
} else {
|
||||
exitCode = -1
|
||||
}
|
||||
return exitCode, stdout, stderr, fmt.Errorf("movescu failed (exit %d): %s", exitCode, strings.TrimSpace(stderr))
|
||||
}
|
||||
|
||||
return 0, stdout, stderr, nil
|
||||
}
|
||||
|
||||
// RunStoreSCU sends DICOM files to a remote AE via C-STORE.
|
||||
// Equivalent to: storescu -aet <ae> +sd +r <host> <port> <dir>
|
||||
func RunStoreSCU(ctx context.Context, bin, aeTitle, host string, port int, dir string) (exitCode int, stdout, stderr string, err error) {
|
||||
args := []string{
|
||||
"-aet", aeTitle,
|
||||
"+sd", // scan directories
|
||||
"+r", // recurse
|
||||
host,
|
||||
strconv.Itoa(port),
|
||||
dir,
|
||||
}
|
||||
|
||||
slog.Info("running storescu",
|
||||
"bin", bin,
|
||||
"ae", aeTitle,
|
||||
"destination", fmt.Sprintf("%s:%d", host, port),
|
||||
"dir", dir,
|
||||
)
|
||||
|
||||
cmd := exec.CommandContext(ctx, bin, args...)
|
||||
var outBuf, errBuf bytes.Buffer
|
||||
cmd.Stdout = &outBuf
|
||||
cmd.Stderr = &errBuf
|
||||
|
||||
err = cmd.Run()
|
||||
stdout = outBuf.String()
|
||||
stderr = errBuf.String()
|
||||
|
||||
if err != nil {
|
||||
if exitErr, ok := err.(*exec.ExitError); ok {
|
||||
exitCode = exitErr.ExitCode()
|
||||
} else {
|
||||
exitCode = -1
|
||||
}
|
||||
return exitCode, stdout, stderr, fmt.Errorf("storescu failed (exit %d): %s", exitCode, strings.TrimSpace(stderr))
|
||||
}
|
||||
|
||||
return 0, stdout, stderr, nil
|
||||
}
|
||||
|
||||
// CopyDir copies a directory recursively using cp -r.
|
||||
func CopyDir(src, dst string) error {
|
||||
cmd := exec.Command("/bin/cp", "-r", src, dst)
|
||||
var stderr bytes.Buffer
|
||||
cmd.Stderr = &stderr
|
||||
if err := cmd.Run(); err != nil {
|
||||
return fmt.Errorf("copy %s -> %s: %w (stderr: %s)", src, dst, err, strings.TrimSpace(stderr.String()))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// FileExists checks if a path exists and is a regular file.
|
||||
func FileExists(path string) bool {
|
||||
info, err := os.Stat(path)
|
||||
return err == nil && !info.IsDir()
|
||||
}
|
||||
|
||||
// DirExists checks if a path exists and is a directory.
|
||||
func DirExists(path string) bool {
|
||||
info, err := os.Stat(path)
|
||||
return err == nil && info.IsDir()
|
||||
}
|
||||
|
||||
// CountFiles counts regular files in a directory (non-recursive).
|
||||
func CountFiles(dir string) (int, error) {
|
||||
dirents, err := os.ReadDir(dir)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("read dir %q: %w", dir, err)
|
||||
}
|
||||
count := 0
|
||||
for _, de := range dirents {
|
||||
if !de.IsDir() {
|
||||
count++
|
||||
}
|
||||
}
|
||||
return count, nil
|
||||
}
|
||||
Reference in New Issue
Block a user