Skip to content

Commit

Permalink
Fix(2496): Random fixes for leave community on mobile (#2498)
Browse files Browse the repository at this point in the history
* Extra logging, blocking actions and random UX error fix

* Some more changes

* Add more logs

* Connect to tor when we should and update peers on connect

* Redial when tor is fully ready

* More logs, spawn hidden services early again, better hang up and mild refactor of tor

* PR fixes

* More fixes

* Small fix

* Fix tests

* Fix setup environment on mac in ci

* Fix mac e2e filename

* Update e2e-mac.yml

* Update e2e-mac.yml

* Comment fix and better redial on resume

* Add back fix I accidentally removed

* Don't redial on tor initialization

* Update .detoxrc.js

* Revert "Update .detoxrc.js"

This reverts commit 9290f25.
  • Loading branch information
Isla Koenigsknecht authored and Lucas Leblow committed May 10, 2024
1 parent 40d067b commit 9f60599
Show file tree
Hide file tree
Showing 17 changed files with 207 additions and 83 deletions.
15 changes: 15 additions & 0 deletions .github/actions/setup-env/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,21 @@ runs:
- uses: actions/setup-node@master
with:
node-version: 18.12.1

- name: Set up Python 3.12
uses: actions/setup-python@v5
if: runner.os == 'macOS'
with:
python-version: 3.12

- name: Print python version
run: which python3
if: runner.os == 'macOS'
shell: bash

- name: Install setuptools
run: python3 -m pip install setuptools
shell: bash

- name: Print OS name
run: echo ${{ runner.os }}
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/e2e-mac.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ jobs:

- name: FILE_NAME env
working-directory: ./packages/desktop/dist
run: echo "FILE_NAME="Quiet-$VERSION.dmg"" >> $GITHUB_ENV
run: echo "FILE_NAME="Quiet-$VERSION-arm64.dmg"" >> $GITHUB_ENV

- name: List dist dir content
working-directory: ./packages/desktop/dist
Expand All @@ -50,7 +50,7 @@ jobs:
run: hdiutil mount $FILE_NAME

- name: Add App file to applications
run: cd ~ && cp -R "/Volumes/Quiet $VERSION/Quiet.app" /Applications
run: cd ~ && cp -R "/Volumes/Quiet $VERSION-arm64/Quiet.app" /Applications

- name: Run invitation link test - Includes 2 separate application clients
uses: nick-fields/retry@14672906e672a08bd6eeb15720e9ed3ce869cdd4 # v2.9.0
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { getLibp2pAddressesFromCsrs, removeFilesFromDir } from '../common/utils'

import { LazyModuleLoader } from '@nestjs/core'
import { createLibp2pAddress, isPSKcodeValid } from '@quiet/common'
import { CertFieldsTypes, getCertFieldValue, loadCertificate } from '@quiet/identity'
import { CertFieldsTypes, createRootCA, getCertFieldValue, loadCertificate } from '@quiet/identity'
import {
ChannelMessageIdsResponse,
ChannelSubscribedPayload,
Expand Down Expand Up @@ -63,6 +63,7 @@ import { ServerStoredCommunityMetadata } from '../storageServiceClient/storageSe
import { Tor } from '../tor/tor.service'
import { ConfigOptions, GetPorts, ServerIoProviderTypes } from '../types'
import { ServiceState, TorInitState } from './connections-manager.types'
import { DateTime } from 'luxon'

@Injectable()
export class ConnectionsManagerService extends EventEmitter implements OnModuleInit {
Expand Down Expand Up @@ -223,7 +224,10 @@ export class ConnectionsManagerService extends EventEmitter implements OnModuleI

public async closeAllServices(options: { saveTor: boolean } = { saveTor: false }) {
if (this.tor && !options.saveTor) {
this.logger('Killing tor')
await this.tor.kill()
} else if (options.saveTor) {
this.logger('Saving tor')
}
if (this.storageService) {
this.logger('Stopping orbitdb')
Expand Down Expand Up @@ -260,24 +264,55 @@ export class ConnectionsManagerService extends EventEmitter implements OnModuleI
this.logger('Resuming!')
this.logger('Reopening socket!')
await this.openSocket()
this.logger('Dialing peers with info: ', this.peerInfo)
await this.libp2pService?.redialPeers(this.peerInfo)
this.logger('Attempting to redial peers!')
if (this.peerInfo && (this.peerInfo?.connected.length !== 0 || this.peerInfo?.dialed.length !== 0)) {
this.logger('Dialing peers with info from pause: ', this.peerInfo)
await this.libp2pService?.redialPeers([...this.peerInfo.connected, ...this.peerInfo.dialed])
} else {
this.logger('Dialing peers from stored community (if exists)')
const community = await this.localDbService.getCurrentCommunity()
if (!community) {
this.logger(`No community launched, can't redial`)
return
}
const sortedPeers = await this.localDbService.getSortedPeers(community.peerList ?? [])
await this.libp2pService?.redialPeers(sortedPeers)
}
}

// This method is only used on iOS through rn-bridge for reacting on lifecycle changes
public async openSocket() {
await this.socketService.init()
}

public async leaveCommunity() {
public async leaveCommunity(): Promise<boolean> {
this.logger('Running leaveCommunity')

this.logger('Resetting tor')
this.tor.resetHiddenServices()

this.logger('Closing the socket')
this.closeSocket()

this.logger('Purging local DB')
await this.localDbService.purge()

this.logger('Closing services')
await this.closeAllServices({ saveTor: true })

this.logger('Purging data')
await this.purgeData()

this.logger('Resetting state')
await this.resetState()

this.logger('Reopening local DB')
await this.localDbService.open()
await this.socketService.init()

this.logger('Restarting socket')
await this.openSocket()

return true
}

async resetState() {
Expand All @@ -295,16 +330,24 @@ export class ConnectionsManagerService extends EventEmitter implements OnModuleI
i.startsWith('Ipfs') || i.startsWith('OrbitDB') || i.startsWith('backendDB') || i.startsWith('Local Storage')
)
for (const dir of dirsToRemove) {
removeFilesFromDir(path.join(this.quietDir, dir))
const dirPath = path.join(this.quietDir, dir)
this.logger(`Removing dir: ${dirPath}`)
removeFilesFromDir(dirPath)
}
}

public async getNetwork(): Promise<NetworkInfo> {
this.logger('Getting network information')

this.logger('Creating hidden service')
const hiddenService = await this.tor.createNewHiddenService({ targetPort: this.ports.libp2pHiddenService })

this.logger('Destroying the hidden service we created')
await this.tor.destroyHiddenService(hiddenService.onionAddress.split('.')[0])

// TODO: Do we want to create the PeerId here? It doesn't necessarily have
// anything to do with Tor.
this.logger('Getting peer ID')
const peerId: PeerId = await PeerId.create()
const peerIdJson = peerId.toJSON()
this.logger(`Created network for peer ${peerId.toString()}. Address: ${hiddenService.onionAddress}`)
Expand Down Expand Up @@ -583,8 +626,19 @@ export class ConnectionsManagerService extends EventEmitter implements OnModuleI
await this.libp2pService.createInstance(params)

// Libp2p event listeners
this.libp2pService.on(Libp2pEvents.PEER_CONNECTED, (payload: { peers: string[] }) => {
this.libp2pService.on(Libp2pEvents.PEER_CONNECTED, async (payload: { peers: string[] }) => {
this.serverIoProvider.io.emit(SocketActionTypes.PEER_CONNECTED, payload)
for (const peer of payload.peers) {
const peerStats: NetworkStats = {
peerId: peer,
connectionTime: 0,
lastSeen: DateTime.utc().toSeconds(),
}

await this.localDbService.update(LocalDBKeys.PEERS, {
[peer]: peerStats,
})
}
})

this.libp2pService.on(Libp2pEvents.PEER_DISCONNECTED, async (payload: NetworkDataPayload) => {
Expand Down Expand Up @@ -633,6 +687,11 @@ export class ConnectionsManagerService extends EventEmitter implements OnModuleI
this.tor.on(SocketActionTypes.CONNECTION_PROCESS_INFO, data => {
this.serverIoProvider.io.emit(SocketActionTypes.CONNECTION_PROCESS_INFO, data)
})
this.tor.on(SocketActionTypes.REDIAL_PEERS, async data => {
this.logger(`Socket - ${SocketActionTypes.REDIAL_PEERS}`)
const peerInfo = this.libp2pService?.getCurrentPeerInfo()
await this.libp2pService?.redialPeers([...peerInfo.connected, ...peerInfo.dialed])
})
this.socketService.on(SocketActionTypes.CONNECTION_PROCESS_INFO, data => {
this.serverIoProvider.io.emit(SocketActionTypes.CONNECTION_PROCESS_INFO, data)
})
Expand All @@ -644,9 +703,9 @@ export class ConnectionsManagerService extends EventEmitter implements OnModuleI
// Update Frontend with Initialized Communities
if (this.communityId) {
this.serverIoProvider.io.emit(SocketActionTypes.COMMUNITY_LAUNCHED, { id: this.communityId })
console.log('this.libp2pService.dialedPeers', this.libp2pService.dialedPeers)
console.log('this.libp2pService.connectedPeers', this.libp2pService.connectedPeers)
console.log('this.libp2pservice', this.libp2pService)
this.logger('this.libp2pService.connectedPeers', this.libp2pService.connectedPeers)
this.logger('this.libp2pservice', this.libp2pService)
this.logger('this.libp2pService.dialedPeers', this.libp2pService.dialedPeers)
this.serverIoProvider.io.emit(
SocketActionTypes.CONNECTED_PEERS,
Array.from(this.libp2pService.connectedPeers.keys())
Expand Down Expand Up @@ -679,8 +738,10 @@ export class ConnectionsManagerService extends EventEmitter implements OnModuleI
callback(await this.joinCommunity(args))
}
)
this.socketService.on(SocketActionTypes.LEAVE_COMMUNITY, async () => {
await this.leaveCommunity()

this.socketService.on(SocketActionTypes.LEAVE_COMMUNITY, async (callback: (closed: boolean) => void) => {
this.logger(`socketService - ${SocketActionTypes.LEAVE_COMMUNITY}`)
callback(await this.leaveCommunity())
})

// Username registration
Expand Down
32 changes: 22 additions & 10 deletions packages/backend/src/nest/libp2p/libp2p.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { webSockets } from '../websocketOverTor'
import { all } from '../websocketOverTor/filters'
import { Libp2pConnectedPeer, Libp2pEvents, Libp2pNodeParams, Libp2pPeerInfo } from './libp2p.types'
import { ProcessInChunksService } from './process-in-chunks.service'
import { peerIdFromString } from '@libp2p/peer-id'

const KEY_LENGTH = 32
export const LIBP2P_PSK_METADATA = '/key/swarm/psk/1.0.0/\n/base16/\n'
Expand All @@ -42,6 +43,7 @@ export class Libp2pService extends EventEmitter {

private dialPeer = async (peerAddress: string) => {
if (this.dialedPeers.has(peerAddress)) {
this.logger(`Skipping dial of ${peerAddress} because its already been dialed`)
return
}
this.dialedPeers.add(peerAddress)
Expand Down Expand Up @@ -94,25 +96,37 @@ export class Libp2pService extends EventEmitter {
for (const peer of peers) {
await this.hangUpPeer(peer)
}
this.logger('All peers hung up')
}

public async hangUpPeer(peerAddress: string) {
this.logger('Hanging up on peer', peerAddress)
await this.libp2pInstance?.hangUp(multiaddr(peerAddress))
try {
const ma = multiaddr(peerAddress)
const peerId = peerIdFromString(ma.getPeerId()!)

this.logger('Hanging up connection on libp2p')
await this.libp2pInstance?.hangUp(ma)

this.logger('Removing peer from peer store')
await this.libp2pInstance?.peerStore.delete(peerId as any)
} catch (e) {
this.logger.error(e)
}
this.logger('Clearing local data')
this.dialedPeers.delete(peerAddress)
this.connectedPeers.delete(peerAddress)
this.logger('Done hanging up')
}

/**
* Hang up existing peer connections and re-dial them. Specifically useful on
* iOS where Tor receives a new port when the app resumes from background and
* we want to close/re-open connections.
*/
public async redialPeers(peerInfo?: Libp2pPeerInfo) {
const dialed = peerInfo ? peerInfo.dialed : Array.from(this.dialedPeers)
const toDial = peerInfo
? [...peerInfo.connected, ...peerInfo.dialed]
: [...this.connectedPeers.keys(), ...this.dialedPeers]
public async redialPeers(peersToDial?: string[]) {
const dialed = peersToDial ?? Array.from(this.dialedPeers)
const toDial = peersToDial ?? [...this.connectedPeers.keys(), ...this.dialedPeers]

if (dialed.length === 0) {
this.logger('No peers to redial!')
Expand All @@ -122,9 +136,7 @@ export class Libp2pService extends EventEmitter {
this.logger(`Re-dialing ${dialed.length} peers`)

// TODO: Sort peers
for (const peerAddress of dialed) {
await this.hangUpPeer(peerAddress)
}
await this.hangUpPeers(dialed)

this.processInChunksService.updateData(toDial)
await this.processInChunksService.process()
Expand All @@ -142,7 +154,7 @@ export class Libp2pService extends EventEmitter {
start: false,
connectionManager: {
minConnections: 3, // TODO: increase?
maxConnections: 8, // TODO: increase?
maxConnections: 20, // TODO: increase?
dialTimeout: 120_000,
maxParallelDials: 10,
autoDial: true, // It's a default but let's set it to have explicit information
Expand Down
4 changes: 2 additions & 2 deletions packages/backend/src/nest/socket/socket.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -175,9 +175,9 @@ export class SocketService extends EventEmitter implements OnModuleInit {
}
)

socket.on(SocketActionTypes.LEAVE_COMMUNITY, async () => {
socket.on(SocketActionTypes.LEAVE_COMMUNITY, async (callback: (closed: boolean) => void) => {
this.logger('Leaving community')
this.emit(SocketActionTypes.LEAVE_COMMUNITY)
this.emit(SocketActionTypes.LEAVE_COMMUNITY, callback)
})

socket.on(SocketActionTypes.LIBP2P_PSK_STORED, payload => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ export class CertificatesStore extends EventEmitter {
write: ['*'],
},
})
await this.store.load()

this.store.events.on('ready', async () => {
this.logger('Loaded certificates to memory')
Expand All @@ -58,8 +59,8 @@ export class CertificatesStore extends EventEmitter {
await this.loadedCertificates()
})

// @ts-expect-error - OrbitDB's type declaration of `load` lacks 'options'
await this.store.load({ fetchEntryTimeout: 15000 })
// // @ts-expect-error - OrbitDB's type declaration of `load` lacks 'options'
// await this.store.load({ fetchEntryTimeout: 15000 })

this.logger('Initialized')
}
Expand Down Expand Up @@ -147,7 +148,9 @@ export class CertificatesStore extends EventEmitter {
* https://github.com/TryQuiet/quiet/issues/1899
*/
public async getCertificates(): Promise<string[]> {
this.logger('Getting certificates')
if (!this.store) {
this.logger('No store found!')
return []
}

Expand Down
11 changes: 7 additions & 4 deletions packages/backend/src/nest/tor/tor-control.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,7 @@ export class TorControl {
this.logger('Tor connected')
return
} catch (e) {
this.logger(e)
this.logger('Retrying...')
this.logger.error('Retrying due to error...', e)
await new Promise(r => setTimeout(r, 500))
}
}
Expand All @@ -76,7 +75,7 @@ export class TorControl {
try {
this.connection?.end()
} catch (e) {
this.logger.error('Disconnect failed:', e.message)
this.logger.error('Disconnect failed:', e)
}
this.connection = null
}
Expand All @@ -94,6 +93,7 @@ export class TorControl {
resolve({ code: 250, messages: dataArray })
} else {
clearTimeout(connectionTimeout)
console.error(`TOR CONNECTION ERROR: ${JSON.stringify(dataArray, null, 2)}`)
reject(`${dataArray[0]}`)
}
clearTimeout(connectionTimeout)
Expand All @@ -104,14 +104,17 @@ export class TorControl {
}

public async sendCommand(command: string): Promise<{ code: number; messages: string[] }> {
this.logger(`Sending tor command: ${command}`)
// Only send one command at a time.
if (this.isSending) {
this.logger('Tor connection already established, waiting...')
}

// Wait for existing command to finish.
while (this.isSending) {
await new Promise(r => setTimeout(r, 750))
const timeout = 750
this.logger(`Waiting for ${timeout}ms to retry command...`)
await new Promise(r => setTimeout(r, timeout))
}

this.isSending = true
Expand Down
Loading

0 comments on commit 9f60599

Please sign in to comment.