Skip to content
Open
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
114 changes: 114 additions & 0 deletions gomobile/gomobile.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
// Package libwallet exports Decred wallet functionalities for mobile platforms.
// This package is designed to be compiled with gomobile for iOS and Android.
//
// Build examples:
//
// gomobile bind -target=android -androidapi=23 -o ./build/libwallet.aar ./gomobile
// gomobile bind -target=ios -o ./build/Libwallet.xcframework ./gomobile
package libwallet

import (
"context"
"errors"
"fmt"
"sync"

"github.com/decred/libwallet/assetlog"
"github.com/decred/libwallet/dcr"
"github.com/decred/slog"
)

// -----------------------------------------------------------------------------
// Global variables (shared across files in this package) x
// -----------------------------------------------------------------------------

var (
mainCtx context.Context
cancelMainCtx context.CancelFunc
wg sync.WaitGroup

logBackend *parentLogger

Check failure on line 30 in gomobile/gomobile.go

View workflow job for this annotation

GitHub Actions / Go CI (1.23)

undefined: parentLogger

Check failure on line 30 in gomobile/gomobile.go

View workflow job for this annotation

GitHub Actions / Go CI (1.22)

undefined: parentLogger

Check failure on line 30 in gomobile/gomobile.go

View workflow job for this annotation

GitHub Actions / Go CI (1.22)

undefined: parentLogger

Check failure on line 30 in gomobile/gomobile.go

View workflow job for this annotation

GitHub Actions / Go CI (1.23)

undefined: parentLogger
logMtx sync.RWMutex
log slog.Logger

// walletsMtx protects wallets and initialized.
walletsMtx sync.RWMutex
wallets = make(map[string]*wallet)

Check failure on line 36 in gomobile/gomobile.go

View workflow job for this annotation

GitHub Actions / Go CI (1.23)

undefined: wallet

Check failure on line 36 in gomobile/gomobile.go

View workflow job for this annotation

GitHub Actions / Go CI (1.22)

undefined: wallet

Check failure on line 36 in gomobile/gomobile.go

View workflow job for this annotation

GitHub Actions / Go CI (1.22)

undefined: wallet

Check failure on line 36 in gomobile/gomobile.go

View workflow job for this annotation

GitHub Actions / Go CI (1.23)

undefined: wallet
initialized bool
)

// -----------------------------------------------------------------------------
// Core Functions
// -----------------------------------------------------------------------------

// Initialize initializes the libwallet mobile library.
func Initialize(logDir, logLvl string) (string, error) {
walletsMtx.Lock()
defer walletsMtx.Unlock()
if initialized {
return "", errors.New("duplicate initialization")
}

lvl, ok := slog.LevelFromString(logLvl)
if !ok {
return "", fmt.Errorf("unknown log level %q", logLvl)
}

if logDir != "" {
logSpinner, err := assetlog.NewRotator(logDir, "dcrwallet.log")
if err != nil {
return "", fmt.Errorf("error initializing log rotator: %v", err)
}

logBackend = newParentLogger(logSpinner, lvl)

Check failure on line 63 in gomobile/gomobile.go

View workflow job for this annotation

GitHub Actions / Go CI (1.23)

undefined: newParentLogger

Check failure on line 63 in gomobile/gomobile.go

View workflow job for this annotation

GitHub Actions / Go CI (1.22)

undefined: newParentLogger

Check failure on line 63 in gomobile/gomobile.go

View workflow job for this annotation

GitHub Actions / Go CI (1.22)

undefined: newParentLogger

Check failure on line 63 in gomobile/gomobile.go

View workflow job for this annotation

GitHub Actions / Go CI (1.23)

undefined: newParentLogger
err = dcr.InitGlobalLogging(logDir, logBackend, lvl)
if err != nil {
return "", fmt.Errorf("error initializing logger for external pkgs: %v", err)
}
} else {
logBackend = newParentStdOutLogger(lvl)

Check failure on line 69 in gomobile/gomobile.go

View workflow job for this annotation

GitHub Actions / Go CI (1.23)

undefined: newParentStdOutLogger

Check failure on line 69 in gomobile/gomobile.go

View workflow job for this annotation

GitHub Actions / Go CI (1.22)

undefined: newParentStdOutLogger

Check failure on line 69 in gomobile/gomobile.go

View workflow job for this annotation

GitHub Actions / Go CI (1.22)

undefined: newParentStdOutLogger

Check failure on line 69 in gomobile/gomobile.go

View workflow job for this annotation

GitHub Actions / Go CI (1.23)

undefined: newParentStdOutLogger
}

logMtx.Lock()
log = logBackend.SubLogger("APP")
logMtx.Unlock()

mainCtx, cancelMainCtx = context.WithCancel(context.Background())
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[SUGGESTION] Initialization order could be improved.

Current: Context created after setting initialized = true flag (line 77).

Suggested: Move mainCtx, cancelMainCtx = context.WithCancel(context.Background()) to before setting initialized = true (around line 75).

Why: If initialization is checked between line 77 and 76, the context might not be ready. Minor race condition.


initialized = true
return "libwallet mobile initialized", nil
}

