Skip to content
Open
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
2 changes: 1 addition & 1 deletion internal/commands/connect_doctor.go
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ func feedCheck(ctx context.Context, account *basecamp.AccountClient) setup.Check
// gaps with their epoch ids, losses, the hold, and lifecycle messages that
// wait for a decision. It reads the ledger read-only.
func ledgerChecks(ctx context.Context, p connectProfile) []setup.Check {
dir, err := connectStatePath(p.file, false)
dir, err := connectStatePath(p.name, p.file, false)
if err != nil {
return []setup.Check{{Name: "Ledger", Status: setup.StatusFail, Message: errorMessage(err)}}
}
Expand Down
64 changes: 51 additions & 13 deletions internal/commands/connect_operator.go
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ func parseEventIDArg(raw string) (int64, error) {
// metadata beside it is written best-effort and says nothing on its own.
func openConnectLedger(ctx context.Context, p connectProfile, requireStopped bool) (*connector.Ledger, func(), error) {
done := func() {}
dir, err := connectStatePath(p.file, false)
dir, err := connectStatePath(p.name, p.file, false)
if err != nil {
return nil, done, err
}
Expand Down Expand Up @@ -132,8 +132,17 @@ func openConnectLedger(ctx context.Context, p connectProfile, requireStopped boo
return nil, done, lockErr
}
done = func() { _ = lock.Release() }
if _, err := os.Lstat(path); err != nil {
done()
return nil, func() {}, output.ErrUsageHint(fmt.Sprintf("Profile %q's connector state moved while this command started", p.name), "Run the command again.")
}
}
var ledger *connector.Ledger
if requireStopped || outOfDate {
ledger, err = connector.OpenLedger(path) //nolint:contextcheck // OpenLedger migrates on its own context
} else {
ledger, err = connector.OpenExistingLedger(ctx, path)
}
ledger, err := connector.OpenLedger(path) //nolint:contextcheck // OpenLedger migrates on its own context
if err != nil {
done()
return nil, func() {}, err
Expand Down Expand Up @@ -205,7 +214,7 @@ func runConnectStatus(cmd *cobra.Command, shadow bool) error {
if err != nil {
return err
}
dir, err := connectStatePath(p.file, shadow)
dir, err := connectStatePath(p.name, p.file, shadow)
if err != nil {
return err
}
Expand Down Expand Up @@ -513,25 +522,30 @@ to be discarded.`,
if err != nil {
return err
}
shadowDir, err := connectStatePath(p.file, true)
stateHome, err := connectStateHome()
if err != nil {
return err
}
stateHome, err := connectStateHome()
shadowDir, err := connectStatePath(p.name, p.file, true)
if err != nil {
return err
}
parent, err := ensurePrivateChain(stateHome, "basecamp", "connect")
if err != nil {
return output.ErrUsage("The connector's state directory cannot be used: " + err.Error())
}
res, err := connector.PromoteShadow(cmd.Context(), connector.PromoteOptions{
ShadowDir: shadowDir,
StateDir: filepath.Join(parent, connector.StateDirName(p.file.AccountID, p.file.Agent.PersonID)),
AccountID: p.file.AccountID,
AgentID: p.file.Agent.PersonID,
By: operatorName(),
})
var res connector.PromoteResult
release, err := lockLegacyStateWithoutLedger(filepath.Join(parent, connector.LegacyStateDirName(p.file.AccountID, p.file.Agent.PersonID)), p.file)
if err == nil {
defer release()
res, err = connector.PromoteShadow(cmd.Context(), connector.PromoteOptions{
ShadowDir: shadowDir,
StateDir: filepath.Join(parent, connector.StateDirName(p.file.AccountID, p.file.Agent.PersonID, p.name)),
AccountID: p.file.AccountID,
AgentID: p.file.Agent.PersonID,
By: operatorName(),
})
}
switch {
case errors.Is(err, connector.ErrAlreadyRunning):
return &output.Error{Code: output.CodeLockUnavailable, Message: err.Error(),
Expand All @@ -551,6 +565,30 @@ to be discarded.`,
return cmd
}

func lockLegacyStateWithoutLedger(legacy string, file setup.File) (func(), error) {
switch _, err := os.Lstat(legacy); {
case errors.Is(err, os.ErrNotExist):
return func() {}, nil
case err != nil:
return nil, err
}
lock, err := connector.AcquireInstanceLock(legacy, file.AccountID, file.Agent.PersonID, time.Now())
if err != nil {
return nil, err
}
ledger := filepath.Join(legacy, connector.LedgerFile)
switch _, err := os.Lstat(ledger); {
case err == nil:
err = fmt.Errorf("connector: %s: %w", ledger, connector.ErrLedgerExists)
_ = lock.Release()
return nil, err
case !errors.Is(err, os.ErrNotExist):
_ = lock.Release()
return nil, err
}
return func() { _ = lock.Release() }, nil
}

// --- import ---------------------------------------------------------------

// maxReconciliationBytes bounds a reconciliation file.
Expand Down Expand Up @@ -583,7 +621,7 @@ The file is JSON:
if err != nil {
return output.ErrUsage(strings.TrimPrefix(err.Error(), "connector: "))
}
dir, err := connectStatePath(p.file, false)
dir, err := connectStatePath(p.name, p.file, false)
if err != nil {
return err
}
Expand Down
16 changes: 8 additions & 8 deletions internal/commands/connect_operator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ func newOperatorFixture(t *testing.T) operatorFixture {
// record 1 and a blocked record 2, and returns it open.
func (f operatorFixture) ledger(t *testing.T, shadow bool) *connector.Ledger {
t.Helper()
dir, err := connectStateDir(f.file, shadow)
dir, err := connectStateDir("agent", f.file, shadow)
require.NoError(t, err)
l, err := connector.OpenLedger(filepath.Join(dir, connector.LedgerFile))
require.NoError(t, err)
Expand Down Expand Up @@ -155,7 +155,7 @@ func TestConnectDiscardAndRelease(t *testing.T) {
require.NoError(t, err, out)
assert.Contains(t, out, "Released")

dir, err := connectStatePath(f.file, false)
dir, err := connectStatePath("agent", f.file, false)
require.NoError(t, err)
l, err = connector.OpenLedgerReadOnly(context.Background(), filepath.Join(dir, connector.LedgerFile))
require.NoError(t, err)
Expand Down Expand Up @@ -189,7 +189,7 @@ func TestConnectShadowPromoteAndImport(t *testing.T) {
require.Error(t, err)
assert.Equal(t, output.CodeUsage, usageError(t, err).Code)

dir, err := connectStatePath(f.file, false)
dir, err := connectStatePath("agent", f.file, false)
require.NoError(t, err)
lock, err := connector.AcquireInstanceLock(dir, f.file.AccountID, f.file.Agent.PersonID, time.Now())
require.NoError(t, err)
Expand Down Expand Up @@ -301,7 +301,7 @@ func TestFakeMCPServer(t *testing.T) {
func TestOperatorCommandsDoNotMigrateUnderARunningConnector(t *testing.T) {
f := newOperatorFixture(t)
require.NoError(t, f.ledger(t, false).Close())
dir, err := connectStatePath(f.file, false)
dir, err := connectStatePath("agent", f.file, false)
require.NoError(t, err)

// A ledger an older binary wrote: its last migration is not recorded.
Expand All @@ -325,7 +325,7 @@ func TestOperatorCommandsDoNotMigrateUnderARunningConnector(t *testing.T) {
func TestTheMigrationGuardIsTheLockNotItsMetadata(t *testing.T) {
f := newOperatorFixture(t)
require.NoError(t, f.ledger(t, false).Close())
dir, err := connectStatePath(f.file, false)
dir, err := connectStatePath("agent", f.file, false)
require.NoError(t, err)
db, err := sql.Open("sqlite", filepath.Join(dir, connector.LedgerFile))
require.NoError(t, err)
Expand All @@ -348,7 +348,7 @@ func TestTheMigrationGuardIsTheLockNotItsMetadata(t *testing.T) {
func TestImportRunsOnALedgerOlderThanTheBuild(t *testing.T) {
f := newOperatorFixture(t)
require.NoError(t, f.ledger(t, false).Close())
dir, err := connectStatePath(f.file, false)
dir, err := connectStatePath("agent", f.file, false)
require.NoError(t, err)
db, err := sql.Open("sqlite", filepath.Join(dir, connector.LedgerFile))
require.NoError(t, err)
Expand All @@ -365,7 +365,7 @@ func TestImportRunsOnALedgerOlderThanTheBuild(t *testing.T) {
// itself over the lock it holds.
_, err = f.run(t, output.FormatJSON, "import", file)
if err != nil {
assert.NotContains(t, err.Error(), "already holds this account")
assert.NotContains(t, err.Error(), "already runs for this profile")
assert.NotContains(t, err.Error(), "Stop the connector")
return
}
Expand Down Expand Up @@ -396,7 +396,7 @@ func TestDoctorRefusesAShadowingToken(t *testing.T) {
require.Error(t, err, args[0])
assert.Contains(t, err.Error(), "BASECAMP_TOKEN", args[0])
}
dir, err := connectStatePath(f.file, false)
dir, err := connectStatePath("agent", f.file, false)
require.NoError(t, err)
db, err := sql.Open("sqlite", filepath.Join(dir, connector.LedgerFile))
require.NoError(t, err)
Expand Down
Loading
Loading