mirror of
https://github.com/gesellix/Bose-SoundTouch.git
synced 2026-08-18 16:46:17 +00:00
feat(setup): run telnet preflight in parallel with SSH probes
GetMigrationSummary now kicks off telnetPreflight in a goroutine at entry and merges the four Telnet* fields into the main summary just before returning. Wall time becomes max(ssh, telnet); the two transports are queried independently and their results combined — SSH retains visibility into /etc/hosts, /etc/resolv.conf and the on-device XML config, while telnet contributes the live URL set readable via `getpdo CurrentSystemConfiguration` without root. Race-free by construction: the goroutine writes to its own MigrationSummary instance and only the four telnet fields are copied back. Verified with `go test -race`. Tests cover telnet-only, ssh-only, and both-succeed paths. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
c84cfeb757
commit
91ba28c52e
@@ -0,0 +1,139 @@
|
||||
package setup
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// telnetSummaryEnv builds a Manager whose:
|
||||
// - SSH client is the supplied mockSSH (or a no-op if nil).
|
||||
// - Telnet client is the supplied fakeTelnet.
|
||||
// - Live :8090/info call hits an httptest server returning a minimal XML.
|
||||
//
|
||||
// The deviceIP returned is the httptest server's listener addr ("host:port"),
|
||||
// so the live-info call works; the SSH and telnet clients ignore the addr
|
||||
// and return whatever the fakes are scripted to return.
|
||||
func telnetSummaryEnv(t *testing.T, ssh *mockSSH, ft *fakeTelnet) (*Manager, string, func()) {
|
||||
t.Helper()
|
||||
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/xml")
|
||||
_, _ = fmt.Fprint(w, `<info deviceID="123"><name>Test</name></info>`)
|
||||
}))
|
||||
|
||||
m := NewManager("http://example:8000", nil, nil)
|
||||
m.NewSSH = func(string) SSHClient {
|
||||
if ssh != nil {
|
||||
return ssh
|
||||
}
|
||||
return &mockSSH{runFunc: func(string) (string, error) { return "", errors.New("ssh disabled in test") }}
|
||||
}
|
||||
m.NewTelnet = func(string) TelnetClient { return ft }
|
||||
|
||||
return m, server.Listener.Addr().String(), server.Close
|
||||
}
|
||||
|
||||
func TestGetMigrationSummary_TelnetSucceedsSSHFails(t *testing.T) {
|
||||
target := "http://example:8000"
|
||||
ft := &fakeTelnet{
|
||||
banner: "BoseShell\n-> ",
|
||||
responses: map[string]string{
|
||||
"getpdo CurrentSystemConfiguration": "margeServerUrl=" + target + "\n",
|
||||
},
|
||||
}
|
||||
|
||||
m, host, cleanup := telnetSummaryEnv(t, nil, ft)
|
||||
defer cleanup()
|
||||
|
||||
summary, err := m.GetMigrationSummary(host, "", "", nil)
|
||||
if err != nil {
|
||||
t.Fatalf("GetMigrationSummary: %v", err)
|
||||
}
|
||||
|
||||
if summary.SSHSuccess {
|
||||
t.Errorf("SSHSuccess = true, want false")
|
||||
}
|
||||
|
||||
if !summary.TelnetReachable {
|
||||
t.Errorf("TelnetReachable = false, want true")
|
||||
}
|
||||
|
||||
if !strings.Contains(summary.TelnetBanner, "BoseShell") {
|
||||
t.Errorf("TelnetBanner = %q, want it to contain BoseShell", summary.TelnetBanner)
|
||||
}
|
||||
|
||||
if !strings.Contains(summary.TelnetVerifiedConfig, target) {
|
||||
t.Errorf("TelnetVerifiedConfig = %q, want it to contain %q", summary.TelnetVerifiedConfig, target)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetMigrationSummary_TelnetFailsSSHFails(t *testing.T) {
|
||||
ft := &fakeTelnet{dialErr: errors.New("connection refused")}
|
||||
|
||||
m, host, cleanup := telnetSummaryEnv(t, nil, ft)
|
||||
defer cleanup()
|
||||
|
||||
summary, err := m.GetMigrationSummary(host, "", "", nil)
|
||||
if err != nil {
|
||||
t.Fatalf("GetMigrationSummary: %v", err)
|
||||
}
|
||||
|
||||
if summary.SSHSuccess {
|
||||
t.Errorf("SSHSuccess = true, want false")
|
||||
}
|
||||
|
||||
if summary.TelnetReachable {
|
||||
t.Errorf("TelnetReachable = true, want false")
|
||||
}
|
||||
|
||||
if !strings.Contains(summary.TelnetProbeError, "connection refused") {
|
||||
t.Errorf("TelnetProbeError = %q, want connection refused", summary.TelnetProbeError)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetMigrationSummary_TelnetSucceedsSSHSucceeds(t *testing.T) {
|
||||
target := "http://example:8000"
|
||||
ft := &fakeTelnet{
|
||||
responses: map[string]string{
|
||||
"getpdo CurrentSystemConfiguration": "margeServerUrl=" + target + "\n",
|
||||
},
|
||||
}
|
||||
|
||||
// SSH mock returns enough for SSHSuccess to be true (cat /opt/Bose/etc/...).
|
||||
ssh := &mockSSH{
|
||||
runFunc: func(cmd string) (string, error) {
|
||||
switch {
|
||||
case strings.HasPrefix(cmd, "cat "+SoundTouchSdkPrivateCfgPath):
|
||||
return `<?xml version="1.0"?><SoundTouchSdkPrivateCfg><margeServerUrl>` + target + `</margeServerUrl></SoundTouchSdkPrivateCfg>`, nil
|
||||
case strings.HasPrefix(cmd, "[ -f"):
|
||||
return "", errors.New("not found")
|
||||
default:
|
||||
return "", nil
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
m, host, cleanup := telnetSummaryEnv(t, ssh, ft)
|
||||
defer cleanup()
|
||||
|
||||
summary, err := m.GetMigrationSummary(host, "", "", nil)
|
||||
if err != nil {
|
||||
t.Fatalf("GetMigrationSummary: %v", err)
|
||||
}
|
||||
|
||||
if !summary.SSHSuccess {
|
||||
t.Errorf("SSHSuccess = false, want true")
|
||||
}
|
||||
|
||||
if !summary.TelnetReachable {
|
||||
t.Errorf("TelnetReachable = false, want true")
|
||||
}
|
||||
|
||||
if !strings.Contains(summary.TelnetVerifiedConfig, target) {
|
||||
t.Errorf("TelnetVerifiedConfig = %q, want %q", summary.TelnetVerifiedConfig, target)
|
||||
}
|
||||
}
|
||||
@@ -244,6 +244,20 @@ func (m *Manager) GetMigrationSummary(deviceIP, targetURL, proxyURL string, opti
|
||||
SSHSuccess: false,
|
||||
}
|
||||
|
||||
// Run the telnet preflight in parallel with the SSH-based probes below.
|
||||
// Both transports are queried independently: SSH gives access to
|
||||
// /etc/hosts, /etc/resolv.conf and the on-device XML config; telnet's
|
||||
// `getpdo CurrentSystemConfiguration` reports the live URL set without
|
||||
// needing root. They are complementary, so we wait for both and merge
|
||||
// the results — total wall time = max(ssh, telnet).
|
||||
telnetCh := make(chan MigrationSummary, 1)
|
||||
|
||||
go func() {
|
||||
var local MigrationSummary
|
||||
m.telnetPreflight(&local, deviceIP)
|
||||
telnetCh <- local
|
||||
}()
|
||||
|
||||
// Populate device info from datastore and live info
|
||||
m.populateDeviceInfo(summary, deviceIP)
|
||||
|
||||
@@ -322,6 +336,13 @@ func (m *Manager) GetMigrationSummary(deviceIP, targetURL, proxyURL string, opti
|
||||
}
|
||||
}
|
||||
|
||||
// 8. Merge telnet preflight results (started in parallel at the top).
|
||||
telnetResult := <-telnetCh
|
||||
summary.TelnetReachable = telnetResult.TelnetReachable
|
||||
summary.TelnetBanner = telnetResult.TelnetBanner
|
||||
summary.TelnetVerifiedConfig = telnetResult.TelnetVerifiedConfig
|
||||
summary.TelnetProbeError = telnetResult.TelnetProbeError
|
||||
|
||||
return summary, nil
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user