// Shutdown shuts down the libwallet mobile library.
func Shutdown() (string, error) {
walletsMtx.Lock()
defer walletsMtx.Unlock()
if !initialized {
return "", errors.New("not initialized")
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[WARNING] RLock held during potentially slow operation.

Current: logMtx.RLock() held during log.Debug() which may involve I/O.

Consider: If logging is fast and non-blocking in this implementation, this may be acceptable. However, best practice is to minimize lock duration.

Suggested pattern (if needed):

logMtx.RLock()
logger := log
logMtx.RUnlock()
logger.Debug("libwallet mobile shutting down")

Why: Avoid holding locks during I/O operations to prevent blocking other goroutines.

logMtx.RLock()
log.Debug("libwallet mobile shutting down")
logMtx.RUnlock()

for _, w := range wallets {
if err := w.CloseWallet(); err != nil {
w.log.Errorf("close wallet error: %v", err)
}
}
wallets = make(map[string]*wallet)

Check failure on line 99 in gomobile/gomobile.go

View workflow job for this annotation

GitHub Actions / Go CI (1.23)

undefined: wallet

Check failure on line 99 in gomobile/gomobile.go

View workflow job for this annotation

GitHub Actions / Go CI (1.22)

undefined: wallet

Check failure on line 99 in gomobile/gomobile.go

View workflow job for this annotation

GitHub Actions / Go CI (1.22)

undefined: wallet

Check failure on line 99 in gomobile/gomobile.go

View workflow job for this annotation

GitHub Actions / Go CI (1.23)

undefined: wallet

// Stop all remaining background processes and wait for them to stop.
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[CRITICAL] Potential deadlock: Context cancellation with mutex held.

Current:

walletsMtx.Lock()
defer walletsMtx.Unlock()
// ...
cancelMainCtx()
wg.Wait()

Issue: If any goroutine in wg tries to acquire walletsMtx (e.g., in wallet operations), it will deadlock because wg.Wait() is called while holding the mutex.

Suggested:

walletsMtx.Lock()
// ... close wallets ...
walletsMtx.Unlock()

// Cancel and wait AFTER releasing the mutex
cancelMainCtx()
wg.Wait()

logMtx.Lock()
// ... close logger ...

Why: Background goroutines must be able to acquire locks during shutdown. Holding locks during wg.Wait() is a common deadlock pattern.

cancelMainCtx()
wg.Wait()

// Close the logger backend as the last step.
logMtx.Lock()
log.Debug("libwallet mobile shutdown")
_ = logBackend.Close()
logBackend = nil
logMtx.Unlock()

initialized = false
return "libwallet mobile shutdown", nil
}
Loading