From 856171acffec32e13f5211112d32b5a6ebcebd4f Mon Sep 17 00:00:00 2001 From: MrLetsplay Date: Sat, 11 May 2024 20:58:24 +0200 Subject: [PATCH] Fix sync issues (WIP) --- eclipse.target | 36 ++++++++ .../mrletsplay/shareclient/ShareClient.java | 91 +++++++++++-------- .../shareclient/util/ProjectAndPath.java | 5 + .../shareclient/util/ShareSession.java | 37 +++++++- .../shareclient/util/SharedProject.java | 27 ++++++ .../ShareClientDocumentListener.java | 11 ++- .../listeners/ShareClientPartListener.java | 23 ++++- .../shareclient/views/ShareView.java | 1 + 8 files changed, 184 insertions(+), 47 deletions(-) create mode 100644 eclipse.target create mode 100644 src/main/java/me/mrletsplay/shareclient/util/ProjectAndPath.java create mode 100644 src/main/java/me/mrletsplay/shareclient/util/SharedProject.java diff --git a/eclipse.target b/eclipse.target new file mode 100644 index 0000000..9b32648 --- /dev/null +++ b/eclipse.target @@ -0,0 +1,36 @@ + + + + + + + + + me.mrletsplay + ShareLib + 1.0-SNAPSHOT + jar + + + + + + + org.java-websocket + Java-WebSocket + 1.5.6 + jar + + + + + + x86_64 + linux + gtk + en_US + + + -Declipse.p2.max.threads=10 -Doomph.update.url=https://download.eclipse.org/oomph/updates/milestone/latest -Doomph.redirection.index.redirection=index:/->http://git.eclipse.org/c/oomph/org.eclipse.oomph.git/plain/setups/ -Dosgi.requiredJavaVersion=17 -Dosgi.instance.area.default=@user.home/eclipse-workspace -Dosgi.dataAreaRequiresExplicitInit=true -Dorg.eclipse.swt.graphics.Resource.reportNonDisposed=true -Dsun.java.command=Eclipse -XX:+UseG1GC -XX:+UseStringDeduplication --add-modules=ALL-SYSTEM -Dorg.eclipse.ecf.provider.filetransfer.excludeContributors=org.eclipse.ecf.provider.filetransfer.httpclientjava -Dosgi.requiredJavaVersion=17 -Dosgi.instance.area.default=@user.home/eclipse-workspace -Dosgi.dataAreaRequiresExplicitInit=true -Dorg.eclipse.swt.graphics.Resource.reportNonDisposed=true -Declipse.e4.inject.javax.warning=false -Dorg.slf4j.simpleLogger.defaultLogLevel=off -Dsun.java.command=Eclipse -Xms256m -Xmx2048m -XX:+UseG1GC -XX:+UseStringDeduplication --add-modules=ALL-SYSTEM -Djava.security.manager=allow + + \ No newline at end of file diff --git a/src/main/java/me/mrletsplay/shareclient/ShareClient.java b/src/main/java/me/mrletsplay/shareclient/ShareClient.java index cfb2d39..d3031de 100644 --- a/src/main/java/me/mrletsplay/shareclient/ShareClient.java +++ b/src/main/java/me/mrletsplay/shareclient/ShareClient.java @@ -30,6 +30,7 @@ import me.mrletsplay.shareclient.util.ChecksumUtil; import me.mrletsplay.shareclient.util.Peer; import me.mrletsplay.shareclient.util.ProjectRelativePath; import me.mrletsplay.shareclient.util.ShareSession; +import me.mrletsplay.shareclient.util.SharedProject; import me.mrletsplay.shareclient.util.listeners.ShareClientDocumentListener; import me.mrletsplay.shareclient.util.listeners.ShareClientPageListener; import me.mrletsplay.shareclient.util.listeners.ShareClientPartListener; @@ -37,6 +38,7 @@ import me.mrletsplay.shareclient.util.listeners.ShareClientWindowListener; import me.mrletsplay.shareclient.views.ShareView; import me.mrletsplay.shareclientcore.connection.Change; import me.mrletsplay.shareclientcore.connection.ConnectionException; +import me.mrletsplay.shareclientcore.connection.DisconnectListener; import me.mrletsplay.shareclientcore.connection.MessageListener; import me.mrletsplay.shareclientcore.connection.RemoteConnection; import me.mrletsplay.shareclientcore.connection.WebSocketConnection; @@ -53,7 +55,7 @@ import me.mrletsplay.shareclientcore.document.SharedDocument; /** * The activator class controls the plug-in life cycle */ -public class ShareClient extends AbstractUIPlugin implements MessageListener, IStartup { +public class ShareClient extends AbstractUIPlugin implements MessageListener, DisconnectListener, IStartup { // The plug-in ID public static final String PLUGIN_ID = "ShareClient"; //$NON-NLS-1$ @@ -132,6 +134,7 @@ public class ShareClient extends AbstractUIPlugin implements MessageListener, IS } connection.addListener(this); + connection.setDisconnectListener(this); updateView(); return activeSession = new ShareSession(connection, sessionID); @@ -180,7 +183,6 @@ public class ShareClient extends AbstractUIPlugin implements MessageListener, IS } if (message instanceof FullSyncMessage sync) { - // TODO: handle FULL_SYNC ProjectRelativePath path; try { path = ProjectRelativePath.of(sync.documentPath()); @@ -189,27 +191,36 @@ public class ShareClient extends AbstractUIPlugin implements MessageListener, IS return; } - ShareClientDocumentListener listener = partListener.getListeners().get(path); + ShareClientDocumentListener listener = partListener.getListener(path); IWorkspace workspace = ResourcesPlugin.getWorkspace(); IWorkspaceRoot workspaceRoot = workspace.getRoot(); - IProject project = workspaceRoot.getProject(path.projectName()); - if(project == null) return; - // TODO: make sure to not overwrite existing non-shared projects - if (!project.exists()) { - IProjectDescription description = workspace.newProjectDescription(path.projectName()); - try { - project.create(description, null); - project.open(null); - } catch (CoreException e) { - e.printStackTrace(); - MessageDialog.openError(null, "Share Client", "Failed to create project: " + e.toString()); - return; + SharedProject sharedProject = activeSession.getSharedProject(path.projectName()); + if(sharedProject == null) { + // New project, TODO: prompt user to choose location + IProject project = workspaceRoot.getProject(path.projectName()); + if(project == null) return; + + // TODO: make sure to not overwrite existing non-shared projects + if (!project.exists()) { + IProjectDescription description = workspace.newProjectDescription(path.projectName()); + try { + project.create(description, null); + project.open(null); + } catch (CoreException e) { + e.printStackTrace(); + MessageDialog.openError(null, "Share Client", "Failed to create project: " + e.toString()); + return; + } + System.out.println("Created project: " + project); } - System.out.println("Created project " + project); + + sharedProject = new SharedProject(path.projectName(), project); + activeSession.getSharedProjects().add(sharedProject); + System.out.println("Received new project: " + path.projectName()); } - Path filePath = project.getLocation().toPath().resolve(path.relativePath()); + Path filePath = sharedProject.getLocal().getLocation().toPath().resolve(path.relativePath()); try { if (!Files.exists(filePath)) { Files.createDirectories(filePath.getParent()); @@ -218,10 +229,10 @@ public class ShareClient extends AbstractUIPlugin implements MessageListener, IS // TODO: update sharedDocuments - listener.setIgnoreChanges(true); + if(listener != null) listener.setIgnoreChanges(true); Files.write(filePath, sync.content()); - project.refreshLocal(IResource.DEPTH_INFINITE, null); - listener.setIgnoreChanges(false); + sharedProject.getLocal().refreshLocal(IResource.DEPTH_INFINITE, null); + if(listener != null) listener.setIgnoreChanges(false); } catch (IOException | CoreException e) { e.printStackTrace(); MessageDialog.openError(null, "Share Client", "Failed to update file: " + e.toString()); @@ -233,7 +244,7 @@ public class ShareClient extends AbstractUIPlugin implements MessageListener, IS Map paths = new HashMap<>(); if (req.documentPath() == null) { // Sync entire (shared) workspace - for (IProject project : activeSession.getSharedProjects()) { + for (SharedProject project : activeSession.getSharedProjects()) { var files = getProjectFiles(project); System.out.println(files); if (files == null) return; @@ -247,18 +258,17 @@ public class ShareClient extends AbstractUIPlugin implements MessageListener, IS return; } - IProject project = ResourcesPlugin.getWorkspace().getRoot().getProject(path.projectName()); - if (project == null || !project.exists()) return; - if (!activeSession.getSharedProjects().contains(project)) return; + SharedProject sharedProject = activeSession.getSharedProject(path.projectName()); + if (sharedProject == null) return; if (!path.relativePath().isEmpty()) { - Path projectLocation = project.getLocation().toPath(); + Path projectLocation = sharedProject.getLocal().getLocation().toPath(); Path filePath = projectLocation.resolve(path.relativePath()).normalize(); if (!filePath.startsWith(projectLocation)) return; - paths.put(new ProjectRelativePath(project.getName(), path.relativePath()), filePath); + paths.put(new ProjectRelativePath(sharedProject.getRemoteName(), path.relativePath()), filePath); } else { // Sync entire project - var files = getProjectFiles(project); + var files = getProjectFiles(sharedProject); if (files == null) return; paths.putAll(files); } @@ -296,11 +306,21 @@ public class ShareClient extends AbstractUIPlugin implements MessageListener, IS }); } + @Override + public void onDisconnect(String reason, boolean remote) { + MessageDialog.openError(null, "Share Client", "Disconnected (remote = " + remote + "): " + reason); + } + + /** + * Shares a project. Includes sending full syncs to other clients + * @param project The project to share + */ public void addSharedProject(IProject project) { - activeSession.getSharedProjects().add(project); + SharedProject shared = new SharedProject(activeSession.getFreeRemoteName(project.getName()), project); + activeSession.getSharedProjects().add(shared); RemoteConnection connection = activeSession.getConnection(); - for(Map.Entry en : getProjectFiles(project).entrySet()) { + for(Map.Entry en : getProjectFiles(shared).entrySet()) { // TODO: add new document to sharedDocuments if(!sendFullSyncOrChecksum(connection, AddressableMessage.BROADCAST_SITE_ID, en.getKey(), en.getValue(), false)) return; } @@ -327,21 +347,20 @@ public class ShareClient extends AbstractUIPlugin implements MessageListener, IS } private Path resolvePath(ProjectRelativePath path) { - IProject project = ResourcesPlugin.getWorkspace().getRoot().getProject(path.projectName()); - if (project == null || !project.exists()) return null; - if (!activeSession.getSharedProjects().contains(project)) return null; - Path projectLocation = project.getLocation().toPath(); + SharedProject sharedProject = activeSession.getSharedProject(path.projectName()); + if(sharedProject == null) return null; + Path projectLocation = sharedProject.getLocal().getLocation().toPath(); Path filePath = projectLocation.resolve(path.relativePath()).normalize(); if (!filePath.startsWith(projectLocation)) return null; return filePath; } - private Map getProjectFiles(IProject project) { + private Map getProjectFiles(SharedProject project) { try { - Path projectLocation = project.getLocation().toPath(); + Path projectLocation = project.getLocal().getLocation().toPath(); return Files.walk(projectLocation) .collect(Collectors.toMap( - p -> new ProjectRelativePath(project.getName(), projectLocation.relativize(p).toString()), + p -> new ProjectRelativePath(project.getRemoteName(), projectLocation.relativize(p).toString()), Function.identity())); } catch (IOException e) { e.printStackTrace(); diff --git a/src/main/java/me/mrletsplay/shareclient/util/ProjectAndPath.java b/src/main/java/me/mrletsplay/shareclient/util/ProjectAndPath.java new file mode 100644 index 0000000..2233a46 --- /dev/null +++ b/src/main/java/me/mrletsplay/shareclient/util/ProjectAndPath.java @@ -0,0 +1,5 @@ +package me.mrletsplay.shareclient.util; + +import org.eclipse.core.resources.IProject; + +public record ProjectAndPath(IProject project, String path) {} \ No newline at end of file diff --git a/src/main/java/me/mrletsplay/shareclient/util/ShareSession.java b/src/main/java/me/mrletsplay/shareclient/util/ShareSession.java index e6dbca1..7ef0c7f 100644 --- a/src/main/java/me/mrletsplay/shareclient/util/ShareSession.java +++ b/src/main/java/me/mrletsplay/shareclient/util/ShareSession.java @@ -22,7 +22,7 @@ public class ShareSession { private RemoteConnection connection; private String sessionID; - private List sharedProjects; + private List sharedProjects; private Map sharedDocuments; private List peers; @@ -42,10 +42,38 @@ public class ShareSession { return sessionID; } - public List getSharedProjects() { + public List getSharedProjects() { return sharedProjects; } + public SharedProject getSharedProject(String remoteName) { + return sharedProjects.stream() + .filter(p -> p.getRemoteName().equals(remoteName)) + .findFirst().orElse(null); + } + + public SharedProject getSharedProject(IProject project) { + return sharedProjects.stream() + .filter(p -> p.getLocal().equals(project)) + .findFirst().orElse(null); + } + + /** + * @param projectName The name of the project + * @return A remote name for the project that is not currently in use + */ + public String getFreeRemoteName(String projectName) { + // TODO: technically, this can fail if two people try to share a project with the same name at the same time + if(getSharedProject(projectName) == null) return projectName; + + int n = 0; + while(getSharedProject(projectName + n) != null) { + n++; + } + + return projectName + n; + } + public Map getSharedDocuments() { return sharedDocuments; } @@ -63,8 +91,9 @@ public class ShareSession { @Override public void onInsert(int index, char character) { + // FIXME: potential desync. If changes arrive while waiting for asyncExec to execute, it will insert at the wrong position Display.getDefault().asyncExec(() -> { - ShareClientDocumentListener documentListener = ShareClient.getDefault().getPartListener().getListeners().get(path); + ShareClientDocumentListener documentListener = ShareClient.getDefault().getPartListener().getListener(path); if(documentListener != null) { IDocument document = documentListener.getDocument(); @@ -84,7 +113,7 @@ public class ShareSession { @Override public void onDelete(int index) { Display.getDefault().asyncExec(() -> { - ShareClientDocumentListener documentListener = ShareClient.getDefault().getPartListener().getListeners().get(path); + ShareClientDocumentListener documentListener = ShareClient.getDefault().getPartListener().getListener(path); if(documentListener != null) { IDocument document = documentListener.getDocument(); diff --git a/src/main/java/me/mrletsplay/shareclient/util/SharedProject.java b/src/main/java/me/mrletsplay/shareclient/util/SharedProject.java new file mode 100644 index 0000000..97c9591 --- /dev/null +++ b/src/main/java/me/mrletsplay/shareclient/util/SharedProject.java @@ -0,0 +1,27 @@ +package me.mrletsplay.shareclient.util; + +import org.eclipse.core.resources.IProject; + +/** + * A project shared in a session. Contains a remote name (name used for {@link ProjectRelativePath}s) and a reference to the local project. + */ +public class SharedProject { + + private String remoteName; + private IProject local; + + public SharedProject(String remoteName, IProject local) { + super(); + this.remoteName = remoteName; + this.local = local; + } + + public String getRemoteName() { + return remoteName; + } + + public IProject getLocal() { + return local; + } + +} diff --git a/src/main/java/me/mrletsplay/shareclient/util/listeners/ShareClientDocumentListener.java b/src/main/java/me/mrletsplay/shareclient/util/listeners/ShareClientDocumentListener.java index ccbd069..0b567af 100644 --- a/src/main/java/me/mrletsplay/shareclient/util/listeners/ShareClientDocumentListener.java +++ b/src/main/java/me/mrletsplay/shareclient/util/listeners/ShareClientDocumentListener.java @@ -6,18 +6,20 @@ import org.eclipse.jface.text.IDocumentListener; import org.eclipse.swt.widgets.Display; import me.mrletsplay.shareclient.ShareClient; +import me.mrletsplay.shareclient.util.ProjectAndPath; import me.mrletsplay.shareclient.util.ProjectRelativePath; import me.mrletsplay.shareclient.util.ShareSession; +import me.mrletsplay.shareclient.util.SharedProject; import me.mrletsplay.shareclientcore.document.SharedDocument; public class ShareClientDocumentListener implements IDocumentListener { - private ProjectRelativePath path; + private ProjectAndPath path; private IDocument document; private boolean ignoreChanges = false; - public ShareClientDocumentListener(ProjectRelativePath path, IDocument document) { + public ShareClientDocumentListener(ProjectAndPath path, IDocument document) { this.path = path; this.document = document; } @@ -40,7 +42,10 @@ public class ShareClientDocumentListener implements IDocumentListener { ShareSession session = ShareClient.getDefault().getActiveSession(); if(session == null) return; - SharedDocument doc = session.getOrCreateSharedDocument(path, () -> event.fDocument.get()); + SharedProject sharedProject = session.getSharedProject(path.project()); + if(sharedProject == null) return; + + SharedDocument doc = session.getOrCreateSharedDocument(new ProjectRelativePath(sharedProject.getRemoteName(), path.path()), () -> event.fDocument.get()); if(event.getLength() > 0) { doc.localDelete(event.getOffset(), event.getLength()); } diff --git a/src/main/java/me/mrletsplay/shareclient/util/listeners/ShareClientPartListener.java b/src/main/java/me/mrletsplay/shareclient/util/listeners/ShareClientPartListener.java index 0c76057..c77b4a1 100644 --- a/src/main/java/me/mrletsplay/shareclient/util/listeners/ShareClientPartListener.java +++ b/src/main/java/me/mrletsplay/shareclient/util/listeners/ShareClientPartListener.java @@ -15,13 +15,17 @@ import org.eclipse.ui.IWorkbenchPartReference; import org.eclipse.ui.part.FileEditorInput; import org.eclipse.ui.texteditor.ITextEditor; +import me.mrletsplay.shareclient.ShareClient; +import me.mrletsplay.shareclient.util.ProjectAndPath; import me.mrletsplay.shareclient.util.ProjectRelativePath; +import me.mrletsplay.shareclient.util.ShareSession; +import me.mrletsplay.shareclient.util.SharedProject; public class ShareClientPartListener implements IPartListener2 { - private Map listeners = new HashMap<>(); + private Map listeners = new HashMap<>(); - private ShareClientDocumentListener createListener(ProjectRelativePath path, IDocument document) { + private ShareClientDocumentListener createListener(ProjectAndPath path, IDocument document) { if(listeners.containsKey(path)) return listeners.get(path); ShareClientDocumentListener listener = new ShareClientDocumentListener(path, document); listeners.put(path, listener); @@ -43,7 +47,8 @@ public class ShareClientPartListener implements IPartListener2 { IProject project = file.getProject(); Path filePath = project.getLocation().toPath().relativize(file.getLocation().toPath()); - ProjectRelativePath relPath = new ProjectRelativePath(project.getName(), filePath.toString()); + // FIXME: this should probably just store an IProject with a relative path + ProjectAndPath relPath = new ProjectAndPath(project, filePath.toString()); System.out.println("Opened editor: " + relPath); document.addDocumentListener(createListener(relPath, document)); } @@ -58,8 +63,18 @@ public class ShareClientPartListener implements IPartListener2 { addDocumentListener(partRef); } - public Map getListeners() { + public Map getListeners() { return listeners; } + public ShareClientDocumentListener getListener(ProjectRelativePath path) { + ShareSession session = ShareClient.getDefault().getActiveSession(); + if(session == null) return null; + + SharedProject project = session.getSharedProject(path.projectName()); + if(project == null) return null; + + return listeners.get(new ProjectAndPath(project.getLocal(), path.relativePath())); + } + } diff --git a/src/main/java/me/mrletsplay/shareclient/views/ShareView.java b/src/main/java/me/mrletsplay/shareclient/views/ShareView.java index e1df2e8..5e884e5 100644 --- a/src/main/java/me/mrletsplay/shareclient/views/ShareView.java +++ b/src/main/java/me/mrletsplay/shareclient/views/ShareView.java @@ -133,6 +133,7 @@ public class ShareView extends ViewPart { public void run() { Clipboard clipboard = new Clipboard(Display.getDefault()); clipboard.setContents(new String[] {ShareClient.getDefault().getActiveSession().getSessionID()}, new Transfer[] {TextTransfer.getInstance()}); + showMessage("The session ID has been copied to the clipboard"); } };