mirror of
https://github.com/MengMengCode/VoCat.git
synced 2026-08-13 03:13:43 +08:00
Make EC20 eSIM channels coexist with VoWiFi
This commit is contained in:
+23
-9
@@ -246,6 +246,15 @@ func (manager *Manager) openEuiccAID(ctx context.Context, id, aidHex string) (*e
|
||||
return channel, nil
|
||||
}
|
||||
lastErr = err
|
||||
if attempt == 0 && errors.Is(err, errNoLogicalChannel) &&
|
||||
manager.releaseStaleEuiccChannel(ctx, id) {
|
||||
// EC20 firmware exposes only one MANAGE CHANNEL slot. A canceled or
|
||||
// interrupted APDU transaction can leave channel 1 allocated, after
|
||||
// which every eSIM page load returns 6A81 until reboot. Closing the
|
||||
// orphan while holding the shared UICC transaction lock makes the
|
||||
// operation self-healing without disturbing an active AKA exchange.
|
||||
continue
|
||||
}
|
||||
if !isTransientEuiccCME(err) {
|
||||
return nil, err
|
||||
}
|
||||
@@ -262,6 +271,11 @@ func (manager *Manager) openEuiccAID(ctx context.Context, id, aidHex string) (*e
|
||||
return nil, lastErr
|
||||
}
|
||||
|
||||
func (manager *Manager) releaseStaleEuiccChannel(ctx context.Context, id string) bool {
|
||||
_, sw, err := manager.csim(ctx, id, []byte{0x00, 0x70, 0x80, 0x01, 0x00})
|
||||
return err == nil && sw == 0x9000
|
||||
}
|
||||
|
||||
func (manager *Manager) openEuiccOnce(ctx context.Context, id string) (*euiccChannel, error) {
|
||||
return manager.openEuiccOnceAID(ctx, id, isdRAID)
|
||||
}
|
||||
@@ -601,8 +615,8 @@ func validProfileICCID(iccid string) bool {
|
||||
|
||||
// ESIMListProfiles reads the eUICC profile list via ES10c GetProfilesInfo.
|
||||
func (manager *Manager) ESIMListProfiles(ctx context.Context, id string) (EsimInfo, error) {
|
||||
manager.esimMu.Lock()
|
||||
defer manager.esimMu.Unlock()
|
||||
manager.lockESIM()
|
||||
defer manager.unlockESIM()
|
||||
if manager.esimRecoveryActive(id) {
|
||||
if cached, ok := manager.cachedESIMInfo(id); ok {
|
||||
return cached, nil
|
||||
@@ -642,14 +656,14 @@ func (manager *Manager) ESIMSwitchProfile(ctx context.Context, id string, iccid
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
manager.esimMu.Lock()
|
||||
manager.lockESIM()
|
||||
if err := manager.waitForESIMRecovery(ctx, id); err != nil {
|
||||
manager.esimMu.Unlock()
|
||||
manager.unlockESIM()
|
||||
return err
|
||||
}
|
||||
channel, err := manager.openEuiccAID(ctx, id, targetEuiccAID(aidHex))
|
||||
if err != nil {
|
||||
manager.esimMu.Unlock()
|
||||
manager.unlockESIM()
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -676,7 +690,7 @@ func (manager *Manager) ESIMSwitchProfile(ctx context.Context, id string, iccid
|
||||
// detached reset is safe in either case and prevents an uncertain switch
|
||||
// from leaving the modem's SIM cache unusable.
|
||||
manager.startProfileSwitchRecovery(id)
|
||||
manager.esimMu.Unlock()
|
||||
manager.unlockESIM()
|
||||
return err
|
||||
}
|
||||
// A transport SW 9000 only means the APDU reached the eUICC. The real outcome
|
||||
@@ -685,11 +699,11 @@ func (manager *Manager) ESIMSwitchProfile(ctx context.Context, id string, iccid
|
||||
result, ok := enableProfileResult(payload)
|
||||
if !ok {
|
||||
manager.startProfileSwitchRecovery(id)
|
||||
manager.esimMu.Unlock()
|
||||
manager.unlockESIM()
|
||||
return fmt.Errorf("esim: unexpected EnableProfile response %s", strings.ToUpper(hex.EncodeToString(payload)))
|
||||
}
|
||||
if err := enableProfileResponseError(byte(result), payload); err != nil {
|
||||
manager.esimMu.Unlock()
|
||||
manager.unlockESIM()
|
||||
return err
|
||||
}
|
||||
manager.markCachedProfileEnabled(id, iccid)
|
||||
@@ -697,7 +711,7 @@ func (manager *Manager) ESIMSwitchProfile(ctx context.Context, id string, iccid
|
||||
// a detached recovery so it survives an HTTP disconnect, but keep this API
|
||||
// call pending until the live modem ICCID proves that the switch took effect.
|
||||
manager.startProfileSwitchRecovery(id)
|
||||
manager.esimMu.Unlock()
|
||||
manager.unlockESIM()
|
||||
|
||||
verifyContext, cancelVerify := context.WithTimeout(context.WithoutCancel(ctx), profileSwitchVerificationTimeout(manager))
|
||||
defer cancelVerify()
|
||||
|
||||
@@ -69,8 +69,8 @@ func (manager *Manager) ESIMDeleteProfile(ctx context.Context, id, iccid, aidHex
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
manager.esimMu.Lock()
|
||||
defer manager.esimMu.Unlock()
|
||||
manager.lockESIM()
|
||||
defer manager.unlockESIM()
|
||||
if err := manager.waitForESIMRecovery(ctx, id); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -63,8 +63,8 @@ func (manager *Manager) ESIMDisableProfile(ctx context.Context, id, iccid, aidHe
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
manager.esimMu.Lock()
|
||||
defer manager.esimMu.Unlock()
|
||||
manager.lockESIM()
|
||||
defer manager.unlockESIM()
|
||||
if err := manager.waitForESIMRecovery(ctx, id); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -48,8 +48,8 @@ func (manager *Manager) ESIMDownloadProfile(ctx context.Context, id string, para
|
||||
}
|
||||
}
|
||||
|
||||
manager.esimMu.Lock()
|
||||
defer manager.esimMu.Unlock()
|
||||
manager.lockESIM()
|
||||
defer manager.unlockESIM()
|
||||
|
||||
report("preflight", "正在检查 eUICC 剩余空间...", 10)
|
||||
channel, err := manager.openEuiccAID(ctx, id, targetEuiccAID(params.AIDHex))
|
||||
@@ -228,8 +228,8 @@ type EsimChipInfo struct {
|
||||
// ESIMChipInfo reads the eUICC's EID, EUICCInfo2, and configured addresses for
|
||||
// the chip header. It takes the eSIM lock like the other card ops.
|
||||
func (manager *Manager) ESIMChipInfo(ctx context.Context, id string) (*EsimChipInfo, error) {
|
||||
manager.esimMu.Lock()
|
||||
defer manager.esimMu.Unlock()
|
||||
manager.lockESIM()
|
||||
defer manager.unlockESIM()
|
||||
|
||||
var lastErr error
|
||||
for _, aid := range manager.discoverEuiccAIDs(ctx, id) {
|
||||
@@ -286,8 +286,8 @@ func readEsimChipInfo(ctx context.Context, channel *euiccChannel, aidHex string)
|
||||
// the inserted card. It is entirely read-only: only SELECT, GetProfilesInfo,
|
||||
// GetEuiccData, GetEuiccInfo2 and GetEuiccConfiguredAddresses are issued.
|
||||
func (manager *Manager) ESIMInventory(ctx context.Context, id string) ([]EsimInventoryEntry, error) {
|
||||
manager.esimMu.Lock()
|
||||
defer manager.esimMu.Unlock()
|
||||
manager.lockESIM()
|
||||
defer manager.unlockESIM()
|
||||
if manager.esimRecoveryActive(id) {
|
||||
return nil, errESIMRecovering
|
||||
}
|
||||
|
||||
@@ -295,8 +295,8 @@ func (channel *euiccChannel) deliverPendingNotifications(ctx context.Context) er
|
||||
// ESIMNotifications returns the notifications retained across every eUICC
|
||||
// storage exposed by the physical card.
|
||||
func (manager *Manager) ESIMNotifications(ctx context.Context, id string) ([]EsimNotification, error) {
|
||||
manager.esimMu.Lock()
|
||||
defer manager.esimMu.Unlock()
|
||||
manager.lockESIM()
|
||||
defer manager.unlockESIM()
|
||||
if err := manager.waitForESIMRecovery(ctx, id); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -331,8 +331,8 @@ func (manager *Manager) ESIMNotifications(ctx context.Context, id string) ([]Esi
|
||||
// ESIMRetryNotification sends one retained notification and removes it from the
|
||||
// eUICC only after the receiver returns the SGP.22 success acknowledgement.
|
||||
func (manager *Manager) ESIMRetryNotification(ctx context.Context, id, aidHex string, sequenceNumber uint64) error {
|
||||
manager.esimMu.Lock()
|
||||
defer manager.esimMu.Unlock()
|
||||
manager.lockESIM()
|
||||
defer manager.unlockESIM()
|
||||
if err := manager.waitForESIMRecovery(ctx, id); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -49,8 +49,8 @@ func (manager *Manager) ESIMRenameProfile(ctx context.Context, id, iccid, nickna
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
manager.esimMu.Lock()
|
||||
defer manager.esimMu.Unlock()
|
||||
manager.lockESIM()
|
||||
defer manager.unlockESIM()
|
||||
if err := manager.waitForESIMRecovery(ctx, id); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -294,6 +294,46 @@ func TestEUICCChannelStuckWrapsTransientCME(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestOpenEuiccRecoversOrphanedSingleLogicalChannel(t *testing.T) {
|
||||
client := &transcriptClient{steps: []clientStep{
|
||||
{
|
||||
command: `AT+CSIM=10,"0070000001"`,
|
||||
response: okResponse(`+CSIM: 6,"006A81"`),
|
||||
},
|
||||
{
|
||||
command: `AT+CSIM=10,"0070800100"`,
|
||||
response: okResponse(`+CSIM: 4,"9000"`),
|
||||
},
|
||||
{
|
||||
command: `AT+CSIM=10,"0070000001"`,
|
||||
response: okResponse(`+CSIM: 6,"019000"`),
|
||||
},
|
||||
{
|
||||
command: fmt.Sprintf(
|
||||
`AT+CSIM=42,"01A4040010%s"`,
|
||||
isdRAID,
|
||||
),
|
||||
response: okResponse(`+CSIM: 4,"9000"`),
|
||||
},
|
||||
{
|
||||
command: `AT+CSIM=10,"0070800100"`,
|
||||
response: okResponse(`+CSIM: 4,"9000"`),
|
||||
},
|
||||
}}
|
||||
manager, id := newStartedTestManager(t, client)
|
||||
|
||||
manager.lockESIM()
|
||||
channel, err := manager.openEuiccAID(context.Background(), id, isdRAID)
|
||||
if err == nil {
|
||||
channel.close(context.Background())
|
||||
}
|
||||
manager.unlockESIM()
|
||||
if err != nil {
|
||||
t.Fatalf("open eUICC after orphaned channel: %v", err)
|
||||
}
|
||||
client.assertDone(t)
|
||||
}
|
||||
|
||||
func TestWaitForESIMRecovery(t *testing.T) {
|
||||
done := make(chan struct{})
|
||||
manager := &Manager{esimRecoveries: map[string]chan struct{}{"dev": done}}
|
||||
|
||||
@@ -25,6 +25,7 @@ type Options struct {
|
||||
|
||||
type Manager struct {
|
||||
mu sync.RWMutex
|
||||
uiccMu sync.Mutex // serializes all multi-command UICC/APDU transactions
|
||||
esimMu sync.Mutex // serializes eSIM card access (list/switch/download)
|
||||
esimRecoveryMu sync.Mutex
|
||||
esimRecoveries map[string]chan struct{}
|
||||
@@ -42,6 +43,23 @@ type Manager struct {
|
||||
ussdSessions map[string]ussdSession
|
||||
}
|
||||
|
||||
// LockUICC and UnlockUICC allow another in-process UICC client (currently the
|
||||
// VoWiFi AKA adapter) to share the same transaction boundary as eSIM ES10.
|
||||
// Individual AT commands are already serialized per modem, but a logical-
|
||||
// channel transaction spans several commands and must not be interleaved.
|
||||
func (manager *Manager) LockUICC() { manager.uiccMu.Lock() }
|
||||
func (manager *Manager) UnlockUICC() { manager.uiccMu.Unlock() }
|
||||
|
||||
func (manager *Manager) lockESIM() {
|
||||
manager.esimMu.Lock()
|
||||
manager.uiccMu.Lock()
|
||||
}
|
||||
|
||||
func (manager *Manager) unlockESIM() {
|
||||
manager.uiccMu.Unlock()
|
||||
manager.esimMu.Unlock()
|
||||
}
|
||||
|
||||
// ussdSession tracks an open USSD dialog on a device so a follow-up Continue or
|
||||
// Cancel can be routed back to the right modem. The modem owns the actual
|
||||
// network session; this map only records which device a session id belongs to.
|
||||
|
||||
@@ -47,6 +47,11 @@ type EC20SensitiveATExecutor interface {
|
||||
ExecuteSensitiveAT(context.Context, string, string) (modem.Response, error)
|
||||
}
|
||||
|
||||
type EC20UICCLocker interface {
|
||||
LockUICC()
|
||||
UnlockUICC()
|
||||
}
|
||||
|
||||
type EC20AdapterOptions struct {
|
||||
// PureAirplanePolicy reports the independent user policy. The adapter only
|
||||
// changes the transactional CFUN projection used by VoWiFi and never
|
||||
@@ -339,6 +344,10 @@ func (adapter *EC20Adapter) CheckReady(
|
||||
// cannot insert an APDU between a 61xx response and GET RESPONSE.
|
||||
adapter.apduMu.Lock()
|
||||
defer adapter.apduMu.Unlock()
|
||||
if locker, ok := adapter.executor.(EC20UICCLocker); ok {
|
||||
locker.LockUICC()
|
||||
defer locker.UnlockUICC()
|
||||
}
|
||||
|
||||
aid, application, err := adapter.discoverAKAApplication(ctx, binding.deviceID)
|
||||
if err != nil {
|
||||
@@ -402,6 +411,10 @@ func (adapter *EC20Adapter) Authenticate(
|
||||
|
||||
adapter.apduMu.Lock()
|
||||
defer adapter.apduMu.Unlock()
|
||||
if locker, ok := adapter.executor.(EC20UICCLocker); ok {
|
||||
locker.LockUICC()
|
||||
defer locker.UnlockUICC()
|
||||
}
|
||||
|
||||
apdu := buildUSIMAuthenticateAPDU(challenge)
|
||||
var raw []byte
|
||||
|
||||
@@ -24,6 +24,23 @@ type ATMapper struct {
|
||||
Devices ATDeviceController
|
||||
}
|
||||
|
||||
type uiccLocker interface {
|
||||
LockUICC()
|
||||
UnlockUICC()
|
||||
}
|
||||
|
||||
func (mapper ATMapper) LockUICC() {
|
||||
if locker, ok := mapper.Devices.(uiccLocker); ok {
|
||||
locker.LockUICC()
|
||||
}
|
||||
}
|
||||
|
||||
func (mapper ATMapper) UnlockUICC() {
|
||||
if locker, ok := mapper.Devices.(uiccLocker); ok {
|
||||
locker.UnlockUICC()
|
||||
}
|
||||
}
|
||||
|
||||
func (mapper ATMapper) Get(configuredID string) (device.Device, error) {
|
||||
physicalID, err := mapper.resolve(context.Background(), configuredID)
|
||||
if err != nil {
|
||||
|
||||
Reference in New Issue
Block a user