From 9334cffc8917d6575ba953e47f228164166c5075 Mon Sep 17 00:00:00 2001 From: jmacxx <47253594+jmacxx@users.noreply.github.com> Date: Thu, 6 Jan 2022 11:49:55 -0600 Subject: [PATCH 1/4] Feature enabling log file upload to mediators --- .../main/java/bisq/common/file/FileUtil.java | 12 + .../network/CoreNetworkProtoResolver.java | 3 + .../bisq/core/support/dispute/Dispute.java | 27 ++ .../core/support/dispute/DisputeManager.java | 18 +- .../mediation/FileTransferReceiver.java | 107 ++++++++ .../dispute/mediation/FileTransferSender.java | 160 ++++++++++++ .../mediation/FileTransferSession.java | 151 +++++++++++ .../dispute/mediation/MediationManager.java | 61 ++++- .../resources/i18n/displayStrings.properties | 16 ++ .../mediation/FileTransferSessionTest.java | 207 +++++++++++++++ .../overlays/windows/SendLogFilesWindow.java | 245 ++++++++++++++++++ .../support/dispute/DisputeChatPopup.java | 18 +- .../main/support/dispute/DisputeView.java | 18 +- .../network/p2p/AckMessageSourceType.java | 3 +- .../bisq/network/p2p/FileTransferPart.java | 106 ++++++++ proto/src/main/proto/pb.proto | 11 + 16 files changed, 1151 insertions(+), 12 deletions(-) create mode 100644 core/src/main/java/bisq/core/support/dispute/mediation/FileTransferReceiver.java create mode 100644 core/src/main/java/bisq/core/support/dispute/mediation/FileTransferSender.java create mode 100644 core/src/main/java/bisq/core/support/dispute/mediation/FileTransferSession.java create mode 100644 core/src/test/java/bisq/core/support/dispute/mediation/FileTransferSessionTest.java create mode 100644 desktop/src/main/java/bisq/desktop/main/overlays/windows/SendLogFilesWindow.java create mode 100644 p2p/src/main/java/bisq/network/p2p/FileTransferPart.java diff --git a/common/src/main/java/bisq/common/file/FileUtil.java b/common/src/main/java/bisq/common/file/FileUtil.java index 1f5771e5010..bc8d906d094 100644 --- a/common/src/main/java/bisq/common/file/FileUtil.java +++ b/common/src/main/java/bisq/common/file/FileUtil.java @@ -34,6 +34,7 @@ import java.nio.file.Paths; import java.io.File; +import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; @@ -43,6 +44,7 @@ import java.util.Comparator; import java.util.Date; import java.util.List; +import java.util.Scanner; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -259,4 +261,14 @@ public static void removeAndBackupFile(File dbDir, File storageFile, String file renameFile(storageFile, corruptedFile); } } + + public static boolean doesFileContainKeyword(File file, String keyword) throws FileNotFoundException { + Scanner s = new Scanner(file); + while (s.hasNextLine()) { + if (s.nextLine().contains(keyword)) { + return true; + } + } + return false; + } } diff --git a/core/src/main/java/bisq/core/proto/network/CoreNetworkProtoResolver.java b/core/src/main/java/bisq/core/proto/network/CoreNetworkProtoResolver.java index 9d6a68584dc..98f4ad3d3e5 100644 --- a/core/src/main/java/bisq/core/proto/network/CoreNetworkProtoResolver.java +++ b/core/src/main/java/bisq/core/proto/network/CoreNetworkProtoResolver.java @@ -72,6 +72,7 @@ import bisq.network.p2p.AckMessage; import bisq.network.p2p.BundleOfEnvelopes; import bisq.network.p2p.CloseConnectionMessage; +import bisq.network.p2p.FileTransferPart; import bisq.network.p2p.PrefixedSealedAndSignedMessage; import bisq.network.p2p.peers.getdata.messages.GetDataResponse; import bisq.network.p2p.peers.getdata.messages.GetUpdatedDataRequest; @@ -131,6 +132,8 @@ public NetworkEnvelope fromProto(protobuf.NetworkEnvelope proto) throws Protobuf return Ping.fromProto(proto.getPing(), messageVersion); case PONG: return Pong.fromProto(proto.getPong(), messageVersion); + case FILE_TRANSFER_PART: + return FileTransferPart.fromProto(proto.getFileTransferPart(), messageVersion); case OFFER_AVAILABILITY_REQUEST: return OfferAvailabilityRequest.fromProto(proto.getOfferAvailabilityRequest(), messageVersion); diff --git a/core/src/main/java/bisq/core/support/dispute/Dispute.java b/core/src/main/java/bisq/core/support/dispute/Dispute.java index 5a0c3eac8f0..c721d0287c1 100644 --- a/core/src/main/java/bisq/core/support/dispute/Dispute.java +++ b/core/src/main/java/bisq/core/support/dispute/Dispute.java @@ -20,9 +20,15 @@ import bisq.core.locale.Res; import bisq.core.proto.CoreProtoResolver; import bisq.core.support.SupportType; +import bisq.core.support.dispute.mediation.FileTransferReceiver; +import bisq.core.support.dispute.mediation.FileTransferSender; +import bisq.core.support.dispute.mediation.FileTransferSession; import bisq.core.support.messages.ChatMessage; import bisq.core.trade.model.bisq_v1.Contract; +import bisq.network.p2p.NodeAddress; +import bisq.network.p2p.network.NetworkNode; + import bisq.common.crypto.PubKeyRing; import bisq.common.proto.ProtoUtil; import bisq.common.proto.network.NetworkPayload; @@ -46,6 +52,8 @@ import javafx.collections.FXCollections; import javafx.collections.ObservableList; +import java.io.IOException; + import java.util.ArrayList; import java.util.Date; import java.util.HashMap; @@ -154,6 +162,20 @@ public static protobuf.Dispute.State toProtoMessage(Dispute.State state) { private transient final BooleanProperty isClosedProperty = new SimpleBooleanProperty(); private transient final IntegerProperty badgeCountProperty = new SimpleIntegerProperty(); + private transient FileTransferReceiver fileTransferSession = null; + + public FileTransferReceiver createOrGetFileTransferReceiver(NetworkNode networkNode, NodeAddress peerNodeAddress, FileTransferSession.FtpCallback callback) throws IOException { + // the receiver stores its state temporarily here in the dispute + // this method gets called to retrieve the session each time a part of the log files is received + if (fileTransferSession == null) { + fileTransferSession = new FileTransferReceiver(networkNode, peerNodeAddress, this.tradeId, this.traderId, this.getRoleStringForLogFile(), callback); + } + return fileTransferSession; + } + + public FileTransferSender createFileTransferSender(NetworkNode networkNode, NodeAddress peerNodeAddress, FileTransferSession.FtpCallback callback) { + return new FileTransferSender(networkNode, peerNodeAddress, this.tradeId, this.traderId, this.getRoleStringForLogFile(), callback); + } /////////////////////////////////////////////////////////////////////////////////////////// // Constructor @@ -441,6 +463,11 @@ public String getRoleString() { } } + public String getRoleStringForLogFile() { + return (disputeOpenerIsBuyer ? "BUYER" : "SELLER") + "_" + + (disputeOpenerIsMaker ? "MAKER" : "TAKER"); + } + @Override public String toString() { return "Dispute{" + diff --git a/core/src/main/java/bisq/core/support/dispute/DisputeManager.java b/core/src/main/java/bisq/core/support/dispute/DisputeManager.java index 79a2e57fd6f..6a490da2928 100644 --- a/core/src/main/java/bisq/core/support/dispute/DisputeManager.java +++ b/core/src/main/java/bisq/core/support/dispute/DisputeManager.java @@ -794,7 +794,7 @@ private Tuple2 getNodeAddressPubKeyRingTuple(Dispute di return new Tuple2<>(peerNodeAddress, receiverPubKeyRing); } - private boolean isAgent(Dispute dispute) { + public boolean isAgent(Dispute dispute) { return pubKeyRing.equals(dispute.getAgentPubKeyRing()); } @@ -812,7 +812,7 @@ private Optional findDispute(ChatMessage message) { return findDispute(message.getTradeId(), message.getTraderId()); } - private Optional findDispute(String tradeId, int traderId) { + protected Optional findDispute(String tradeId, int traderId) { T disputeList = getDisputeList(); if (disputeList == null) { log.warn("disputes is null"); @@ -873,6 +873,20 @@ public void addMediationReOpenedMessage(Dispute dispute, boolean senderIsTrader) requestPersistence(); } + protected void addMediationLogsReceivedMessage(Dispute dispute, String logsIdentifier) { + String logsReceivedMessage = Res.get("support.mediatorReceivedLogs", logsIdentifier); + ChatMessage chatMessage = new ChatMessage( + getSupportType(), + dispute.getTradeId(), + pubKeyRing.hashCode(), + false, + logsReceivedMessage, + p2PService.getAddress()); + chatMessage.setSystemMessage(true); + dispute.addAndPersistChatMessage(chatMessage); + requestPersistence(); + } + // If price was going down between take offer time and open dispute time the buyer has an incentive to // not send the payment but to try to make a new trade with the better price. We risks to lose part of the // security deposit (in mediation we will always get back 0.003 BTC to keep some incentive to accept mediated diff --git a/core/src/main/java/bisq/core/support/dispute/mediation/FileTransferReceiver.java b/core/src/main/java/bisq/core/support/dispute/mediation/FileTransferReceiver.java new file mode 100644 index 00000000000..c10609d9197 --- /dev/null +++ b/core/src/main/java/bisq/core/support/dispute/mediation/FileTransferReceiver.java @@ -0,0 +1,107 @@ +package bisq.core.support.dispute.mediation; + +import bisq.network.p2p.AckMessage; +import bisq.network.p2p.AckMessageSourceType; +import bisq.network.p2p.FileTransferPart; +import bisq.network.p2p.NodeAddress; +import bisq.network.p2p.network.NetworkNode; + +import bisq.common.UserThread; +import bisq.common.config.Config; +import bisq.common.util.Utilities; + +import java.nio.file.FileSystems; + +import java.io.File; +import java.io.IOException; +import java.io.RandomAccessFile; + +import java.util.concurrent.TimeUnit; + +import lombok.extern.slf4j.Slf4j; + +import javax.annotation.Nullable; + +@Slf4j +public class FileTransferReceiver extends FileTransferSession { + protected final String zipFilePath; + + public FileTransferReceiver(NetworkNode networkNode, NodeAddress peerNodeAddress, + String tradeId, int traderId, String traderRole, @Nullable FileTransferSession.FtpCallback callback) throws IOException { + super(networkNode, peerNodeAddress, tradeId, traderId, traderRole, callback); + zipFilePath = ensureReceivingDirectoryExists().getAbsolutePath() + FileSystems.getDefault().getSeparator() + zipId + ".zip"; + } + + public void processFilePartReceived(FileTransferPart ftp) { + checkpointLastActivity(); + // check that the supplied sequence number is in line with what we are expecting + if (currentBlockSeqNum < 0) { + // we have not yet started receiving a file, validate this ftp packet as the initiation request + initReceiveSession(ftp.uid, ftp.seqNumOrFileLength); + } else if (currentBlockSeqNum == ftp.seqNumOrFileLength) { + // we are in the middle of receiving a file; add the block of data to the file + processReceivedBlock(ftp, networkNode, peerNodeAddress); + } else { + log.error("ftp sequence num mismatch, expected {} received {}", currentBlockSeqNum, ftp.seqNumOrFileLength); + resetSession(); // aborts the file transfer + } + } + + public void initReceiveSession(String uid, long expectedFileBytes) { + networkNode.addMessageListener(this); + this.expectedFileLength = expectedFileBytes; + fileOffsetBytes = 0; + currentBlockSeqNum = 0; + initSessionTimer(); + log.info("Received a start file transfer request, tradeId={}, traderId={}, size={}", fullTradeId, traderId, expectedFileBytes); + log.info("New file will be written to {}", zipFilePath); + UserThread.execute(() -> { + ackReceivedPart(uid, networkNode, peerNodeAddress); + }); + } + + private void processReceivedBlock(FileTransferPart ftp, NetworkNode networkNode, NodeAddress peerNodeAddress) { + try { + RandomAccessFile file = new RandomAccessFile(zipFilePath, "rwd"); + file.seek(fileOffsetBytes); + file.write(ftp.messageData.toByteArray(), 0, ftp.messageData.size()); + fileOffsetBytes = fileOffsetBytes + ftp.messageData.size(); + log.info("Sequence number {} for {}, received data {} / {}", + ftp.seqNumOrFileLength, Utilities.getShortId(ftp.tradeId), fileOffsetBytes, expectedFileLength); + currentBlockSeqNum++; + UserThread.runAfter(() -> { + ackReceivedPart(ftp.uid, networkNode, peerNodeAddress); + if (fileOffsetBytes >= expectedFileLength) { + log.info("Success! We have reached the EOF, received {} expected {}", fileOffsetBytes, expectedFileLength); + ftpCallback.ifPresent(c -> c.onFtpComplete(this)); + resetSession(); + } + }, 100, TimeUnit.MILLISECONDS); + } catch (IOException e) { + log.error(e.toString()); + e.printStackTrace(); + } + } + + private void ackReceivedPart(String uid, NetworkNode networkNode, NodeAddress peerNodeAddress) { + AckMessage ackMessage = new AckMessage(peerNodeAddress, + AckMessageSourceType.LOG_TRANSFER, + FileTransferPart.class.getSimpleName(), + uid, + Utilities.getShortId(fullTradeId), + true, // result + null); // errorMessage + log.info("Send AckMessage for {} to peer {}. id={}, uid={}", + ackMessage.getSourceMsgClassName(), peerNodeAddress, ackMessage.getSourceId(), ackMessage.getSourceUid()); + sendMessage(ackMessage, networkNode, peerNodeAddress); + } + + private static File ensureReceivingDirectoryExists() throws IOException { + File directory = new File(Config.appDataDir() + "/clientLogs"); + if (!directory.exists() && !directory.mkdirs()) { + log.error("Could not create directory {}", directory.getAbsolutePath()); + throw new IOException("Could not create directory: " + directory.getAbsolutePath()); + } + return directory; + } +} diff --git a/core/src/main/java/bisq/core/support/dispute/mediation/FileTransferSender.java b/core/src/main/java/bisq/core/support/dispute/mediation/FileTransferSender.java new file mode 100644 index 00000000000..3c6e95f3a47 --- /dev/null +++ b/core/src/main/java/bisq/core/support/dispute/mediation/FileTransferSender.java @@ -0,0 +1,160 @@ +package bisq.core.support.dispute.mediation; + +import bisq.network.p2p.FileTransferPart; +import bisq.network.p2p.NodeAddress; +import bisq.network.p2p.network.NetworkNode; + +import bisq.common.UserThread; +import bisq.common.config.Config; + +import com.google.protobuf.ByteString; + +import java.net.URI; + +import java.nio.file.FileSystem; +import java.nio.file.FileSystems; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardCopyOption; + +import java.io.IOException; +import java.io.RandomAccessFile; + +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; +import java.util.concurrent.TimeUnit; +import java.util.stream.Stream; + +import lombok.extern.slf4j.Slf4j; + +import javax.annotation.Nullable; + +import static bisq.common.file.FileUtil.doesFileContainKeyword; + +@Slf4j +public class FileTransferSender extends FileTransferSession { + protected final String zipFilePath; + + public FileTransferSender(NetworkNode networkNode, NodeAddress peerNodeAddress, + String tradeId, int traderId, String traderRole, @Nullable FileTransferSession.FtpCallback callback) { + super(networkNode, peerNodeAddress, tradeId, traderId, traderRole, callback); + zipFilePath = Config.appDataDir() + FileSystems.getDefault().getSeparator() + zipId + ".zip"; + updateProgress(); + } + + public void createZipFileToSend() { + try { + Map env = new HashMap<>(); + env.put("create", "true"); + FileSystem zipfs = FileSystems.newFileSystem(URI.create("jar:file:" + zipFilePath), env); + Files.createDirectory(zipfs.getPath(zipId)); // store logfiles in a usefully-named subdir + Stream paths = Files.walk(Paths.get(Config.appDataDir().toString()), 1); + paths.filter(Files::isRegularFile).forEach(externalTxtFile -> { + try { + // always include bisq.log; and other .log files if they contain the TradeId + if (externalTxtFile.getFileName().toString().equals("bisq.log") || + (externalTxtFile.getFileName().toString().matches(".*.log") && + doesFileContainKeyword(externalTxtFile.toFile(), fullTradeId))) { + Path pathInZipfile = zipfs.getPath(zipId + "/" + externalTxtFile.getFileName().toString()); + log.info("adding {} to zip file {}", pathInZipfile, zipfs); + Files.copy(externalTxtFile, pathInZipfile, StandardCopyOption.REPLACE_EXISTING); + } + } catch (IOException e) { + log.error(e.toString()); + e.printStackTrace(); + } + }); + zipfs.close(); + } catch (IOException ex) { + log.error(ex.toString()); + ex.printStackTrace(); + } + } + + public void initSend() throws IOException { + initSessionTimer(); + networkNode.addMessageListener(this); + RandomAccessFile file = new RandomAccessFile(zipFilePath, "r"); + expectedFileLength = file.length(); + // an empty block is sent as request to initiate file transfer, peer must ACK for transfer to continue + dataAwaitingAck = Optional.of(new FileTransferPart(networkNode.getNodeAddress(), fullTradeId, traderId, UUID.randomUUID().toString(), expectedFileLength, ByteString.EMPTY)); + uploadData(); + } + + public void sendNextBlock() throws IOException, IllegalStateException { + if (dataAwaitingAck.isPresent()) { + log.warn("prepNextBlockToSend invoked, but we are still waiting for a previous ACK"); + throw new IllegalStateException("prepNextBlockToSend invoked, but we are still waiting for a previous ACK"); + } + RandomAccessFile file = new RandomAccessFile(zipFilePath, "r"); + file.seek(fileOffsetBytes); + byte[] buff = new byte[FILE_BLOCK_SIZE]; + int nBytesRead = file.read(buff, 0, FILE_BLOCK_SIZE); + if (nBytesRead < 0) { + log.info("Success! We have reached the EOF, {} bytes sent. Removing zip file {}", fileOffsetBytes, zipFilePath); + Files.delete(Paths.get(zipFilePath)); + ftpCallback.ifPresent(c -> c.onFtpComplete(this)); + UserThread.runAfter(this::resetSession, 1); + return; + } + dataAwaitingAck = Optional.of(new FileTransferPart(networkNode.getNodeAddress(), fullTradeId, traderId, UUID.randomUUID().toString(), currentBlockSeqNum, ByteString.copyFrom(buff, 0, nBytesRead))); + uploadData(); + } + + public void retrySend() { + if (transferIsInProgress()) { + log.info("Retry send of current block"); + initSessionTimer(); + uploadData(); + } else { + UserThread.runAfter(() -> ftpCallback.ifPresent((f) -> f.onFtpTimeout("Could not re-send", this)), 1); + } + } + + protected void uploadData() { + if (!dataAwaitingAck.isPresent()) { + return; + } + FileTransferPart ftp = dataAwaitingAck.get(); + log.info("Send FileTransferPart seq {} length {} to peer {}, UID={}", + ftp.seqNumOrFileLength, ftp.messageData.size(), peerNodeAddress, ftp.uid); + sendMessage(ftp, networkNode, peerNodeAddress); + } + + public boolean processAckForFilePart(String ackUid) { + if (!dataAwaitingAck.isPresent()) { + log.warn("We received an ACK we were not expecting. {}", ackUid); + return false; + } + if (!dataAwaitingAck.get().uid.equals(ackUid)) { + log.warn("We received an ACK that has a different UID to what we were expecting. We ignore and wait for the correct ACK"); + log.info("Received {} expecting {}", ackUid, dataAwaitingAck.get().uid); + return false; + } + // fileOffsetBytes gets incremented by the size of the block that was ack'd + fileOffsetBytes += dataAwaitingAck.get().messageData.size(); + currentBlockSeqNum++; + dataAwaitingAck = Optional.empty(); + checkpointLastActivity(); + updateProgress(); + UserThread.runAfter(() -> { // to trigger continuing the file transfer + try { + sendNextBlock(); + } catch (IOException e) { + log.error(e.toString()); + e.printStackTrace(); + } + }, 100, TimeUnit.MILLISECONDS); + return true; + } + + public void updateProgress() { + double progressPct = expectedFileLength > 0 ? + ((double) fileOffsetBytes / expectedFileLength) : 0.0; + ftpCallback.ifPresent(c -> c.onFtpProgress(progressPct)); + log.info("ftp progress: {}", String.format("%.0f%%", progressPct * 100)); + } +} diff --git a/core/src/main/java/bisq/core/support/dispute/mediation/FileTransferSession.java b/core/src/main/java/bisq/core/support/dispute/mediation/FileTransferSession.java new file mode 100644 index 00000000000..85e9ebe4e1b --- /dev/null +++ b/core/src/main/java/bisq/core/support/dispute/mediation/FileTransferSession.java @@ -0,0 +1,151 @@ +package bisq.core.support.dispute.mediation; + +import bisq.network.p2p.AckMessage; +import bisq.network.p2p.AckMessageSourceType; +import bisq.network.p2p.FileTransferPart; +import bisq.network.p2p.NodeAddress; +import bisq.network.p2p.network.Connection; +import bisq.network.p2p.network.MessageListener; +import bisq.network.p2p.network.NetworkNode; + +import bisq.common.UserThread; +import bisq.common.proto.network.NetworkEnvelope; +import bisq.common.util.Utilities; + +import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.MoreExecutors; +import com.google.common.util.concurrent.SettableFuture; + +import java.text.SimpleDateFormat; + +import java.util.Date; +import java.util.Optional; +import java.util.concurrent.TimeUnit; + +import lombok.extern.slf4j.Slf4j; +import lombok.Getter; + +import org.jetbrains.annotations.NotNull; + +import javax.annotation.Nullable; + +import static bisq.network.p2p.network.Connection.getPermittedMessageSize; + +@Slf4j +public abstract class FileTransferSession implements MessageListener { + protected static final int FTP_SESSION_TIMEOUT_MILLIS = (int) TimeUnit.SECONDS.toMillis(60); + protected static final int FILE_BLOCK_SIZE = getPermittedMessageSize() - 1024; // allowing space for protobuf + + public interface FtpCallback { + void onFtpProgress(double progressPct); + void onFtpComplete(FileTransferSession session); + void onFtpTimeout(String statusMsg, FileTransferSession session); + } + + @Getter + protected final String fullTradeId; + @Getter + protected final int traderId; + @Getter + protected final String zipId; + protected final Optional ftpCallback; + protected final NetworkNode networkNode; // for sending network messages + protected final NodeAddress peerNodeAddress; + protected Optional dataAwaitingAck; + protected long fileOffsetBytes; + protected long currentBlockSeqNum; + protected long expectedFileLength; + protected long lastActivityTime; + + public FileTransferSession(NetworkNode networkNode, NodeAddress peerNodeAddress, + String tradeId, int traderId, String traderRole, @Nullable FileTransferSession.FtpCallback callback) { + this.networkNode = networkNode; + this.peerNodeAddress = peerNodeAddress; + this.fullTradeId = tradeId; + this.traderId = traderId; + this.ftpCallback = Optional.ofNullable(callback); + this.zipId = Utilities.getShortId(fullTradeId) + "_" + traderRole.toUpperCase() + "_" + + new SimpleDateFormat("yyyyMMdd_HHmmss").format(new Date()); + resetSession(); + } + + public void resetSession() { + lastActivityTime = 0; + currentBlockSeqNum = -1; + fileOffsetBytes = 0; + expectedFileLength = 0; + dataAwaitingAck = Optional.empty(); + networkNode.removeMessageListener(this); + log.info("Ftp session parameters have been reset."); + } + + @Override + public void onMessage(NetworkEnvelope networkEnvelope, Connection connection) { + if (networkEnvelope instanceof FileTransferPart) { + // mediator receiving log file data + FileTransferPart ftp = (FileTransferPart) networkEnvelope; + if (this instanceof FileTransferReceiver) { + ((FileTransferReceiver) this).processFilePartReceived(ftp); + } + } else if (networkEnvelope instanceof AckMessage) { + AckMessage ackMessage = (AckMessage) networkEnvelope; + if (ackMessage.getSourceType() == AckMessageSourceType.LOG_TRANSFER) { + if (ackMessage.isSuccess()) { + log.info("Received AckMessage for {} with id {} and uid {}", + ackMessage.getSourceMsgClassName(), ackMessage.getSourceId(), ackMessage.getSourceUid()); + if (this instanceof FileTransferSender) { + ((FileTransferSender) this).processAckForFilePart(ackMessage.getSourceUid()); + } + } else { + log.warn("Received AckMessage with error state for {} with id {} and errorMessage={}", + ackMessage.getSourceMsgClassName(), ackMessage.getSourceId(), ackMessage.getErrorMessage()); + } + } + } + } + + protected void checkpointLastActivity() { + lastActivityTime = System.currentTimeMillis(); + } + + protected void initSessionTimer() { + UserThread.runAfter(() -> { + if (!transferIsInProgress()) // transfer may have finished before this timer executes + return; + if (System.currentTimeMillis() - lastActivityTime < FTP_SESSION_TIMEOUT_MILLIS) { + log.info("Last activity was {}, we have not yet timed out.", new Date(lastActivityTime)); + initSessionTimer(); + } else { + log.warn("File transfer session timed out. expected: {} received: {}", expectedFileLength, fileOffsetBytes); + ftpCallback.ifPresent((e) -> e.onFtpTimeout("Timed out during send", this)); + } + }, FTP_SESSION_TIMEOUT_MILLIS / 4, TimeUnit.MILLISECONDS); // check more frequently than the timeout + } + + protected boolean transferIsInProgress() { + return fileOffsetBytes != expectedFileLength; + } + + protected void sendMessage(NetworkEnvelope message, NetworkNode networkNode, NodeAddress nodeAddress) { + SettableFuture future = networkNode.sendMessage(nodeAddress, message); + if (future != null) { // is null when testing with Mockito + Futures.addCallback(future, new FutureCallback<>() { + @Override + public void onSuccess(Connection connection) { + } + + @Override + public void onFailure(@NotNull Throwable throwable) { + String errorSend = "Sending " + message.getClass().getSimpleName() + + " to " + nodeAddress.getFullAddress() + + " failed. That is expected if the peer is offline.\n\t" + + ".\n\tException=" + throwable.getMessage(); + log.warn(errorSend); + ftpCallback.ifPresent((f) -> f.onFtpTimeout("Peer offline", FileTransferSession.this)); + resetSession(); + } + }, MoreExecutors.directExecutor()); + } + } +} diff --git a/core/src/main/java/bisq/core/support/dispute/mediation/MediationManager.java b/core/src/main/java/bisq/core/support/dispute/mediation/MediationManager.java index b2d64bcb94e..0fba279c209 100644 --- a/core/src/main/java/bisq/core/support/dispute/mediation/MediationManager.java +++ b/core/src/main/java/bisq/core/support/dispute/mediation/MediationManager.java @@ -42,8 +42,11 @@ import bisq.core.trade.protocol.bisq_v1.model.ProcessModel; import bisq.network.p2p.AckMessageSourceType; +import bisq.network.p2p.FileTransferPart; import bisq.network.p2p.NodeAddress; import bisq.network.p2p.P2PService; +import bisq.network.p2p.network.Connection; +import bisq.network.p2p.network.MessageListener; import bisq.common.Timer; import bisq.common.UserThread; @@ -52,12 +55,15 @@ import bisq.common.crypto.KeyRing; import bisq.common.handlers.ErrorMessageHandler; import bisq.common.handlers.ResultHandler; +import bisq.common.proto.network.NetworkEnvelope; import org.bitcoinj.core.Coin; import com.google.inject.Inject; import com.google.inject.Singleton; +import java.io.IOException; + import java.util.Optional; import lombok.extern.slf4j.Slf4j; @@ -69,7 +75,7 @@ @Slf4j @Singleton -public final class MediationManager extends DisputeManager { +public final class MediationManager extends DisputeManager implements MessageListener, FileTransferSession.FtpCallback { /////////////////////////////////////////////////////////////////////////////////////////// // Constructor @@ -90,6 +96,7 @@ public MediationManager(P2PService p2PService, PriceFeedService priceFeedService) { super(p2PService, tradeWalletService, walletService, walletsSetup, tradeManager, closedTradableManager, openOfferManager, daoFacade, keyRing, mediationDisputeListService, config, priceFeedService); + p2PService.getNetworkNode().addMessageListener(this); // listening for FileTransferPart message } @@ -272,4 +279,56 @@ public void rejectMediationResult(Trade trade) { trade.setMediationResultState(MediationResultState.MEDIATION_RESULT_REJECTED); tradeManager.requestPersistence(); } + + public FileTransferSender initLogUpload(FileTransferSession.FtpCallback callback, String tradeId, int traderId) throws IOException { + Dispute dispute = findDispute(tradeId, traderId) + .orElseThrow(() -> new IOException("could not locate Dispute for tradeId/traderId")); + return dispute.createFileTransferSender(p2PService.getNetworkNode(), + dispute.getContract().getMediatorNodeAddress(), callback); + } + + private void processFilePartReceived(FileTransferPart ftp) { + if (!ftp.isInitialRequest()) { + return; // existing sessions are processed by FileTransferSession object directly + } + // we create a new session which is related to an open dispute from our list + Optional dispute = findDispute(ftp.getTradeId(), ftp.getTraderId()); + if (!dispute.isPresent()) { + log.error("Received log upload request for unknown TradeId/TraderId {}/{}", ftp.getTradeId(), ftp.getTraderId()); + return; + } + if (dispute.get().isClosed()) { + log.error("Received a file transfer request for closed dispute {}", ftp.getTradeId()); + return; + } + try { + FileTransferReceiver session = dispute.get().createOrGetFileTransferReceiver( + p2PService.getNetworkNode(), ftp.getSenderNodeAddress(), this); + session.processFilePartReceived(ftp); + } catch (IOException e) { + log.error("Unable to process a received file message" + e); + } + } + + @Override + public void onMessage(NetworkEnvelope networkEnvelope, Connection connection) { + if (networkEnvelope instanceof FileTransferPart) { // mediator receiving log file data + FileTransferPart ftp = (FileTransferPart) networkEnvelope; + processFilePartReceived(ftp); + } + } + + @Override + public void onFtpProgress(double progressPct) { + log.trace("ftp progress: {}", progressPct); + } + @Override + public void onFtpComplete(FileTransferSession session) { + Optional dispute = findDispute(session.getFullTradeId(), session.getTraderId()); + dispute.ifPresent(d -> addMediationLogsReceivedMessage(d, session.getZipId())); + } + @Override + public void onFtpTimeout(String statusMsg, FileTransferSession session) { + session.resetSession(); + } } diff --git a/core/src/main/resources/i18n/displayStrings.properties b/core/src/main/resources/i18n/displayStrings.properties index 09a2ccd34eb..7d8ba931364 100644 --- a/core/src/main/resources/i18n/displayStrings.properties +++ b/core/src/main/resources/i18n/displayStrings.properties @@ -1238,6 +1238,21 @@ support.buyerOfferer=BTC buyer/Maker support.sellerOfferer=BTC seller/Maker support.buyerTaker=BTC buyer/Taker support.sellerTaker=BTC seller/Taker +support.sendLogs.title=Send Logs +support.sendLogs.backgroundInfo=When traders experience a bug, mediators and support staff will often request copies of the users logs to diagnose the issue.\n\n\ + Upon pressing [Send], your log file will be compressed and transmitted direct to the mediator. +support.sendLogs.step1=Create Zip Archive of Logs +support.sendLogs.step2=Connection Request to Mediator +support.sendLogs.step3=Upload Archived Log Data +support.sendLogs.waiting=Waiting for your input +support.sendLogs.send=Send +support.sendLogs.stop=Stop +support.sendLogs.init=Initializing +support.sendLogs.retry=Retrying send +support.sendLogs.stopped=Transfer stopped +support.sendLogs.progress=Transfer progress: %.0f%% +support.sendLogs.finished=Transfer complete! +support.sendLogs.command=Press [Send] to retry, or [Stop] to abort support.backgroundInfo=Bisq is not a company, so it handles disputes differently.\n\n\ Traders can communicate within the application via secure chat on the open trades screen to try solving disputes on their own. \ @@ -1281,6 +1296,7 @@ support.peerOpenedTicket=Your trading peer has requested support due to technica support.peerOpenedDispute=Your trading peer has requested a dispute.\n\n{0}\n\nBisq version: {1} support.peerOpenedDisputeForMediation=Your trading peer has requested mediation.\n\n{0}\n\nBisq version: {1} support.mediatorsDisputeSummary=System message: Mediator''s dispute summary:\n{0} +support.mediatorReceivedLogs=System message: Mediator has received logs: {0} support.mediatorsAddress=Mediator''s node address: {0} support.warning.disputesWithInvalidDonationAddress=The delayed payout transaction has used an invalid receiver address. \ It does not match any of the DAO parameter values for the valid donation addresses.\n\nThis might be a scam attempt. \ diff --git a/core/src/test/java/bisq/core/support/dispute/mediation/FileTransferSessionTest.java b/core/src/test/java/bisq/core/support/dispute/mediation/FileTransferSessionTest.java new file mode 100644 index 00000000000..8e74cb6f029 --- /dev/null +++ b/core/src/test/java/bisq/core/support/dispute/mediation/FileTransferSessionTest.java @@ -0,0 +1,207 @@ +package bisq.core.support.dispute.mediation; + +import bisq.network.p2p.FileTransferPart; +import bisq.network.p2p.NodeAddress; +import bisq.network.p2p.network.NetworkNode; + +import bisq.common.config.Config; + +import java.io.FileWriter; +import java.io.IOException; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import static org.mockito.Mockito.*; + +public class FileTransferSessionTest implements FileTransferSession.FtpCallback { + + double notedProgressPct = -1.0; + int progressInvocations = 0; + boolean ftpCompleteStatus = false; + String testTradeId = "foo"; + int testTraderId = 123; + String testClientId = "bar"; + NetworkNode networkNode; + NodeAddress counterpartyNodeAddress; + + @Before + public void setUp() throws Exception { + new Config(); // static methods like Config.appDataDir() require config to be created once + networkNode = mock(NetworkNode.class); + when(networkNode.getNodeAddress()).thenReturn(new NodeAddress("null:0000")); + counterpartyNodeAddress = new NodeAddress("null:0000"); + } + + @Test + public void testSendCreate() { + new FileTransferSender(networkNode, counterpartyNodeAddress, testTradeId, testTraderId, testClientId, this); + Assert.assertEquals(0.0, notedProgressPct, 0.0); + Assert.assertEquals(1, progressInvocations); + } + + @Test + public void testSendInitialize() { + // checks that the initial send request packet contains correct information + try { + int testVerifyDataSize = 13; + FileTransferSender session = initializeSession(testVerifyDataSize); + session.initSend(); + FileTransferPart ftp = session.dataAwaitingAck.get(); + Assert.assertEquals(ftp.tradeId, testTradeId); + Assert.assertTrue(ftp.uid.length() > 0); + Assert.assertEquals(0, ftp.messageData.size()); + Assert.assertEquals(ftp.seqNumOrFileLength, testVerifyDataSize); + Assert.assertEquals(-1, session.currentBlockSeqNum); + return; + } catch (IOException e) { + e.printStackTrace(); + } + Assert.fail(); + } + + @Test + public void testSendSmallFile() { + try { + int testVerifyDataSize = 13; + FileTransferSender session = initializeSession(testVerifyDataSize); + // the first block contains zero data, as it is a "request to send" + session.initSend(); + simulateAckFromPeerAndVerify(session, 0, 0, 2); + // the second block contains all the test file data (because it is a small file) + session.sendNextBlock(); + simulateAckFromPeerAndVerify(session, testVerifyDataSize, 1, 3); + // the final invocation sends no data, and wraps up the session + session.sendNextBlock(); + Assert.assertEquals(1, session.currentBlockSeqNum); + Assert.assertEquals(3, progressInvocations); + Assert.assertEquals(1.0, notedProgressPct, 0.0); + Assert.assertTrue(ftpCompleteStatus); + } catch (IOException ioe) { + ioe.printStackTrace(); + Assert.fail(); + } + } + + @Test + public void testSendOneFullBlock() { + try { + int testVerifyDataSize = FileTransferSession.FILE_BLOCK_SIZE; + FileTransferSender session = initializeSession(testVerifyDataSize); + // the first block contains zero data, as it is a "request to send" + session.initSend(); + simulateAckFromPeerAndVerify(session, 0, 0, 2); + // the second block contains all the test file data (because it is a small file) + session.sendNextBlock(); + simulateAckFromPeerAndVerify(session, testVerifyDataSize, 1, 3); + // the final invocation sends no data, and wraps up the session + session.sendNextBlock(); + Assert.assertEquals(1, session.currentBlockSeqNum); + Assert.assertEquals(3, progressInvocations); + Assert.assertEquals(1.0, notedProgressPct, 0.0); + Assert.assertTrue(ftpCompleteStatus); + } catch (IOException ioe) { + ioe.printStackTrace(); + Assert.fail(); + } + } + + @Test + public void testSendTwoFullBlocks() { + try { + int testVerifyDataSize = FileTransferSession.FILE_BLOCK_SIZE * 2; + FileTransferSender session = initializeSession(testVerifyDataSize); + // the first block contains zero data, as it is a "request to send" + session.initSend(); + simulateAckFromPeerAndVerify(session, 0, 0, 2); + // the second block contains half of the test file data + session.sendNextBlock(); + simulateAckFromPeerAndVerify(session, testVerifyDataSize / 2, 1, 3); + // the third block contains half of the test file data + session.sendNextBlock(); + simulateAckFromPeerAndVerify(session, testVerifyDataSize / 2, 2, 4); + // the final invocation sends no data, and wraps up the session + session.sendNextBlock(); + Assert.assertEquals(2, session.currentBlockSeqNum); + Assert.assertEquals(4, progressInvocations); + Assert.assertEquals(1.0, notedProgressPct, 0.0); + Assert.assertTrue(ftpCompleteStatus); + } catch (IOException ioe) { + ioe.printStackTrace(); + Assert.fail(); + } + } + + @Test + public void testSendTwoFullBlocksPlusOneByte() { + try { + int testVerifyDataSize = 1 + FileTransferSession.FILE_BLOCK_SIZE * 2; + FileTransferSender session = initializeSession(testVerifyDataSize); + // the first block contains zero data, as it is a "request to send" + session.initSend(); + simulateAckFromPeerAndVerify(session, 0, 0, 2); + session.sendNextBlock(); + simulateAckFromPeerAndVerify(session, FileTransferSession.FILE_BLOCK_SIZE, 1, 3); + session.sendNextBlock(); + simulateAckFromPeerAndVerify(session, FileTransferSession.FILE_BLOCK_SIZE, 2, 4); + // the fourth block contains one byte + session.sendNextBlock(); + simulateAckFromPeerAndVerify(session, 1, 3, 5); + // the final invocation sends no data, and wraps up the session + session.sendNextBlock(); + Assert.assertEquals(3, session.currentBlockSeqNum); + Assert.assertEquals(5, progressInvocations); + Assert.assertEquals(1.0, notedProgressPct, 0.0); + Assert.assertTrue(ftpCompleteStatus); + } catch (IOException ioe) { + ioe.printStackTrace(); + Assert.fail(); + } + } + + private FileTransferSender initializeSession(int testSize) { + try { + FileTransferSender session = new FileTransferSender(networkNode, counterpartyNodeAddress, testTradeId, testTraderId, testClientId, this); + // simulate a file for sending + FileWriter fileWriter = new FileWriter(session.zipFilePath); + char[] buf = new char[testSize]; + for (int x = 0; x < testSize; x++) + buf[x] = 'A'; + fileWriter.write(buf); + fileWriter.close(); + Assert.assertFalse(ftpCompleteStatus); + Assert.assertEquals(1, progressInvocations); + Assert.assertEquals(0.0, notedProgressPct, 0.0); + Assert.assertFalse(session.processAckForFilePart("not_expected_uid")); + return session; + } catch (IOException e) { + e.printStackTrace(); + } + Assert.fail(); + return null; + } + + private void simulateAckFromPeerAndVerify(FileTransferSender session, int expectedDataSize, long expectedSeqNum, int expectedProgressInvocations) { + FileTransferPart ftp = session.dataAwaitingAck.get(); + Assert.assertEquals(expectedDataSize, ftp.messageData.size()); + Assert.assertTrue(session.processAckForFilePart(ftp.uid)); + Assert.assertEquals(expectedSeqNum, session.currentBlockSeqNum); + Assert.assertEquals(expectedProgressInvocations, progressInvocations); + } + + @Override + public void onFtpProgress(double progressPct) { + notedProgressPct = progressPct; + progressInvocations++; + } + + @Override + public void onFtpComplete(FileTransferSession session) { + ftpCompleteStatus = true; + } + + @Override + public void onFtpTimeout(String status, FileTransferSession session) { + } +} diff --git a/desktop/src/main/java/bisq/desktop/main/overlays/windows/SendLogFilesWindow.java b/desktop/src/main/java/bisq/desktop/main/overlays/windows/SendLogFilesWindow.java new file mode 100644 index 00000000000..e15be9ff2fc --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/main/overlays/windows/SendLogFilesWindow.java @@ -0,0 +1,245 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.desktop.main.overlays.windows; + +import bisq.desktop.components.AutoTooltipButton; +import bisq.desktop.main.overlays.Overlay; +import bisq.desktop.main.portfolio.pendingtrades.steps.TradeWizardItem; +import bisq.desktop.main.portfolio.pendingtrades.steps.buyer.BuyerStep1View; +import bisq.desktop.main.portfolio.pendingtrades.steps.buyer.BuyerStep2View; +import bisq.desktop.main.portfolio.pendingtrades.steps.buyer.BuyerStep3View; + +import bisq.core.locale.Res; +import bisq.core.support.dispute.mediation.FileTransferSender; +import bisq.core.support.dispute.mediation.FileTransferSession; +import bisq.core.support.dispute.mediation.MediationManager; + +import bisq.common.UserThread; + +import javafx.scene.layout.GridPane; +import javafx.scene.layout.HBox; +import javafx.scene.control.Button; +import javafx.scene.control.ProgressBar; +import javafx.scene.control.Label; +import javafx.scene.control.Separator; +import javafx.scene.layout.Region; + +import javafx.geometry.HPos; +import javafx.geometry.Insets; +import javafx.geometry.Orientation; +import javafx.geometry.Pos; +import javafx.beans.property.SimpleDoubleProperty; +import javafx.beans.property.DoubleProperty; + +import java.io.IOException; + +import lombok.extern.slf4j.Slf4j; + +import static bisq.desktop.util.FormBuilder.addMultilineLabel; + +@Slf4j +public class SendLogFilesWindow extends Overlay implements FileTransferSession.FtpCallback { + + private final String tradeId; + private final int traderId; + private final MediationManager mediationManager; + private ProgressBar progressBar; + private Label statusLabel; + private Button sendButton, stopButton; + private final DoubleProperty ftpProgress = new SimpleDoubleProperty(-1); + TradeWizardItem step1, step2, step3; + private FileTransferSender fileTransferSender; + + public SendLogFilesWindow(String tradeId, int traderId, + MediationManager mediationManager) { + this.tradeId = tradeId; + this.traderId = traderId; + this.mediationManager = mediationManager; + type = Type.Attention; + } + + public void show() { + headLine = Res.get("support.sendLogs.title"); + width = 668; + createGridPane(); + addHeadLine(); + addContent(); + addButtons(); + applyStyles(); + display(); + } + + @Override + protected void createGridPane() { + gridPane = new GridPane(); + gridPane.setHgap(5); + gridPane.setVgap(5); + gridPane.setPadding(new Insets(64, 64, 64, 64)); + gridPane.setPrefWidth(width); + } + + void addWizardsToGridPane(TradeWizardItem tradeWizardItem) { + GridPane.setRowIndex(tradeWizardItem, rowIndex++); + GridPane.setColumnIndex(tradeWizardItem, 0); + GridPane.setHalignment(tradeWizardItem, HPos.LEFT); + gridPane.getChildren().add(tradeWizardItem); + } + + void addLineSeparatorToGridPane() { + final Separator separator = new Separator(Orientation.VERTICAL); + separator.setMinHeight(22); + GridPane.setMargin(separator, new Insets(0, 0, 0, 13)); + GridPane.setHalignment(separator, HPos.LEFT); + GridPane.setRowIndex(separator, rowIndex++); + gridPane.getChildren().add(separator); + } + + void addRegionToGridPane() { + final Region region = new Region(); + region.setMinHeight(22); + GridPane.setMargin(region, new Insets(0, 0, 0, 13)); + GridPane.setRowIndex(region, rowIndex++); + gridPane.getChildren().add(region); + } + + private void addContent() { + this.hideCloseButton = true; + + addMultilineLabel(gridPane, ++rowIndex, Res.get("support.sendLogs.backgroundInfo"), 0); + addRegionToGridPane(); + + step1 = new TradeWizardItem(BuyerStep1View.class, Res.get("support.sendLogs.step1"), "1"); + step2 = new TradeWizardItem(BuyerStep2View.class, Res.get("support.sendLogs.step2"), "2"); + step3 = new TradeWizardItem(BuyerStep3View.class, Res.get("support.sendLogs.step3"), "3"); + + addRegionToGridPane(); + addRegionToGridPane(); + addWizardsToGridPane(step1); + addLineSeparatorToGridPane(); + addWizardsToGridPane(step2); + addLineSeparatorToGridPane(); + addWizardsToGridPane(step3); + addRegionToGridPane(); + + progressBar = new ProgressBar(); + progressBar.setMinHeight(19); + progressBar.setMaxHeight(19); + progressBar.setPrefWidth(9305); + progressBar.progressProperty().bind(ftpProgress); + gridPane.add(progressBar, 0, ++rowIndex); + + statusLabel = addMultilineLabel(gridPane, ++rowIndex, Res.get("support.sendLogs.waiting")); + addRegionToGridPane(); + + sendButton = new AutoTooltipButton(Res.get("support.sendLogs.send")); + stopButton = new AutoTooltipButton(Res.get("support.sendLogs.stop")); + closeButton = new AutoTooltipButton(Res.get("shared.close")); + sendButton.setOnAction(e -> { + try { + if (fileTransferSender == null) { + setActiveStep(1); + statusLabel.setText(Res.get("support.sendLogs.init")); + fileTransferSender = mediationManager.initLogUpload(this, tradeId, traderId); + UserThread.runAfter(() -> { + fileTransferSender.createZipFileToSend(); + setActiveStep(2); + UserThread.runAfter(() -> { + setActiveStep(3); + try { + fileTransferSender.initSend(); + } catch (IOException ioe) { + log.error(ioe.toString()); + statusLabel.setText(ioe.toString()); + ioe.printStackTrace(); + } + }, 1); + }, 1); + sendButton.setDisable(true); + stopButton.setDisable(false); + } else { + // resend the latest block in the event of a timeout + statusLabel.setText(Res.get("support.sendLogs.retry")); + fileTransferSender.retrySend(); + sendButton.setDisable(true); + } + } catch (IOException ex) { + log.error(ex.toString()); + statusLabel.setText(ex.toString()); + ex.printStackTrace(); + } + }); + stopButton.setOnAction(e -> { + if (fileTransferSender != null) { + fileTransferSender.resetSession(); + statusLabel.setText(Res.get("support.sendLogs.stopped")); + stopButton.setDisable(true); + } + }); + closeButton.setOnAction(e -> { + hide(); + closeHandlerOptional.ifPresent(Runnable::run); + }); + HBox hBox = new HBox(); + hBox.setSpacing(10); + hBox.setAlignment(Pos.CENTER_RIGHT); + GridPane.setRowIndex(hBox, ++rowIndex); + GridPane.setColumnSpan(hBox, 2); + GridPane.setColumnIndex(hBox, 0); + hBox.getChildren().addAll(sendButton, stopButton, closeButton); + gridPane.getChildren().add(hBox); + GridPane.setMargin(hBox, new Insets(10, 0, 0, 0)); + } + + void setActiveStep(int step) { + if (step < 1) { + step1.setDisabled(); + step2.setDisabled(); + step3.setDisabled(); + } else if (step == 1) { + step1.setActive(); + } else if (step == 2) { + step1.setCompleted(); + step2.setActive(); + } else if (step == 3) { + step2.setCompleted(); + step3.setActive(); + } else { + step3.setCompleted(); + } + } + + @Override + public void onFtpProgress(double progressPct) { + if (progressPct > 0.0) { + statusLabel.setText(String.format(Res.get("support.sendLogs.progress"), progressPct * 100)); + sendButton.setDisable(true); + } + ftpProgress.set(progressPct); + } + @Override + public void onFtpComplete(FileTransferSession session) { + setActiveStep(4); // all finished + statusLabel.setText(Res.get("support.sendLogs.finished")); + stopButton.setDisable(true); + } + @Override + public void onFtpTimeout(String statusMsg, FileTransferSession session) { + statusLabel.setText(statusMsg + "\r\n" + Res.get("support.sendLogs.command")); + sendButton.setDisable(false); + } +} diff --git a/desktop/src/main/java/bisq/desktop/main/support/dispute/DisputeChatPopup.java b/desktop/src/main/java/bisq/desktop/main/support/dispute/DisputeChatPopup.java index 73294461476..1ea9a5e160b 100644 --- a/desktop/src/main/java/bisq/desktop/main/support/dispute/DisputeChatPopup.java +++ b/desktop/src/main/java/bisq/desktop/main/support/dispute/DisputeChatPopup.java @@ -51,6 +51,7 @@ public class DisputeChatPopup { public interface ChatCallback { void onCloseDisputeFromChatWindow(Dispute dispute); + void onSendLogsFromChatWindow(Dispute dispute); } private Stage chatPopupStage; @@ -102,12 +103,19 @@ public void openChat(Dispute selectedDispute, DisputeSession concreteDisputeSess AnchorPane.setTopAnchor(chatView, -20d); AnchorPane.setBottomAnchor(chatView, 10d); pane.getStyleClass().add("dispute-chat-border"); - Button closeDisputeButton = null; - if (!selectedDispute.isClosed() && !disputeManager.isTrader(selectedDispute)) { - closeDisputeButton = new AutoTooltipButton(Res.get("support.closeTicket")); - closeDisputeButton.setOnAction(e -> chatCallback.onCloseDisputeFromChatWindow(selectedDispute)); + if (selectedDispute.isClosed()) { + chatView.display(concreteDisputeSession, null, pane.widthProperty()); + } else { + if (disputeManager.isAgent(selectedDispute)) { + Button closeDisputeButton = new AutoTooltipButton(Res.get("support.closeTicket")); + closeDisputeButton.setOnAction(e -> chatCallback.onCloseDisputeFromChatWindow(selectedDispute)); + chatView.display(concreteDisputeSession, closeDisputeButton, pane.widthProperty()); + } else { + Button sendLogsButton = new AutoTooltipButton("Send Logs"); + sendLogsButton.setOnAction(e -> chatCallback.onSendLogsFromChatWindow(selectedDispute)); + chatView.display(concreteDisputeSession, sendLogsButton, pane.widthProperty()); + } } - chatView.display(concreteDisputeSession, closeDisputeButton, pane.widthProperty()); chatView.activate(); chatView.scrollToBottom(); chatPopupStage = new Stage(); diff --git a/desktop/src/main/java/bisq/desktop/main/support/dispute/DisputeView.java b/desktop/src/main/java/bisq/desktop/main/support/dispute/DisputeView.java index 0447bc9eafc..8aa20e357a5 100644 --- a/desktop/src/main/java/bisq/desktop/main/support/dispute/DisputeView.java +++ b/desktop/src/main/java/bisq/desktop/main/support/dispute/DisputeView.java @@ -28,6 +28,7 @@ import bisq.desktop.main.overlays.popups.Popup; import bisq.desktop.main.overlays.windows.ContractWindow; import bisq.desktop.main.overlays.windows.DisputeSummaryWindow; +import bisq.desktop.main.overlays.windows.SendLogFilesWindow; import bisq.desktop.main.overlays.windows.SendPrivateNotificationWindow; import bisq.desktop.main.overlays.windows.TradeDetailsWindow; import bisq.desktop.main.overlays.windows.VerifyDisputeResultSignatureWindow; @@ -125,7 +126,7 @@ import static bisq.desktop.util.FormBuilder.getIconForLabel; import static bisq.desktop.util.FormBuilder.getRegularIconButton; -public abstract class DisputeView extends ActivatableView implements PeerInfoIcon.notify { +public abstract class DisputeView extends ActivatableView implements PeerInfoIcon.notify, DisputeChatPopup.ChatCallback { public enum FilterResult { NO_MATCH("No Match"), NO_FILTER("No filter text"), @@ -228,8 +229,7 @@ public DisputeView(DisputeManager> disputeManager this.refundAgentManager = refundAgentManager; this.daoFacade = daoFacade; this.useDevPrivilegeKeys = useDevPrivilegeKeys; - DisputeChatPopup.ChatCallback chatCallback = this::handleOnProcessDispute; - chatPopup = new DisputeChatPopup(disputeManager, formatter, preferences, chatCallback); + chatPopup = new DisputeChatPopup(disputeManager, formatter, preferences, this); } @Override @@ -1499,4 +1499,16 @@ public void avatarTagUpdated() { }); } + @Override + public void onCloseDisputeFromChatWindow(Dispute dispute) { + handleOnProcessDispute(dispute); + } + + @Override + public void onSendLogsFromChatWindow(Dispute dispute) { + if (!(disputeManager instanceof MediationManager)) + return; + MediationManager mediationManager = (MediationManager) disputeManager; + new SendLogFilesWindow(dispute.getTradeId(), dispute.getTraderId(), mediationManager).show(); + } } diff --git a/p2p/src/main/java/bisq/network/p2p/AckMessageSourceType.java b/p2p/src/main/java/bisq/network/p2p/AckMessageSourceType.java index 4724bc7e56f..5d0f7bcfa17 100644 --- a/p2p/src/main/java/bisq/network/p2p/AckMessageSourceType.java +++ b/p2p/src/main/java/bisq/network/p2p/AckMessageSourceType.java @@ -24,5 +24,6 @@ public enum AckMessageSourceType { ARBITRATION_MESSAGE, MEDIATION_MESSAGE, TRADE_CHAT_MESSAGE, - REFUND_MESSAGE + REFUND_MESSAGE, + LOG_TRANSFER } diff --git a/p2p/src/main/java/bisq/network/p2p/FileTransferPart.java b/p2p/src/main/java/bisq/network/p2p/FileTransferPart.java new file mode 100644 index 00000000000..e3d822fea32 --- /dev/null +++ b/p2p/src/main/java/bisq/network/p2p/FileTransferPart.java @@ -0,0 +1,106 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.network.p2p; + +import bisq.common.app.Version; +import bisq.common.proto.network.NetworkEnvelope; + +import com.google.protobuf.ByteString; + +import lombok.EqualsAndHashCode; +import lombok.Value; + +@EqualsAndHashCode(callSuper = true) +@Value +public final class FileTransferPart extends NetworkEnvelope implements ExtendedDataSizePermission, SendersNodeAddressMessage { + private final NodeAddress senderNodeAddress; + public String uid; + public String tradeId; + public int traderId; + public long seqNumOrFileLength; + public ByteString messageData; // if message_data is empty it is the first message, requesting file upload permission + + public FileTransferPart(NodeAddress senderNodeAddress, + String tradeId, + int traderId, + String uid, + long seqNumOrFileLength, + ByteString messageData) { + this(senderNodeAddress, tradeId, traderId, uid, seqNumOrFileLength, messageData, Version.getP2PMessageVersion()); + } + + public boolean isInitialRequest() { + return messageData.size() == 0; + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + private FileTransferPart(NodeAddress senderNodeAddress, + String tradeId, + int traderId, + String uid, + long seqNumOrFileLength, + ByteString messageData, + int messageVersion) { + super(messageVersion); + this.senderNodeAddress = senderNodeAddress; + this.tradeId = tradeId; + this.traderId = traderId; + this.uid = uid; + this.seqNumOrFileLength = seqNumOrFileLength; + this.messageData = messageData; + } + + @Override + public protobuf.NetworkEnvelope toProtoNetworkEnvelope() { + return getNetworkEnvelopeBuilder() + .setFileTransferPart(protobuf.FileTransferPart.newBuilder() + .setSenderNodeAddress(senderNodeAddress.toProtoMessage()) + .setTradeId(tradeId) + .setTraderId(traderId) + .setUid(uid) + .setSeqNumOrFileLength(seqNumOrFileLength) + .setMessageData(messageData) + .build()) + .build(); + } + + public static FileTransferPart fromProto(protobuf.FileTransferPart proto, int messageVersion) { + return new FileTransferPart( + NodeAddress.fromProto(proto.getSenderNodeAddress()), + proto.getTradeId(), + proto.getTraderId(), + proto.getUid(), + proto.getSeqNumOrFileLength(), + proto.getMessageData(), + messageVersion); + } + + @Override + public String toString() { + return "FileTransferPart{" + + "\n senderNodeAddress='" + senderNodeAddress.getHostNameForDisplay() + '\'' + + ",\n uid='" + uid + '\'' + + ",\n tradeId='" + tradeId + '\'' + + ",\n traderId='" + traderId + '\'' + + ",\n seqNumOrFileLength=" + seqNumOrFileLength + + "\n} " + super.toString(); + } +} diff --git a/proto/src/main/proto/pb.proto b/proto/src/main/proto/pb.proto index 7f9b4d435fc..5b9e3a0a468 100644 --- a/proto/src/main/proto/pb.proto +++ b/proto/src/main/proto/pb.proto @@ -89,6 +89,8 @@ message NetworkEnvelope { BsqSwapTxInputsMessage bsq_swap_tx_inputs_message = 57; BsqSwapFinalizeTxRequest bsq_swap_finalize_tx_request = 58; BsqSwapFinalizedTxMessage bsq_swap_finalized_tx_message = 59; + + FileTransferPart file_transfer_part = 60; } } @@ -124,6 +126,15 @@ message GetUpdatedDataRequest { string version = 4; } +message FileTransferPart { + NodeAddress sender_node_address = 1; + string uid = 2; + string trade_id = 3; + int32 trader_id = 4; + int64 seq_num_or_file_length = 5; + bytes message_data = 6; +} + // peers message GetPeersRequest { From 3ea15438820747db88a3a4ef1bdce2a1edda5faf Mon Sep 17 00:00:00 2001 From: jmacxx <47253594+jmacxx@users.noreply.github.com> Date: Fri, 7 Jan 2022 19:42:24 -0600 Subject: [PATCH 2/4] fixes for Windows compatability, test added --- .../dispute/mediation/FileTransferSender.java | 4 +++- .../dispute/mediation/FileTransferSessionTest.java | 12 ++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/core/src/main/java/bisq/core/support/dispute/mediation/FileTransferSender.java b/core/src/main/java/bisq/core/support/dispute/mediation/FileTransferSender.java index 3c6e95f3a47..fd59beea5bb 100644 --- a/core/src/main/java/bisq/core/support/dispute/mediation/FileTransferSender.java +++ b/core/src/main/java/bisq/core/support/dispute/mediation/FileTransferSender.java @@ -49,7 +49,7 @@ public void createZipFileToSend() { try { Map env = new HashMap<>(); env.put("create", "true"); - FileSystem zipfs = FileSystems.newFileSystem(URI.create("jar:file:" + zipFilePath), env); + FileSystem zipfs = FileSystems.newFileSystem(URI.create("jar:file:///" + (zipFilePath.replace('\\', '/'))), env); Files.createDirectory(zipfs.getPath(zipId)); // store logfiles in a usefully-named subdir Stream paths = Files.walk(Paths.get(Config.appDataDir().toString()), 1); paths.filter(Files::isRegularFile).forEach(externalTxtFile -> { @@ -79,6 +79,7 @@ public void initSend() throws IOException { networkNode.addMessageListener(this); RandomAccessFile file = new RandomAccessFile(zipFilePath, "r"); expectedFileLength = file.length(); + file.close(); // an empty block is sent as request to initiate file transfer, peer must ACK for transfer to continue dataAwaitingAck = Optional.of(new FileTransferPart(networkNode.getNodeAddress(), fullTradeId, traderId, UUID.randomUUID().toString(), expectedFileLength, ByteString.EMPTY)); uploadData(); @@ -93,6 +94,7 @@ public void sendNextBlock() throws IOException, IllegalStateException { file.seek(fileOffsetBytes); byte[] buff = new byte[FILE_BLOCK_SIZE]; int nBytesRead = file.read(buff, 0, FILE_BLOCK_SIZE); + file.close(); if (nBytesRead < 0) { log.info("Success! We have reached the EOF, {} bytes sent. Removing zip file {}", fileOffsetBytes, zipFilePath); Files.delete(Paths.get(zipFilePath)); diff --git a/core/src/test/java/bisq/core/support/dispute/mediation/FileTransferSessionTest.java b/core/src/test/java/bisq/core/support/dispute/mediation/FileTransferSessionTest.java index 8e74cb6f029..42fad3c4cbe 100644 --- a/core/src/test/java/bisq/core/support/dispute/mediation/FileTransferSessionTest.java +++ b/core/src/test/java/bisq/core/support/dispute/mediation/FileTransferSessionTest.java @@ -6,6 +6,7 @@ import bisq.common.config.Config; +import java.io.File; import java.io.FileWriter; import java.io.IOException; @@ -41,6 +42,17 @@ public void testSendCreate() { Assert.assertEquals(1, progressInvocations); } + @Test + public void testCreateZip() { + FileTransferSender sender = new FileTransferSender(networkNode, counterpartyNodeAddress, testTradeId, testTraderId, testClientId, this); + Assert.assertEquals(0.0, notedProgressPct, 0.0); + Assert.assertEquals(1, progressInvocations); + sender.createZipFileToSend(); + File file = new File(sender.zipFilePath); + Assert.assertTrue(file.exists()); + Assert.assertTrue(file.length() > 0); + } + @Test public void testSendInitialize() { // checks that the initial send request packet contains correct information From a1d0e570d141c4a6af30e2772f7b0de2ab502492 Mon Sep 17 00:00:00 2001 From: Christoph Atteneder Date: Tue, 11 Jan 2022 12:36:44 +0100 Subject: [PATCH 3/4] Apply minor improvements and clean-ups --- .../main/java/bisq/common/file/FileUtil.java | 2 +- .../core/support/dispute/DisputeManager.java | 17 +++++----- .../mediation/FileTransferReceiver.java | 23 +++++++++++--- .../dispute/mediation/FileTransferSender.java | 31 ++++++++++++++----- .../mediation/FileTransferSession.java | 21 ++++++++++++- .../dispute/mediation/MediationManager.java | 16 +++++----- .../resources/i18n/displayStrings.properties | 14 ++++----- .../mediation/FileTransferSessionTest.java | 20 +++++++++++- desktop/src/main/java/bisq/desktop/bisq.css | 2 +- .../overlays/windows/SendLogFilesWindow.java | 22 ++++++++----- .../support/dispute/DisputeChatPopup.java | 10 +++--- .../src/main/java/bisq/desktop/theme-dark.css | 2 ++ .../main/java/bisq/desktop/theme-light.css | 2 ++ .../bisq/network/p2p/FileTransferPart.java | 4 +-- 14 files changed, 130 insertions(+), 56 deletions(-) diff --git a/common/src/main/java/bisq/common/file/FileUtil.java b/common/src/main/java/bisq/common/file/FileUtil.java index bc8d906d094..1d5e8912da7 100644 --- a/common/src/main/java/bisq/common/file/FileUtil.java +++ b/common/src/main/java/bisq/common/file/FileUtil.java @@ -130,7 +130,7 @@ public static void deleteDirectory(File file, try { deleteFileIfExists(file, ignoreLockedFiles); } catch (Throwable t) { - log.error("Could not delete file. Error=" + t.toString()); + log.error("Could not delete file. Error=" + t); throw new IOException(t); } } diff --git a/core/src/main/java/bisq/core/support/dispute/DisputeManager.java b/core/src/main/java/bisq/core/support/dispute/DisputeManager.java index 6a490da2928..68e0a351bd0 100644 --- a/core/src/main/java/bisq/core/support/dispute/DisputeManager.java +++ b/core/src/main/java/bisq/core/support/dispute/DisputeManager.java @@ -145,7 +145,7 @@ public void requestPersistence() { @Override public NodeAddress getPeerNodeAddress(ChatMessage message) { Optional disputeOptional = findDispute(message); - if (!disputeOptional.isPresent()) { + if (disputeOptional.isEmpty()) { log.warn("Could not find dispute for tradeId = {} traderId = {}", message.getTradeId(), message.getTraderId()); return null; @@ -156,7 +156,7 @@ public NodeAddress getPeerNodeAddress(ChatMessage message) { @Override public PubKeyRing getPeerPubKeyRing(ChatMessage message) { Optional disputeOptional = findDispute(message); - if (!disputeOptional.isPresent()) { + if (disputeOptional.isEmpty()) { log.warn("Could not find dispute for tradeId = {} traderId = {}", message.getTradeId(), message.getTraderId()); return null; @@ -325,7 +325,7 @@ protected void onOpenNewDisputeMessage(OpenNewDisputeMessage openNewDisputeMessa if (isAgent(dispute)) { if (!disputeList.contains(dispute)) { Optional storedDisputeOptional = findDispute(dispute); - if (!storedDisputeOptional.isPresent()) { + if (storedDisputeOptional.isEmpty()) { disputeList.add(dispute); sendPeerOpenedDisputeMessage(dispute, contract, peersPubKeyRing); } else { @@ -378,7 +378,7 @@ protected void onPeerOpenedDisputeMessage(PeerOpenedDisputeMessage peerOpenedDis Dispute dispute = peerOpenedDisputeMessage.getDispute(); Optional optionalTrade = tradeManager.getTradeById(dispute.getTradeId()); - if (!optionalTrade.isPresent()) { + if (optionalTrade.isEmpty()) { return; } @@ -399,11 +399,10 @@ protected void onPeerOpenedDisputeMessage(PeerOpenedDisputeMessage peerOpenedDis if (!isAgent(dispute)) { if (!disputeList.contains(dispute)) { Optional storedDisputeOptional = findDispute(dispute); - if (!storedDisputeOptional.isPresent()) { + if (storedDisputeOptional.isEmpty()) { disputeList.add(dispute); trade.setDisputeState(getDisputeStateStartedByPeer()); tradeManager.requestPersistence(); - errorMessage = null; } else { // valid case if both have opened a dispute and agent was not online. log.debug("We got a dispute already open for that trade and trading peer. TradeId = {}", @@ -452,7 +451,7 @@ public void sendOpenNewDisputeMessage(Dispute dispute, } Optional storedDisputeOptional = findDispute(dispute); - if (!storedDisputeOptional.isPresent() || reOpen) { + if (storedDisputeOptional.isEmpty() || reOpen) { String disputeInfo = getDisputeInfo(dispute); String disputeMessage = getDisputeIntroForDisputeCreator(disputeInfo); String sysMsg = dispute.isSupportTicket() ? @@ -836,7 +835,7 @@ public Optional findDispute(String tradeId) { public Optional findTrade(Dispute dispute) { Optional retVal = tradeManager.getTradeById(dispute.getTradeId()); - if (!retVal.isPresent()) { + if (retVal.isEmpty()) { retVal = closedTradableManager.getClosedTrades().stream().filter(e -> e.getId().equals(dispute.getTradeId())).findFirst(); } return retVal; @@ -972,7 +971,7 @@ private Price getPrice(String currencyCode) { long roundedToLong = MathUtils.roundDoubleToLong(scaled); return Price.valueOf(currencyCode, roundedToLong); } catch (Exception e) { - log.error("Exception at getPrice / parseToFiat: " + e.toString()); + log.error("Exception at getPrice / parseToFiat: " + e); return null; } } else { diff --git a/core/src/main/java/bisq/core/support/dispute/mediation/FileTransferReceiver.java b/core/src/main/java/bisq/core/support/dispute/mediation/FileTransferReceiver.java index c10609d9197..06597c2d740 100644 --- a/core/src/main/java/bisq/core/support/dispute/mediation/FileTransferReceiver.java +++ b/core/src/main/java/bisq/core/support/dispute/mediation/FileTransferReceiver.java @@ -1,3 +1,20 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + package bisq.core.support.dispute.mediation; import bisq.network.p2p.AckMessage; @@ -27,7 +44,7 @@ public class FileTransferReceiver extends FileTransferSession { protected final String zipFilePath; public FileTransferReceiver(NetworkNode networkNode, NodeAddress peerNodeAddress, - String tradeId, int traderId, String traderRole, @Nullable FileTransferSession.FtpCallback callback) throws IOException { + String tradeId, int traderId, String traderRole, @Nullable FileTransferSession.FtpCallback callback) throws IOException { super(networkNode, peerNodeAddress, tradeId, traderId, traderRole, callback); zipFilePath = ensureReceivingDirectoryExists().getAbsolutePath() + FileSystems.getDefault().getSeparator() + zipId + ".zip"; } @@ -55,9 +72,7 @@ public void initReceiveSession(String uid, long expectedFileBytes) { initSessionTimer(); log.info("Received a start file transfer request, tradeId={}, traderId={}, size={}", fullTradeId, traderId, expectedFileBytes); log.info("New file will be written to {}", zipFilePath); - UserThread.execute(() -> { - ackReceivedPart(uid, networkNode, peerNodeAddress); - }); + UserThread.execute(() -> ackReceivedPart(uid, networkNode, peerNodeAddress)); } private void processReceivedBlock(FileTransferPart ftp, NetworkNode networkNode, NodeAddress peerNodeAddress) { diff --git a/core/src/main/java/bisq/core/support/dispute/mediation/FileTransferSender.java b/core/src/main/java/bisq/core/support/dispute/mediation/FileTransferSender.java index fd59beea5bb..e7eecacaff0 100644 --- a/core/src/main/java/bisq/core/support/dispute/mediation/FileTransferSender.java +++ b/core/src/main/java/bisq/core/support/dispute/mediation/FileTransferSender.java @@ -1,3 +1,20 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + package bisq.core.support.dispute.mediation; import bisq.network.p2p.FileTransferPart; @@ -39,7 +56,7 @@ public class FileTransferSender extends FileTransferSession { protected final String zipFilePath; public FileTransferSender(NetworkNode networkNode, NodeAddress peerNodeAddress, - String tradeId, int traderId, String traderRole, @Nullable FileTransferSession.FtpCallback callback) { + String tradeId, int traderId, String traderRole, @Nullable FileTransferSession.FtpCallback callback) { super(networkNode, peerNodeAddress, tradeId, traderId, traderRole, callback); zipFilePath = Config.appDataDir() + FileSystems.getDefault().getSeparator() + zipId + ".zip"; updateProgress(); @@ -54,10 +71,10 @@ public void createZipFileToSend() { Stream paths = Files.walk(Paths.get(Config.appDataDir().toString()), 1); paths.filter(Files::isRegularFile).forEach(externalTxtFile -> { try { - // always include bisq.log; and other .log files if they contain the TradeId - if (externalTxtFile.getFileName().toString().equals("bisq.log") || - (externalTxtFile.getFileName().toString().matches(".*.log") && - doesFileContainKeyword(externalTxtFile.toFile(), fullTradeId))) { + // always include bisq.log; and other .log files if they contain the TradeId + if (externalTxtFile.getFileName().toString().equals("bisq.log") || + (externalTxtFile.getFileName().toString().matches(".*.log") && + doesFileContainKeyword(externalTxtFile.toFile(), fullTradeId))) { Path pathInZipfile = zipfs.getPath(zipId + "/" + externalTxtFile.getFileName().toString()); log.info("adding {} to zip file {}", pathInZipfile, zipfs); Files.copy(externalTxtFile, pathInZipfile, StandardCopyOption.REPLACE_EXISTING); @@ -117,7 +134,7 @@ public void retrySend() { } protected void uploadData() { - if (!dataAwaitingAck.isPresent()) { + if (dataAwaitingAck.isEmpty()) { return; } FileTransferPart ftp = dataAwaitingAck.get(); @@ -127,7 +144,7 @@ protected void uploadData() { } public boolean processAckForFilePart(String ackUid) { - if (!dataAwaitingAck.isPresent()) { + if (dataAwaitingAck.isEmpty()) { log.warn("We received an ACK we were not expecting. {}", ackUid); return false; } diff --git a/core/src/main/java/bisq/core/support/dispute/mediation/FileTransferSession.java b/core/src/main/java/bisq/core/support/dispute/mediation/FileTransferSession.java index 85e9ebe4e1b..336e8eac7b4 100644 --- a/core/src/main/java/bisq/core/support/dispute/mediation/FileTransferSession.java +++ b/core/src/main/java/bisq/core/support/dispute/mediation/FileTransferSession.java @@ -1,3 +1,20 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + package bisq.core.support.dispute.mediation; import bisq.network.p2p.AckMessage; @@ -23,8 +40,8 @@ import java.util.Optional; import java.util.concurrent.TimeUnit; -import lombok.extern.slf4j.Slf4j; import lombok.Getter; +import lombok.extern.slf4j.Slf4j; import org.jetbrains.annotations.NotNull; @@ -39,7 +56,9 @@ public abstract class FileTransferSession implements MessageListener { public interface FtpCallback { void onFtpProgress(double progressPct); + void onFtpComplete(FileTransferSession session); + void onFtpTimeout(String statusMsg, FileTransferSession session); } diff --git a/core/src/main/java/bisq/core/support/dispute/mediation/MediationManager.java b/core/src/main/java/bisq/core/support/dispute/mediation/MediationManager.java index 0fba279c209..26adfd38858 100644 --- a/core/src/main/java/bisq/core/support/dispute/mediation/MediationManager.java +++ b/core/src/main/java/bisq/core/support/dispute/mediation/MediationManager.java @@ -141,19 +141,17 @@ protected AckMessageSourceType getAckMessageSourceType() { @Override public void cleanupDisputes() { - disputeListService.cleanupDisputes(tradeId -> { - tradeManager.getTradeById(tradeId).filter(trade -> trade.getPayoutTx() != null) - .ifPresent(trade -> { - tradeManager.closeDisputedTrade(tradeId, Trade.DisputeState.MEDIATION_CLOSED); - }); - }); + disputeListService.cleanupDisputes(tradeId -> tradeManager.getTradeById(tradeId).filter(trade -> trade.getPayoutTx() != null) + .ifPresent(trade -> tradeManager.closeDisputedTrade(tradeId, Trade.DisputeState.MEDIATION_CLOSED))); } @Override protected String getDisputeInfo(Dispute dispute) { String role = Res.get("shared.mediator").toLowerCase(); + NodeAddress agentNodeAddress = getAgentNodeAddress(dispute); + checkNotNull(agentNodeAddress, "Agent node address must not be null"); String roleContextMsg = Res.get("support.initialMediatorMsg", - DisputeAgentLookupMap.getKeybaseLinkForAgent(getAgentNodeAddress(dispute).getFullAddress())); + DisputeAgentLookupMap.getKeybaseLinkForAgent(agentNodeAddress.getFullAddress())); String link = "https://bisq.wiki/Dispute_resolution#Level_2:_Mediation"; return Res.get("support.initialInfo", role, roleContextMsg, role, link); } @@ -181,7 +179,7 @@ public void onDisputeResultMessage(DisputeResultMessage disputeResultMessage) { checkNotNull(chatMessage, "chatMessage must not be null"); Optional disputeOptional = findDispute(disputeResult); String uid = disputeResultMessage.getUid(); - if (!disputeOptional.isPresent()) { + if (disputeOptional.isEmpty()) { log.warn("We got a dispute result msg but we don't have a matching dispute. " + "That might happen when we get the disputeResultMessage before the dispute was created. " + "We try again after 2 sec. to apply the disputeResultMessage. TradeId = " + tradeId); @@ -293,7 +291,7 @@ private void processFilePartReceived(FileTransferPart ftp) { } // we create a new session which is related to an open dispute from our list Optional dispute = findDispute(ftp.getTradeId(), ftp.getTraderId()); - if (!dispute.isPresent()) { + if (dispute.isEmpty()) { log.error("Received log upload request for unknown TradeId/TraderId {}/{}", ftp.getTradeId(), ftp.getTraderId()); return; } diff --git a/core/src/main/resources/i18n/displayStrings.properties b/core/src/main/resources/i18n/displayStrings.properties index 7d8ba931364..5153785e28f 100644 --- a/core/src/main/resources/i18n/displayStrings.properties +++ b/core/src/main/resources/i18n/displayStrings.properties @@ -1233,26 +1233,26 @@ support.state=State support.chat=Chat support.closed=Closed support.open=Open +support.sendLogFiles=Send Log Files support.process=Process support.buyerOfferer=BTC buyer/Maker support.sellerOfferer=BTC seller/Maker support.buyerTaker=BTC buyer/Taker support.sellerTaker=BTC seller/Taker -support.sendLogs.title=Send Logs -support.sendLogs.backgroundInfo=When traders experience a bug, mediators and support staff will often request copies of the users logs to diagnose the issue.\n\n\ - Upon pressing [Send], your log file will be compressed and transmitted direct to the mediator. -support.sendLogs.step1=Create Zip Archive of Logs +support.sendLogs.title=Send Log Files +support.sendLogs.backgroundInfo=When you experience a bug, mediators and support staff will often request copies of the your log files to diagnose the issue.\n\n\ \ + Upon pressing 'Send', your log files will be compressed and transmitted directly to the mediator. +support.sendLogs.step1=Create Zip Archive of Log Files support.sendLogs.step2=Connection Request to Mediator support.sendLogs.step3=Upload Archived Log Data -support.sendLogs.waiting=Waiting for your input support.sendLogs.send=Send -support.sendLogs.stop=Stop +support.sendLogs.cancel=Cancel support.sendLogs.init=Initializing support.sendLogs.retry=Retrying send support.sendLogs.stopped=Transfer stopped support.sendLogs.progress=Transfer progress: %.0f%% support.sendLogs.finished=Transfer complete! -support.sendLogs.command=Press [Send] to retry, or [Stop] to abort +support.sendLogs.command=Press 'Send' to retry, or 'Stop' to abort support.backgroundInfo=Bisq is not a company, so it handles disputes differently.\n\n\ Traders can communicate within the application via secure chat on the open trades screen to try solving disputes on their own. \ diff --git a/core/src/test/java/bisq/core/support/dispute/mediation/FileTransferSessionTest.java b/core/src/test/java/bisq/core/support/dispute/mediation/FileTransferSessionTest.java index 42fad3c4cbe..50c76c1e5f3 100644 --- a/core/src/test/java/bisq/core/support/dispute/mediation/FileTransferSessionTest.java +++ b/core/src/test/java/bisq/core/support/dispute/mediation/FileTransferSessionTest.java @@ -1,3 +1,20 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + package bisq.core.support.dispute.mediation; import bisq.network.p2p.FileTransferPart; @@ -14,7 +31,8 @@ import org.junit.Before; import org.junit.Test; -import static org.mockito.Mockito.*; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; public class FileTransferSessionTest implements FileTransferSession.FtpCallback { diff --git a/desktop/src/main/java/bisq/desktop/bisq.css b/desktop/src/main/java/bisq/desktop/bisq.css index 51862da5561..20a8b7f3a0b 100644 --- a/desktop/src/main/java/bisq/desktop/bisq.css +++ b/desktop/src/main/java/bisq/desktop/bisq.css @@ -2032,7 +2032,7 @@ textfield */ } .dispute-chat-border { - -fx-background-color: -bs-color-blue-5; + -fx-background-color: -bs-support-chat-background; } /******************************************************************************************************************** diff --git a/desktop/src/main/java/bisq/desktop/main/overlays/windows/SendLogFilesWindow.java b/desktop/src/main/java/bisq/desktop/main/overlays/windows/SendLogFilesWindow.java index e15be9ff2fc..393e05b31cf 100644 --- a/desktop/src/main/java/bisq/desktop/main/overlays/windows/SendLogFilesWindow.java +++ b/desktop/src/main/java/bisq/desktop/main/overlays/windows/SendLogFilesWindow.java @@ -23,6 +23,7 @@ import bisq.desktop.main.portfolio.pendingtrades.steps.buyer.BuyerStep1View; import bisq.desktop.main.portfolio.pendingtrades.steps.buyer.BuyerStep2View; import bisq.desktop.main.portfolio.pendingtrades.steps.buyer.BuyerStep3View; +import bisq.desktop.util.Layout; import bisq.core.locale.Res; import bisq.core.support.dispute.mediation.FileTransferSender; @@ -31,20 +32,22 @@ import bisq.common.UserThread; -import javafx.scene.layout.GridPane; -import javafx.scene.layout.HBox; +import com.jfoenix.controls.JFXProgressBar; + import javafx.scene.control.Button; -import javafx.scene.control.ProgressBar; import javafx.scene.control.Label; import javafx.scene.control.Separator; +import javafx.scene.layout.GridPane; +import javafx.scene.layout.HBox; import javafx.scene.layout.Region; import javafx.geometry.HPos; import javafx.geometry.Insets; import javafx.geometry.Orientation; import javafx.geometry.Pos; -import javafx.beans.property.SimpleDoubleProperty; + import javafx.beans.property.DoubleProperty; +import javafx.beans.property.SimpleDoubleProperty; import java.io.IOException; @@ -58,7 +61,6 @@ public class SendLogFilesWindow extends Overlay implements F private final String tradeId; private final int traderId; private final MediationManager mediationManager; - private ProgressBar progressBar; private Label statusLabel; private Button sendButton, stopButton; private final DoubleProperty ftpProgress = new SimpleDoubleProperty(-1); @@ -136,21 +138,25 @@ private void addContent() { addWizardsToGridPane(step3); addRegionToGridPane(); - progressBar = new ProgressBar(); + JFXProgressBar progressBar = new JFXProgressBar(); progressBar.setMinHeight(19); progressBar.setMaxHeight(19); progressBar.setPrefWidth(9305); + progressBar.setVisible(false); progressBar.progressProperty().bind(ftpProgress); gridPane.add(progressBar, 0, ++rowIndex); - statusLabel = addMultilineLabel(gridPane, ++rowIndex, Res.get("support.sendLogs.waiting")); + statusLabel = addMultilineLabel(gridPane, ++rowIndex, "", -Layout.FLOATING_LABEL_DISTANCE); + statusLabel.getStyleClass().add("sub-info"); addRegionToGridPane(); sendButton = new AutoTooltipButton(Res.get("support.sendLogs.send")); - stopButton = new AutoTooltipButton(Res.get("support.sendLogs.stop")); + stopButton = new AutoTooltipButton(Res.get("support.sendLogs.cancel")); + stopButton.setDisable(true); closeButton = new AutoTooltipButton(Res.get("shared.close")); sendButton.setOnAction(e -> { try { + progressBar.setVisible(true); if (fileTransferSender == null) { setActiveStep(1); statusLabel.setText(Res.get("support.sendLogs.init")); diff --git a/desktop/src/main/java/bisq/desktop/main/support/dispute/DisputeChatPopup.java b/desktop/src/main/java/bisq/desktop/main/support/dispute/DisputeChatPopup.java index 1ea9a5e160b..bbc6a0f1505 100644 --- a/desktop/src/main/java/bisq/desktop/main/support/dispute/DisputeChatPopup.java +++ b/desktop/src/main/java/bisq/desktop/main/support/dispute/DisputeChatPopup.java @@ -58,11 +58,9 @@ public interface ChatCallback { protected final DisputeManager> disputeManager; protected final CoinFormatter formatter; protected final Preferences preferences; - private ChatCallback chatCallback; + private final ChatCallback chatCallback; private double chatPopupStageXPosition = -1; private double chatPopupStageYPosition = -1; - private ChangeListener xPositionListener; - private ChangeListener yPositionListener; @Getter private Dispute selectedDispute; DisputeChatPopup(DisputeManager> disputeManager, @@ -111,7 +109,7 @@ public void openChat(Dispute selectedDispute, DisputeSession concreteDisputeSess closeDisputeButton.setOnAction(e -> chatCallback.onCloseDisputeFromChatWindow(selectedDispute)); chatView.display(concreteDisputeSession, closeDisputeButton, pane.widthProperty()); } else { - Button sendLogsButton = new AutoTooltipButton("Send Logs"); + Button sendLogsButton = new AutoTooltipButton(Res.get("support.sendLogFiles")); sendLogsButton.setOnAction(e -> chatCallback.onSendLogsFromChatWindow(selectedDispute)); chatView.display(concreteDisputeSession, sendLogsButton, pane.widthProperty()); } @@ -146,9 +144,9 @@ public void openChat(Dispute selectedDispute, DisputeSession concreteDisputeSess chatPopupStage.setOpacity(0); chatPopupStage.show(); - xPositionListener = (observable, oldValue, newValue) -> chatPopupStageXPosition = (double) newValue; + ChangeListener xPositionListener = (observable, oldValue, newValue) -> chatPopupStageXPosition = (double) newValue; chatPopupStage.xProperty().addListener(xPositionListener); - yPositionListener = (observable, oldValue, newValue) -> chatPopupStageYPosition = (double) newValue; + ChangeListener yPositionListener = (observable, oldValue, newValue) -> chatPopupStageYPosition = (double) newValue; chatPopupStage.yProperty().addListener(yPositionListener); if (chatPopupStageXPosition == -1) { diff --git a/desktop/src/main/java/bisq/desktop/theme-dark.css b/desktop/src/main/java/bisq/desktop/theme-dark.css index 642d99775b5..2529c9e6a1d 100644 --- a/desktop/src/main/java/bisq/desktop/theme-dark.css +++ b/desktop/src/main/java/bisq/desktop/theme-dark.css @@ -148,6 +148,8 @@ /* Monero orange color code */ -xmr-orange: #f26822; + + -bs-support-chat-background: #cccccc; } /* table view */ diff --git a/desktop/src/main/java/bisq/desktop/theme-light.css b/desktop/src/main/java/bisq/desktop/theme-light.css index 4688387ddf6..c23a6ef0a0f 100644 --- a/desktop/src/main/java/bisq/desktop/theme-light.css +++ b/desktop/src/main/java/bisq/desktop/theme-light.css @@ -115,6 +115,8 @@ /* Monero orange color code */ -xmr-orange: #f26822; + + -bs-support-chat-background: #4b4b4b; } .warning-box { diff --git a/p2p/src/main/java/bisq/network/p2p/FileTransferPart.java b/p2p/src/main/java/bisq/network/p2p/FileTransferPart.java index e3d822fea32..d63d069ecd2 100644 --- a/p2p/src/main/java/bisq/network/p2p/FileTransferPart.java +++ b/p2p/src/main/java/bisq/network/p2p/FileTransferPart.java @@ -27,8 +27,8 @@ @EqualsAndHashCode(callSuper = true) @Value -public final class FileTransferPart extends NetworkEnvelope implements ExtendedDataSizePermission, SendersNodeAddressMessage { - private final NodeAddress senderNodeAddress; +public class FileTransferPart extends NetworkEnvelope implements ExtendedDataSizePermission, SendersNodeAddressMessage { + NodeAddress senderNodeAddress; public String uid; public String tradeId; public int traderId; From d9ad531b3c7bd7550d8e5aeda8fd94470f2b47fc Mon Sep 17 00:00:00 2001 From: jmacxx <47253594+jmacxx@users.noreply.github.com> Date: Tue, 11 Jan 2022 09:23:49 -0600 Subject: [PATCH 4/4] Fix issue of space in file path --- .../core/support/dispute/mediation/FileTransferSender.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/core/src/main/java/bisq/core/support/dispute/mediation/FileTransferSender.java b/core/src/main/java/bisq/core/support/dispute/mediation/FileTransferSender.java index e7eecacaff0..3040285288f 100644 --- a/core/src/main/java/bisq/core/support/dispute/mediation/FileTransferSender.java +++ b/core/src/main/java/bisq/core/support/dispute/mediation/FileTransferSender.java @@ -66,7 +66,10 @@ public void createZipFileToSend() { try { Map env = new HashMap<>(); env.put("create", "true"); - FileSystem zipfs = FileSystems.newFileSystem(URI.create("jar:file:///" + (zipFilePath.replace('\\', '/'))), env); + URI uri = URI.create("jar:file:///" + zipFilePath + .replace('\\', '/') + .replaceAll(" ", "%20")); + FileSystem zipfs = FileSystems.newFileSystem(uri, env); Files.createDirectory(zipfs.getPath(zipId)); // store logfiles in a usefully-named subdir Stream paths = Files.walk(Paths.get(Config.appDataDir().toString()), 1); paths.filter(Files::isRegularFile).forEach(externalTxtFile -> { @@ -85,7 +88,7 @@ public void createZipFileToSend() { } }); zipfs.close(); - } catch (IOException ex) { + } catch (IOException | IllegalArgumentException ex) { log.error(ex.toString()); ex.printStackTrace(); }