Skip to content
Draft
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
29 changes: 24 additions & 5 deletions internal/commands/activate/activate.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,8 @@ func (cmd *ActivateCmd) Validate() error {
log.Trace("Entering Validate method of ActivateCmd")

// Determine if caller intends local activation (explicit --local or local-only flags)
cmd.URL = normalizeActivateURL(cmd.URL)

localIntent := cmd.Local || cmd.hasLocalActivationFlags()

// Resolve local-vs-remote precedence when both are present.
Expand All @@ -81,7 +83,6 @@ func (cmd *ActivateCmd) Validate() error {
if localIntent && cmd.URL != "" {
lowerURL := strings.ToLower(cmd.URL)
if strings.HasPrefix(lowerURL, "http://") || strings.HasPrefix(lowerURL, "https://") {
log.Warn("Both --url and local activation flags detected; proceeding with local activation via http://")
// Clear URL so we don't trigger HTTP profile fullflow during local runs (prevents recursion)
Comment on lines 80 to 86
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment describing local-vs-remote precedence for HTTP(S) URLs doesn’t match the implemented behavior: the code clears an HTTP(S) --url when local intent is detected (local flags win), but the comment says to keep the URL and ignore local flags. Please update the comment (or the logic) so the documented precedence matches what Validate() actually enforces.

Copilot uses AI. Check for mistakes.
cmd.URL = ""
}
Expand Down Expand Up @@ -201,6 +202,25 @@ func (cmd *ActivateCmd) hasLocalActivationFlags() bool {
cmd.ProvisioningCert != "" || cmd.ProvisioningCertPwd != "" || cmd.SkipIPRenew
}

func normalizeActivateURL(raw string) string {
value := strings.TrimSpace(raw)
if value == "" {
return ""
}

u, err := url.Parse(value)
if err != nil {
return value
}

scheme := strings.ToLower(u.Scheme)
if (scheme == "http" || scheme == "https" || scheme == "ws" || scheme == "wss") && u.Host == "" {
return ""
}

return value
}

// Run executes the activate command based on detected mode
func (cmd *ActivateCmd) Run(ctx *commands.Context) error {
log.Tracef("Entering Run method of ActivateCmd. Context: %s", ctx.AuthEndpoint)
Expand All @@ -210,10 +230,9 @@ func (cmd *ActivateCmd) Run(ctx *commands.Context) error {
if err := cmd.EnsureAMTPassword(ctx, cmd); err != nil {
return err
}

if err := cmd.EnsureWSMAN(ctx); err != nil {
return err
}
// Do not pre-create WSMAN for local activation here.
// LocalActivateCmd sets up its own local WSMAN transport, and doing both
// can trigger an extra LME/APF initialize cycle.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Move this content as part of the PR description

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

}
// Determine activation mode based on flags
if cmd.URL != "" {
Expand Down
12 changes: 12 additions & 0 deletions internal/commands/activate/activate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -416,6 +416,18 @@ func TestActivateCmd_Validate_PrecendenceMatrix(t *testing.T) {
wantErr: false,
wantClearedURL: true,
},
{
name: "Local CCM with placeholder HTTP URL clears URL",
cmd: ActivateCmd{Local: true, CCM: true, URL: "http://"},
wantErr: false,
wantClearedURL: true,
},
{
name: "Local CCM with spaced placeholder HTTPS URL clears URL",
cmd: ActivateCmd{Local: true, CCM: true, URL: " https:// "},
wantErr: false,
wantClearedURL: true,
},
{
name: "HTTP URL remote only (no local flags) retains URL",
cmd: ActivateCmd{URL: "https://server/p3"},
Expand Down
39 changes: 36 additions & 3 deletions internal/commands/activate/local.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,9 @@ import (
"errors"
"fmt"
"strings"
"time"

"github.com/device-management-toolkit/go-wsman-messages/v2/pkg/wsman/amt/general"
"github.com/device-management-toolkit/go-wsman-messages/v2/pkg/wsman/client"
"github.com/device-management-toolkit/rpc-go/v2/internal/certs"
"github.com/device-management-toolkit/rpc-go/v2/internal/commands"
Expand Down Expand Up @@ -479,7 +481,7 @@ func (service *LocalActivationService) activateCCM() error {

// Get general settings for digest realm

generalSettings, err := service.wsman.GetGeneralSettings()
generalSettings, err := service.getGeneralSettingsWithRetry()
if err != nil {
return utils.ActivationFailedGeneralSettings
}
Expand Down Expand Up @@ -749,7 +751,7 @@ func (service *LocalActivationService) activateACMWithTLS(tlsConfig *tls.Config)

// Get general settings to obtain digest realm for password hashing

generalSettings, err := service.wsman.GetGeneralSettings()
generalSettings, err := service.getGeneralSettingsWithRetry()
if err != nil {
return fmt.Errorf("failed to get AMT general settings: %w", err)
}
Expand Down Expand Up @@ -969,7 +971,7 @@ func (service *LocalActivationService) activateACMLegacy(tlsConfig *tls.Config)

// Get general settings for digest realm

generalSettings, err := service.wsman.GetGeneralSettings()
generalSettings, err := service.getGeneralSettingsWithRetry()
if err != nil {
return utils.ActivationFailedGeneralSettings
}
Expand Down Expand Up @@ -1044,6 +1046,37 @@ func (service *LocalActivationService) handleSetupErrorWithControlModeVerificati
return utils.ActivationFailedControlMode
}

// TODO: Move retry logic in wsman pkg
func (service *LocalActivationService) getGeneralSettingsWithRetry() (general.Response, error) {
Copy link
Copy Markdown
Contributor

@sudhir-intc sudhir-intc Mar 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can this retry logic be implemented in the GoWSMAN package. Check this issue: device-management-toolkit/go-wsman-messages#656

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, for now we can keep it, todo added

const maxRetries = 3

var lastErr error

for attempt := 0; attempt <= maxRetries; attempt++ {
response, err := service.wsman.GetGeneralSettings()
if err == nil {
return response, nil
}

lastErr = err
errText := strings.ToLower(err.Error())

transientBusy := strings.Contains(errText, "device or resource busy") ||
strings.Contains(errText, "resource busy") ||
strings.Contains(errText, "no such device") ||
strings.Contains(errText, "device unavailable")
if !transientBusy || attempt == maxRetries {
break
}

delay := time.Duration(attempt+1) * time.Duration(utils.HeciConnectRetryBackoff) * time.Millisecond
log.WithError(err).Warnf("GetGeneralSettings busy, retrying (%d/%d)", attempt+1, maxRetries)
time.Sleep(delay)
}

return general.Response{}, lastErr
}

// Certificate handling methods for ACM activation

// convertPfxToObject converts a base64 PFX certificate to a CertsAndKeys object
Expand Down
24 changes: 22 additions & 2 deletions internal/lm/engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ import (
log "github.com/sirupsen/logrus"
)

const lmeAPFChannelDataFlushOverride = 500 * time.Millisecond

// LMConnection is struct for managing connection to LMS
type LMEConnection struct {
Command pthi.Command
Expand Down Expand Up @@ -179,7 +181,7 @@ func (lme *LMEConnection) execute(bin_buf bytes.Buffer) error {
return err
}

bin_buf = apf.Process(result, lme.Session)
bin_buf = lme.processWithLocalTimerOverride(result)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change could you please explain as part of the PR description what was the behavior before and after

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TODO added

if bin_buf.Len() == 0 {
log.Debug("done EXECUTING.........")

Expand Down Expand Up @@ -244,7 +246,7 @@ func (lme *LMEConnection) Listen() {

break
} else {
result := apf.Process(result2, lme.Session)
result := lme.processWithLocalTimerOverride(result2)
if result.Len() != 0 {
err2 = lme.execute(result)
if err2 != nil {
Expand All @@ -257,6 +259,24 @@ func (lme *LMEConnection) Listen() {
}
}

// TODO: Optimize/test changes if wsman pkg can handle it
func (lme *LMEConnection) processWithLocalTimerOverride(message []byte) bytes.Buffer {
processed := apf.Process(message, lme.Session)

if len(message) > 0 && message[0] == apf.APF_CHANNEL_DATA && lme.Session.Timer != nil {
if !lme.Session.Timer.Stop() {
select {
case <-lme.Session.Timer.C:
default:
}
}

lme.Session.Timer.Reset(lmeAPFChannelDataFlushOverride)
}

return processed
}

// Close closes the LME connection

func (lme *LMEConnection) Close() error {
Expand Down
89 changes: 82 additions & 7 deletions internal/local/amt/localTransport.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,11 @@ import (
"net/http"
"strings"
"sync"
"time"

"github.com/device-management-toolkit/rpc-go/v2/internal/lm"
"github.com/device-management-toolkit/rpc-go/v2/pkg/heci"
"github.com/device-management-toolkit/rpc-go/v2/pkg/utils"
"github.com/sirupsen/logrus"
)

Expand All @@ -28,6 +30,8 @@ type LocalTransport struct {
waitGroup *sync.WaitGroup
}

const maxChannelOpenBusyRetries = 2

func NewLocalTransport() *LocalTransport {
lmDataChannel := make(chan []byte)
lmErrorChannel := make(chan error)
Expand Down Expand Up @@ -66,18 +70,62 @@ func (l *LocalTransport) Close() error {

// Custom dialer function
func (l *LocalTransport) RoundTrip(r *http.Request) (*http.Response, error) {
// send channel open
err := l.local.Connect()
var err error
for attempt := 0; attempt <= maxChannelOpenBusyRetries; attempt++ {
err = l.local.Connect()
if err == nil {
break
}

if !isMEIDeviceBusyError(err) || attempt == maxChannelOpenBusyRetries {
logrus.Error(err)

return nil, err
}

wait := time.Duration(attempt+1) * time.Duration(utils.HeciConnectRetryBackoff) * time.Millisecond
logrus.Warnf("mei busy during channel open, retry %d/%d", attempt+1, maxChannelOpenBusyRetries)
time.Sleep(wait)
}

go l.local.Listen()

if err != nil {
logrus.Error(err)
channelOpenTimeout := time.Duration(utils.LMETimerTimeout) * time.Second
if channelOpenTimeout <= 0 || channelOpenTimeout > utils.AMTResponseTimeout*time.Second {
channelOpenTimeout = utils.AMTResponseTimeout * time.Second
}

return nil, err
channelOpenTimer := time.NewTimer(channelOpenTimeout)

defer func() {
if !channelOpenTimer.Stop() {
select {
case <-channelOpenTimer.C:
default:
}
}
}()

channelOpenDone := make(chan struct{})

go func() {
defer close(channelOpenDone)

l.waitGroup.Wait()
}()

select {
case <-channelOpenDone:
case <-channelOpenTimer.C:
// Close the LME connection so the goroutine waiting on WaitGroup can
// unblock and the next request can start with a clean state.
if closeErr := l.Close(); closeErr != nil {
logrus.Errorf("failed to close LME connection after channel open timeout: %v", closeErr)
}

return nil, fmt.Errorf("timeout waiting for LME channel open confirmation after %s", channelOpenTimeout)
}

// wait for channel open confirmation
l.waitGroup.Wait()
logrus.Trace("Channel open confirmation received")
// Serialize the HTTP request to raw form
rawRequest, err := serializeHTTPRequest(r)
Expand All @@ -99,6 +147,19 @@ func (l *LocalTransport) RoundTrip(r *http.Request) (*http.Response, error) {
return nil, err
}

responseTimeout := utils.AMTResponseTimeout * time.Second

responseTimer := time.NewTimer(responseTimeout)

defer func() {
if !responseTimer.Stop() {
select {
case <-responseTimer.C:
default:
}
}
}()

Comment thread
nbmaiti marked this conversation as resolved.
Loop:
for {
select {
Expand All @@ -117,6 +178,10 @@ Loop:
respErr = errFromLMS
}

break Loop
case <-responseTimer.C:
respErr = fmt.Errorf("timeout waiting for LME response after %s", responseTimeout)

break Loop
}
}
Expand Down Expand Up @@ -171,3 +236,13 @@ func serializeHTTPRequest(r *http.Request) ([]byte, error) {

return reqBuffer.Bytes(), nil
}

func isMEIDeviceBusyError(err error) bool {
if err == nil {
return false
}

errMsg := strings.ToLower(err.Error())

return strings.Contains(errMsg, "device or resource busy") || strings.Contains(errMsg, "resource busy")
}
Loading
Loading