Add state management to network peers

This commit is contained in:
schlagmichdoch 2024-02-06 04:36:52 +01:00
parent 3dd40e238a
commit 1d62a9ff49
3 changed files with 116 additions and 44 deletions

View file

@ -180,6 +180,7 @@
"waiting": "Waiting…", "waiting": "Waiting…",
"processing": "Processing…", "processing": "Processing…",
"transferring": "Transferring…", "transferring": "Transferring…",
"receiving": "Receiving…" "receiving": "Receiving…",
"complete": "Transfer complete"
} }
} }

View file

@ -329,14 +329,14 @@ class Peer {
this._filesQueue = []; this._filesQueue = [];
this._busy = false; this._busy = false;
this._state = 'idle'; // 'idle', 'prepare', 'wait', 'receive', 'transfer', 'text-sent'
// evaluate auto accept // evaluate auto accept
this._evaluateAutoAccept(); this._evaluateAutoAccept();
} }
// Is overwritten in expanding classes
_onServerSignalMessage(message) {} _onServerSignalMessage(message) {}
// Is overwritten in expanding classes
_refresh() {} _refresh() {}
_onDisconnected() {} _onDisconnected() {}
@ -345,10 +345,8 @@ class Peer {
this._isCaller = isCaller; this._isCaller = isCaller;
} }
// Is overwritten in expanding classes
_sendMessage(message) {} _sendMessage(message) {}
// Is overwritten in expanding classes
_sendData(data) {} _sendData(data) {}
_sendDisplayName(displayName) { _sendDisplayName(displayName) {
@ -444,10 +442,28 @@ class Peer {
} }
_onPeerConnected() { _onPeerConnected() {
if (this._digester) { this._sendCurrentState();
}
_sendCurrentState() {
this._sendMessage({type: 'state', state: this._state})
}
_onReceiveState(peerState) {
if (this._state === "receive") {
if (peerState !== "transfer" || !this._digester) {
this._abortTransfer();
return;
}
// Reconnection during receiving of file. Send request for restart // Reconnection during receiving of file. Send request for restart
const offset = this._digester._bytesReceived; const offset = this._digester._bytesReceived;
this._sendResendRequest(offset); this._sendResendRequest(offset);
return
}
if (this._state === "transfer" && peerState !== "receive") {
this._abortTransfer();
return;
} }
} }
@ -455,7 +471,8 @@ class Peer {
let header = []; let header = [];
let totalSize = 0; let totalSize = 0;
let imagesOnly = true let imagesOnly = true
for (let i=0; i<files.length; i++) { this._state = 'prepare';
for (let i = 0; i < files.length; i++) {
Events.fire('set-progress', {peerId: this._peerId, progress: 0.8*i/files.length, status: 'prepare'}) Events.fire('set-progress', {peerId: this._peerId, progress: 0.8*i/files.length, status: 'prepare'})
header.push({ header.push({
name: files[i].name, name: files[i].name,
@ -488,6 +505,7 @@ class Peer {
thumbnailDataUrl: dataUrl thumbnailDataUrl: dataUrl
}); });
Events.fire('set-progress', {peerId: this._peerId, progress: 0, status: 'wait'}) Events.fire('set-progress', {peerId: this._peerId, progress: 0, status: 'wait'})
this._state = 'wait';
} }
sendFiles() { sendFiles() {
@ -527,7 +545,7 @@ class Peer {
_onResendRequest(offset) { _onResendRequest(offset) {
if (!this._chunker) { if (this._state !== 'transfer' || !this._chunker) {
this._sendTransferAbortion(); this._sendTransferAbortion();
return; return;
} }
@ -549,7 +567,7 @@ class Peer {
this._onFilesTransferRequest(message); this._onFilesTransferRequest(message);
break; break;
case 'header': case 'header':
this._onFileHeader(message); this._onHeader(message);
break; break;
case 'progress': case 'progress':
this._onProgress(message.progress); this._onProgress(message.progress);
@ -564,7 +582,7 @@ class Peer {
this._onFileTransferRequestResponded(message); this._onFileTransferRequestResponded(message);
break; break;
case 'file-transfer-complete': case 'file-transfer-complete':
this._onFileTransferCompleted(message); this._onFileTransferComplete(message);
break; break;
case 'message-transfer-complete': case 'message-transfer-complete':
this._onMessageTransferCompleted(); this._onMessageTransferCompleted();
@ -575,6 +593,9 @@ class Peer {
case 'display-name-changed': case 'display-name-changed':
this._onDisplayNameChanged(message); this._onDisplayNameChanged(message);
break; break;
case 'state':
this._onReceiveState(message.state);
break;
default: default:
Logger.warn('RTC: Unknown message type:', message.type); Logger.warn('RTC: Unknown message type:', message.type);
} }
@ -613,12 +634,13 @@ class Peer {
this._pendingRequest = null; this._pendingRequest = null;
} }
_onFileHeader(header) { _onHeader(header) {
if (!this._acceptedRequest || !this._acceptedRequest.header.length) { if (!this._acceptedRequest || !this._acceptedRequest.header.length) {
this._sendTransferAbortion(); this._sendTransferAbortion();
return; return;
} }
this._state = 'receive';
this._lastProgress = 0; this._lastProgress = 0;
this._timeStart = Date.now(); this._timeStart = Date.now();
this._addFileDigester(header); this._addFileDigester(header);
@ -631,16 +653,18 @@ class Peer {
} }
_abortTransfer() { _abortTransfer() {
Events.fire('set-progress', {peerId: this._peerId, progress: 1, status: 'wait'}); Events.fire('set-progress', {peerId: this._peerId, progress: 1, status: null});
Events.fire('notify-user', Localization.getTranslation("notifications.files-incorrect")); this._state = 'idle';
this._filesReceived = []; this._busy = false;
this._requestAccepted = null; this._chunker = null;
this._pendingRequest = null;
this._acceptedRequest = null;
this._digester = null; this._digester = null;
throw new Error("Received files differ from requested files. Abort!"); this._filesReceived = [];
} }
_onChunkReceived(chunk) { _onChunkReceived(chunk) {
if(!this._digester || !(chunk.byteLength || chunk.size)) return; if(this._state !== 'receive' || !this._digester || !(chunk.byteLength || chunk.size)) return;
this._digester.unchunk(chunk); this._digester.unchunk(chunk);
@ -648,6 +672,7 @@ class Peer {
if (progress > 1) { if (progress > 1) {
this._abortTransfer(); this._abortTransfer();
Logger.error("Too many bytes received. Abort!");
return; return;
} }
@ -665,42 +690,46 @@ class Peer {
} }
_onProgress(progress) { _onProgress(progress) {
if (this._state !== 'transfer') return;
Events.fire('set-progress', {peerId: this._peerId, progress: progress, status: 'transfer'}); Events.fire('set-progress', {peerId: this._peerId, progress: progress, status: 'transfer'});
} }
_onReceiveConfirmation(bytesReceived) { _onReceiveConfirmation(bytesReceived) {
if (!this._chunker) return; if (!this._chunker || this._state !== 'transfer') return;
this._chunker._onReceiveConfirmation(bytesReceived); this._chunker._onReceiveConfirmation(bytesReceived);
} }
async _onFileReceived(file) { _fitsHeader(file) {
if (!this._acceptedRequest) {
return false;
}
// Check if file fits to header
const acceptedHeader = this._acceptedRequest.header.shift(); const acceptedHeader = this._acceptedRequest.header.shift();
this._totalBytesReceived += file.size;
let duration = (Date.now() - this._timeStart) / 1000;
let size = Math.round(10 * file.size / 1000000) / 10;
let speed = Math.round(100 * file.size / 1000000 / duration) / 100;
Logger.log(`File received.\n\nSize: ${size} MB\tDuration: ${duration} s\tSpeed: ${speed} MB/s`);
this._sendMessage({type: 'file-transfer-complete', success: true, size: size, duration: duration, speed: speed});
const sameSize = file.size === acceptedHeader.size; const sameSize = file.size === acceptedHeader.size;
const sameName = file.name === acceptedHeader.name const sameName = file.name === acceptedHeader.name
if (!sameSize || !sameName) { return sameSize && sameName;
this._abortTransfer();
} }
_logTransferSpeed(size, duration, speed) {
Logger.log(`File received.\n\nSize: ${size} MB\tDuration: ${duration} s\tSpeed: ${speed} MB/s`);
}
_singleFileTransferComplete(file, duration, size, speed) {
this._totalBytesReceived += file.size;
this._sendMessage({type: 'file-transfer-complete', success: true, duration: duration, size: size, speed: speed});
// include for compatibility with 'Snapdrop & PairDrop for Android' app // include for compatibility with 'Snapdrop & PairDrop for Android' app
Events.fire('file-received', file); Events.fire('file-received', file);
this._filesReceived.push(file); this._filesReceived.push(file);
}
if (this._acceptedRequest.header.length) return; _allFilesTransferComplete() {
// We are done receiving
this._busy = false;
Events.fire('set-progress', {peerId: this._peerId, progress: 0, status: 'process'}); Events.fire('set-progress', {peerId: this._peerId, progress: 0, status: 'process'});
this._state = 'idle';
Events.fire('files-received', { Events.fire('files-received', {
peerId: this._peerId, peerId: this._peerId,
files: this._filesReceived, files: this._filesReceived,
@ -708,15 +737,43 @@ class Peer {
totalSize: this._acceptedRequest.totalSize totalSize: this._acceptedRequest.totalSize
}); });
this._filesReceived = []; this._filesReceived = [];
this._requestAccepted = null; this._acceptedRequest = null;
this._busy = false;
} }
_onFileTransferCompleted(message) { async _onFileReceived(file) {
if (!this._fitsHeader(file)) {
this._abortTransfer();
Events.fire('notify-user', Localization.getTranslation("notifications.files-incorrect"));
Logger.error("Received files differ from requested files. Abort!");
return;
}
const duration = (Date.now() - this._timeStart) / 1000;
const size = Math.round(10 * file.size / 1000000) / 10;
const speed = Math.round(100 * file.size / 1000000 / duration) / 100;
// Log speed
this._logTransferSpeed(duration, size, speed);
// File transfer complete
this._singleFileTransferComplete(file, duration, size, speed);
if (this._acceptedRequest.header.length) return;
// We are done receiving
Events.fire('set-progress', {peerId: this._peerId, progress: 1, status: 'transfer'});
this._allFilesTransferComplete();
}
_onFileTransferComplete(message) {
this._chunker = null; this._chunker = null;
if (!message.success) { if (!message.success) {
Logger.warn('File could not be sent'); Logger.warn('File could not be sent');
Events.fire('set-progress', {peerId: this._peerId, progress: 0, status: null}); Events.fire('set-progress', {peerId: this._peerId, progress: 0, status: null});
this._state = 'idle';
return; return;
} }
@ -728,37 +785,49 @@ class Peer {
} }
// No more files in queue. Transfer is complete // No more files in queue. Transfer is complete
this._state = 'idle';
this._busy = false; this._busy = false;
Events.fire('set-progress', {peerId: this._peerId, progress: 0, status: 'complete'});
Events.fire('notify-user', Localization.getTranslation("notifications.file-transfer-completed")); Events.fire('notify-user', Localization.getTranslation("notifications.file-transfer-completed"));
Events.fire('files-sent'); // used by 'Snapdrop & PairDrop for Android' app Events.fire('files-sent'); // used by 'Snapdrop & PairDrop for Android' app
} }
_onFileTransferRequestResponded(message) { _onFileTransferRequestResponded(message) {
if (!message.accepted) { if (!message.accepted || this._state !== 'wait') {
Events.fire('set-progress', {peerId: this._peerId, progress: 1, status: 'wait'}); Events.fire('set-progress', {peerId: this._peerId, progress: 1, status: null});
this._state = 'idle';
this._filesRequested = null; this._filesRequested = null;
return; return;
} }
Events.fire('file-transfer-accepted'); Events.fire('file-transfer-accepted');
Events.fire('set-progress', {peerId: this._peerId, progress: 0, status: 'transfer'}); Events.fire('set-progress', {peerId: this._peerId, progress: 0, status: 'transfer'});
this._state = 'transfer';
this.sendFiles(); this.sendFiles();
} }
_onMessageTransferCompleted() { _onMessageTransferCompleted() {
if (this._state !== 'text-sent') return;
this._state = 'idle';
Events.fire('notify-user', Localization.getTranslation("notifications.message-transfer-completed")); Events.fire('notify-user', Localization.getTranslation("notifications.message-transfer-completed"));
} }
sendText(text) { sendText(text) {
this._state = 'text-sent';
const unescaped = btoa(unescape(encodeURIComponent(text))); const unescaped = btoa(unescape(encodeURIComponent(text)));
this._sendMessage({ type: 'text', text: unescaped }); this._sendMessage({ type: 'text', text: unescaped });
} }
_onTextReceived(message) { _onTextReceived(message) {
if (!message.text) return; if (!message.text) return;
try {
const escaped = decodeURIComponent(escape(atob(message.text))); const escaped = decodeURIComponent(escape(atob(message.text)));
Events.fire('text-received', { text: escaped, peerId: this._peerId }); Events.fire('text-received', { text: escaped, peerId: this._peerId });
this._sendMessage({ type: 'message-transfer-complete' }); this._sendMessage({ type: 'message-transfer-complete' });
} }
catch (e) {
Logger.error(e);
}
}
_onDisplayNameChanged(message) { _onDisplayNameChanged(message) {
const displayNameHasChanged = message.displayName !== this._displayName; const displayNameHasChanged = message.displayName !== this._displayName;
@ -1133,7 +1202,6 @@ class RTCPeer extends Peer {
} }
async _sendFile(file) { async _sendFile(file) {
this._sendHeader(file);
this._chunker = new FileChunkerRTC( this._chunker = new FileChunkerRTC(
file, file,
chunk => this._sendData(chunk), chunk => this._sendData(chunk),
@ -1141,6 +1209,8 @@ class RTCPeer extends Peer {
this._dataChannel this._dataChannel
); );
this._chunker._readChunk(); this._chunker._readChunk();
this._sendHeader(file);
this._state = 'transfer';
} }
_onMessage(message) { _onMessage(message) {

View file

@ -715,7 +715,8 @@ class PeerUI {
"transfer": Localization.getTranslation("peer-ui.transferring"), "transfer": Localization.getTranslation("peer-ui.transferring"),
"receive": Localization.getTranslation("peer-ui.receiving"), "receive": Localization.getTranslation("peer-ui.receiving"),
"process": Localization.getTranslation("peer-ui.processing"), "process": Localization.getTranslation("peer-ui.processing"),
"wait": Localization.getTranslation("peer-ui.waiting") "wait": Localization.getTranslation("peer-ui.waiting"),
"complete": Localization.getTranslation("peer-ui.complete")
}[status]; }[status];
this.$el.setAttribute('status', status); this.$el.setAttribute('status', status);