mirror of
https://github.com/MengMengCode/VoCat.git
synced 2026-08-13 03:13:43 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2780dd96de | ||
|
|
2922d6a275 | ||
|
|
288e856fdb |
@@ -12,19 +12,32 @@ import (
|
||||
)
|
||||
|
||||
func lockServerInstance(databasePath string) (*os.File, error) {
|
||||
directory := filepath.Dir(databasePath)
|
||||
if err := os.MkdirAll(directory, 0o755); err != nil {
|
||||
return nil, fmt.Errorf("create data directory for instance lock: %w", err)
|
||||
// The modem, PC/SC reader, XFRM policies and listener are host resources,
|
||||
// not database resources. Lock per OS user so a diagnostic instance using a
|
||||
// different VOCAT_DATABASE_PATH cannot silently steal the same AT port from
|
||||
// the managed service. Prefer /run because systemd's PrivateTmp would
|
||||
// otherwise hide the managed service's lock from a manually started process.
|
||||
// The UID-specific directory still permits intentionally isolated users to
|
||||
// operate independently; development hosts without writable /run fall back
|
||||
// to TempDir.
|
||||
uid := os.Geteuid()
|
||||
directory := filepath.Join("/run", fmt.Sprintf("vocat-%d", uid))
|
||||
if uid == 0 {
|
||||
directory = "/run/vocat"
|
||||
}
|
||||
path := filepath.Join(directory, ".vocat.lock")
|
||||
file, err := os.OpenFile(path, os.O_CREATE|os.O_RDWR, 0o600)
|
||||
if err := os.MkdirAll(directory, 0o700); err != nil {
|
||||
directory = os.TempDir()
|
||||
}
|
||||
path := filepath.Join(directory, "vocat-server.lock")
|
||||
fd, err := unix.Open(path, unix.O_CREAT|unix.O_RDWR|unix.O_CLOEXEC|unix.O_NOFOLLOW, 0o600)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("open server instance lock: %w", err)
|
||||
}
|
||||
if err := unix.Flock(int(file.Fd()), unix.LOCK_EX|unix.LOCK_NB); err != nil {
|
||||
file := os.NewFile(uintptr(fd), path)
|
||||
if err := unix.Flock(fd, unix.LOCK_EX|unix.LOCK_NB); err != nil {
|
||||
_ = file.Close()
|
||||
if errors.Is(err, unix.EWOULDBLOCK) || errors.Is(err, unix.EAGAIN) {
|
||||
return nil, fmt.Errorf("another vocat server is already using database %s", databasePath)
|
||||
return nil, errors.New("another vocat server already controls this host's modem resources")
|
||||
}
|
||||
return nil, fmt.Errorf("lock server instance: %w", err)
|
||||
}
|
||||
|
||||
@@ -9,17 +9,18 @@ import (
|
||||
)
|
||||
|
||||
func TestServerInstanceLockRejectsSecondProcess(t *testing.T) {
|
||||
database := filepath.Join(t.TempDir(), "vocat.db")
|
||||
first, err := lockServerInstance(database)
|
||||
firstDatabase := filepath.Join(t.TempDir(), "vocat.db")
|
||||
first, err := lockServerInstance(firstDatabase)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer first.Close()
|
||||
second, err := lockServerInstance(database)
|
||||
secondDatabase := filepath.Join(t.TempDir(), "other.db")
|
||||
second, err := lockServerInstance(secondDatabase)
|
||||
if second != nil {
|
||||
second.Close()
|
||||
}
|
||||
if err == nil || !strings.Contains(err.Error(), "already using database") {
|
||||
if err == nil || !strings.Contains(err.Error(), "already controls this host") {
|
||||
t.Fatalf("second lock error = %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
+61
-5
@@ -622,12 +622,18 @@ func configureVoWiFiRuntime(
|
||||
}
|
||||
if deviceConfig.VoWiFiEnabled {
|
||||
if entry, mapErr := mapper.Get(deviceConfig.ID); mapErr == nil {
|
||||
flightContext, cancelFlight := context.WithTimeout(ctx, 10*time.Second)
|
||||
_, flightErr := deviceManager.SetFlight(flightContext, entry.ID, true)
|
||||
cancelFlight()
|
||||
flightErr := protectVoWiFiStartupRadio(ctx, deviceManager, entry.ID)
|
||||
if flightErr != nil {
|
||||
_ = manager.Close(context.Background())
|
||||
return nil, fmt.Errorf("protect device %q before VoWiFi startup: %w", deviceConfig.ID, flightErr)
|
||||
// A modem can be temporarily unavailable while OpenWrt/procd is
|
||||
// restarting the service (notably after loading XFRM modules). Do
|
||||
// not take the Web/API service down with it: the orchestrator below
|
||||
// remains fail-closed and its runtime manager retries until CFUN=4
|
||||
// can be established.
|
||||
logger.Warn(
|
||||
"VoWiFi startup radio protection deferred to automatic retry",
|
||||
"device_id", deviceConfig.ID,
|
||||
"error", flightErr,
|
||||
)
|
||||
}
|
||||
}
|
||||
if _, err := manager.RequestEnabled(deviceConfig.ID, true); err != nil {
|
||||
@@ -639,6 +645,56 @@ func configureVoWiFiRuntime(
|
||||
return manager, nil
|
||||
}
|
||||
|
||||
const (
|
||||
vowifiStartupRadioAttempts = 3
|
||||
vowifiStartupRadioDelay = time.Second
|
||||
)
|
||||
|
||||
type flightModeSetter interface {
|
||||
SetFlight(context.Context, string, bool) (device.FlightResult, error)
|
||||
}
|
||||
|
||||
func protectVoWiFiStartupRadio(ctx context.Context, manager flightModeSetter, physicalID string) error {
|
||||
return protectVoWiFiStartupRadioWithRetry(
|
||||
ctx,
|
||||
manager,
|
||||
physicalID,
|
||||
vowifiStartupRadioAttempts,
|
||||
vowifiStartupRadioDelay,
|
||||
)
|
||||
}
|
||||
|
||||
func protectVoWiFiStartupRadioWithRetry(
|
||||
ctx context.Context,
|
||||
manager flightModeSetter,
|
||||
physicalID string,
|
||||
attempts int,
|
||||
delay time.Duration,
|
||||
) error {
|
||||
var lastErr error
|
||||
for attempt := 0; attempt < attempts; attempt++ {
|
||||
flightContext, cancel := context.WithTimeout(ctx, 10*time.Second)
|
||||
_, lastErr = manager.SetFlight(flightContext, physicalID, true)
|
||||
cancel()
|
||||
if lastErr == nil {
|
||||
return nil
|
||||
}
|
||||
if attempt+1 == attempts {
|
||||
break
|
||||
}
|
||||
timer := time.NewTimer(delay)
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
if !timer.Stop() {
|
||||
<-timer.C
|
||||
}
|
||||
return errors.Join(lastErr, ctx.Err())
|
||||
case <-timer.C:
|
||||
}
|
||||
}
|
||||
return lastErr
|
||||
}
|
||||
|
||||
type vowifiDeviceAdapter interface {
|
||||
vowifi.SIMIdentityReader
|
||||
vowifi.AKAProvider
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"vocat/internal/device"
|
||||
)
|
||||
|
||||
type startupFlightSetter struct {
|
||||
errors []error
|
||||
calls int
|
||||
id string
|
||||
}
|
||||
|
||||
func (setter *startupFlightSetter) SetFlight(
|
||||
_ context.Context,
|
||||
id string,
|
||||
enabled bool,
|
||||
) (device.FlightResult, error) {
|
||||
setter.calls++
|
||||
setter.id = id
|
||||
if !enabled {
|
||||
return device.FlightResult{}, errors.New("expected flight mode to be enabled")
|
||||
}
|
||||
if setter.calls <= len(setter.errors) {
|
||||
return device.FlightResult{}, setter.errors[setter.calls-1]
|
||||
}
|
||||
return device.FlightResult{CurrentMode: 4, FlightMode: true, RadioOff: true}, nil
|
||||
}
|
||||
|
||||
func TestProtectVoWiFiStartupRadioRetriesTransientFailure(t *testing.T) {
|
||||
transient := errors.New("modem is reopening")
|
||||
setter := &startupFlightSetter{errors: []error{transient, transient}}
|
||||
if err := protectVoWiFiStartupRadioWithRetry(
|
||||
context.Background(), setter, "quectel-1", 3, 0,
|
||||
); err != nil {
|
||||
t.Fatalf("protect startup radio: %v", err)
|
||||
}
|
||||
if setter.calls != 3 || setter.id != "quectel-1" {
|
||||
t.Fatalf("SetFlight calls = %d, id = %q", setter.calls, setter.id)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProtectVoWiFiStartupRadioReturnsLastFailure(t *testing.T) {
|
||||
first := errors.New("first")
|
||||
last := errors.New("last")
|
||||
setter := &startupFlightSetter{errors: []error{first, last}}
|
||||
err := protectVoWiFiStartupRadioWithRetry(
|
||||
context.Background(), setter, "quectel-1", 2, 0,
|
||||
)
|
||||
if !errors.Is(err, last) || setter.calls != 2 {
|
||||
t.Fatalf("protect startup radio = %v after %d calls", err, setter.calls)
|
||||
}
|
||||
}
|
||||
+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 {
|
||||
|
||||
+32
-5
@@ -337,6 +337,8 @@ TimeoutStartSec=30s
|
||||
# HTTP, VoWiFi, and modem cleanup have bounded shutdown contexts totalling up
|
||||
# to 30 seconds. Leave a small margin before systemd resorts to SIGKILL.
|
||||
TimeoutStopSec=40s
|
||||
RuntimeDirectory=vocat
|
||||
RuntimeDirectoryMode=0755
|
||||
|
||||
AmbientCapabilities=CAP_NET_ADMIN CAP_NET_RAW
|
||||
CapabilityBoundingSet=CAP_NET_ADMIN CAP_NET_RAW
|
||||
@@ -408,12 +410,37 @@ write_service() {
|
||||
enable_and_start() {
|
||||
if [ -x "$OPENWRT_INIT_PATH" ] && { [ -x /sbin/procd ] || [ -x /sbin/ubusd ]; }; then
|
||||
"$OPENWRT_INIT_PATH" enable
|
||||
# Stop explicitly before restart. Some procd/rc.common variants return
|
||||
# from restart while the previous process is still inside its bounded
|
||||
# VoWiFi cleanup, so the replacement can race the host-wide instance
|
||||
# lock and enter a respawn cycle.
|
||||
"$OPENWRT_INIT_PATH" stop || true
|
||||
local stop_attempt
|
||||
for stop_attempt in 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40; do
|
||||
if ! "$OPENWRT_INIT_PATH" running; then
|
||||
break
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
if "$OPENWRT_INIT_PATH" restart; then
|
||||
sleep 2
|
||||
if "$OPENWRT_INIT_PATH" running; then
|
||||
rm -f "${BINARY_PATH}.bak"
|
||||
return
|
||||
fi
|
||||
# Modems may need several seconds to release and reopen their AT
|
||||
# port after procd stops the previous process. Require consecutive
|
||||
# healthy observations so a short-lived respawn is not mistaken for
|
||||
# a successful upgrade.
|
||||
local attempt stable
|
||||
stable=0
|
||||
for attempt in 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30; do
|
||||
sleep 1
|
||||
if "$OPENWRT_INIT_PATH" running; then
|
||||
stable=$((stable + 1))
|
||||
if [ "$stable" -ge 3 ]; then
|
||||
rm -f "${BINARY_PATH}.bak"
|
||||
return
|
||||
fi
|
||||
else
|
||||
stable=0
|
||||
fi
|
||||
done
|
||||
fi
|
||||
if [ -e "${BINARY_PATH}.bak" ]; then
|
||||
cp -a "${BINARY_PATH}.bak" "$BINARY_PATH"
|
||||
|
||||
Reference in New Issue
Block a user