fix: findscu go mkiso

This commit is contained in:
2026-06-05 08:33:09 +07:00
parent 983667a76a
commit 1142c86e70
2 changed files with 135 additions and 41 deletions

View File

@@ -4,6 +4,7 @@ import (
"context"
"fmt"
"log/slog"
"path/filepath"
"sync"
"time"
@@ -78,7 +79,17 @@ func (s *DicomService) FetchDICOMMultiple(ctx context.Context, accessionNumbers
default:
}
exitCode, _, stderr, err := dicom.RunMoveSCU(
studyUIDs, err := s.findStudyUIDs(ctx, acc)
if err != nil {
slog.Warn("findscu failed for accession",
"accession", acc,
"error", err,
)
continue
}
for _, studyUID := range studyUIDs {
exitCode, _, stderr, err := dicom.RunMoveSCUByStudyUID(
ctx,
s.cfg.DCMTK.Movescu,
s.cfg.OurAE.AETitle,
@@ -86,24 +97,26 @@ func (s *DicomService) FetchDICOMMultiple(ctx context.Context, accessionNumbers
s.cfg.PACS.Host,
s.cfg.PACS.Port,
port,
acc,
studyUID,
300, // 5 min timeout
)
if err != nil {
slog.Warn("movescu failed for accession",
slog.Warn("movescu failed for study uid",
"accession", acc,
"study_uid", studyUID,
"exit_code", exitCode,
"stderr", stderr,
"error", err,
)
// Continue with next accession — partial success is acceptable
continue
}
slog.Info("movescu completed",
"accession", acc,
"study_uid", studyUID,
"exit_code", exitCode,
)
}
}
// Count files retrieved
filesCount, err := countFiles(destDir)
@@ -140,8 +153,13 @@ func (s *DicomService) fetchDICOMWithPort(ctx context.Context, accessionNumber,
return 0, err
}
// Run movescu
exitCode, _, stderr, err := dicom.RunMoveSCU(
studyUIDs, err := s.findStudyUIDs(ctx, accessionNumber)
if err != nil {
return 0, err
}
for _, studyUID := range studyUIDs {
exitCode, _, stderr, err := dicom.RunMoveSCUByStudyUID(
ctx,
s.cfg.DCMTK.Movescu,
s.cfg.OurAE.AETitle,
@@ -149,17 +167,19 @@ func (s *DicomService) fetchDICOMWithPort(ctx context.Context, accessionNumber,
s.cfg.PACS.Host,
s.cfg.PACS.Port,
port,
accessionNumber,
studyUID,
300, // 5 min timeout
)
if err != nil {
return 0, fmt.Errorf("movescu failed (exit %d): %s (stderr: %s)", exitCode, err, stderr)
return 0, fmt.Errorf("movescu failed for study uid %q (exit %d): %s (stderr: %s)", studyUID, exitCode, err, stderr)
}
slog.Info("movescu completed",
"accession", accessionNumber,
"study_uid", studyUID,
"exit_code", exitCode,
)
}
// Count files
filesCount, err = countFiles(destDir)
@@ -174,6 +194,24 @@ func (s *DicomService) fetchDICOMWithPort(ctx context.Context, accessionNumber,
return filesCount, nil
}
func (s *DicomService) findStudyUIDs(ctx context.Context, accessionNumber string) ([]string, error) {
findscuBin := filepath.Join(filepath.Dir(s.cfg.DCMTK.Movescu), "findscu")
studyUIDs, _, stderr, err := dicom.RunFindSCUStudyUIDs(
ctx,
findscuBin,
s.cfg.OurAE.AETitle,
s.cfg.PACS.AETitle,
s.cfg.PACS.Host,
s.cfg.PACS.Port,
accessionNumber,
60,
)
if err != nil {
return nil, fmt.Errorf("findscu failed for accession %q: %w (stderr: %s)", accessionNumber, err, stderr)
}
return studyUIDs, nil
}
// waitForStorescpReady waits for storescp to start or detects early failure.
func waitForStorescpReady(resultCh <-chan dicom.StorescpResult) error {
select {

View File

@@ -7,6 +7,7 @@ import (
"log/slog"
"os"
"os/exec"
"regexp"
"strconv"
"strings"
"sync"
@@ -101,17 +102,72 @@ func StartStoresCP(ctx context.Context, bin, aeTitle string, port int, outputDir
return ch, stop, nil
}
// RunMoveSCU executes movescu for the given accession number.
// RunFindSCUStudyUIDs queries PACS by accession number and returns matching StudyInstanceUID values.
func RunFindSCUStudyUIDs(ctx context.Context, bin, ourAE, pacsAE, pacsHost string, pacsPort int, accessionNumber string, timeoutSec int) (studyUIDs []string, stdout, stderr string, err error) {
args := []string{
"-v",
"-aet", ourAE,
"-aec", pacsAE,
pacsHost, strconv.Itoa(pacsPort),
"-S",
"-k", "0008,0052=STUDY",
"-k", fmt.Sprintf("0008,0050=%s", accessionNumber),
"-k", "0020,000D",
}
if timeoutSec > 0 {
args = append(args, "-to", strconv.Itoa(timeoutSec))
}
slog.Info("running findscu",
"bin", bin,
"accession", accessionNumber,
"pacs", fmt.Sprintf("%s@%s:%d", pacsAE, pacsHost, pacsPort),
)
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()
combined := stdout + "\n" + stderr
if err != nil {
return nil, stdout, stderr, fmt.Errorf("findscu failed: %w", err)
}
uidRe := regexp.MustCompile(`\(0020,000d\) UI \[([^\]]+)\]`)
seen := make(map[string]bool)
for _, match := range uidRe.FindAllStringSubmatch(combined, -1) {
uid := strings.TrimSpace(match[1])
if uid == "" || seen[uid] {
continue
}
seen[uid] = true
studyUIDs = append(studyUIDs, uid)
}
if len(studyUIDs) == 0 {
return nil, stdout, stderr, fmt.Errorf("no StudyInstanceUID found for accession %q", accessionNumber)
}
return studyUIDs, stdout, stderr, nil
}
// RunMoveSCUByStudyUID executes movescu for the given StudyInstanceUID.
// 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) {
func RunMoveSCUByStudyUID(ctx context.Context, bin, ourAE, pacsAE, pacsHost string, pacsPort, moveDestPort int, studyUID 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)
"-k", "0008,0052=STUDY",
"-k", fmt.Sprintf("0020,000D=%s", studyUID),
"--no-port",
}
@@ -121,7 +177,7 @@ func RunMoveSCU(ctx context.Context, bin, ourAE, pacsAE, pacsHost string, pacsPo
slog.Info("running movescu",
"bin", bin,
"accession", accessionNumber,
"study_uid", studyUID,
"pacs", fmt.Sprintf("%s@%s:%d", pacsAE, pacsHost, pacsPort),
"dest", fmt.Sprintf("%s:%d", ourAE, moveDestPort),
)