Fix sync issues (WIP)

This commit is contained in:
MrLetsplay 2024-05-11 20:58:24 +02:00
parent 1a120f98eb
commit 856171acff
Signed by: mr
SSH Key Fingerprint: SHA256:92jBH80vpXyaZHjaIl47pjRq+Yt7XGTArqQg1V7hSqg
8 changed files with 184 additions and 47 deletions

36
eclipse.target Normal file
View File

@ -0,0 +1,36 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<?pde version="3.8"?>
<target name="Running Platform">
<locations>
<location path="${eclipse_home}" type="Profile"/>
<location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="generate" type="Maven">
<dependencies>
<dependency>
<groupId>me.mrletsplay</groupId>
<artifactId>ShareLib</artifactId>
<version>1.0-SNAPSHOT</version>
<type>jar</type>
</dependency>
</dependencies>
</location>
<location includeDependencyDepth="none" includeDependencyScopes="compile" includeSource="true" missingManifest="generate" type="Maven">
<dependencies>
<dependency>
<groupId>org.java-websocket</groupId>
<artifactId>Java-WebSocket</artifactId>
<version>1.5.6</version>
<type>jar</type>
</dependency>
</dependencies>
</location>
</locations>
<environment>
<arch>x86_64</arch>
<os>linux</os>
<ws>gtk</ws>
<nl>en_US</nl>
</environment>
<launcherArgs>
<vmArgs>-Declipse.p2.max.threads=10 -Doomph.update.url=https://download.eclipse.org/oomph/updates/milestone/latest -Doomph.redirection.index.redirection=index:/-&gt;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</vmArgs>
</launcherArgs>
</target>

View File

@ -30,6 +30,7 @@ import me.mrletsplay.shareclient.util.ChecksumUtil;
import me.mrletsplay.shareclient.util.Peer; import me.mrletsplay.shareclient.util.Peer;
import me.mrletsplay.shareclient.util.ProjectRelativePath; import me.mrletsplay.shareclient.util.ProjectRelativePath;
import me.mrletsplay.shareclient.util.ShareSession; 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.ShareClientDocumentListener;
import me.mrletsplay.shareclient.util.listeners.ShareClientPageListener; import me.mrletsplay.shareclient.util.listeners.ShareClientPageListener;
import me.mrletsplay.shareclient.util.listeners.ShareClientPartListener; 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.shareclient.views.ShareView;
import me.mrletsplay.shareclientcore.connection.Change; import me.mrletsplay.shareclientcore.connection.Change;
import me.mrletsplay.shareclientcore.connection.ConnectionException; import me.mrletsplay.shareclientcore.connection.ConnectionException;
import me.mrletsplay.shareclientcore.connection.DisconnectListener;
import me.mrletsplay.shareclientcore.connection.MessageListener; import me.mrletsplay.shareclientcore.connection.MessageListener;
import me.mrletsplay.shareclientcore.connection.RemoteConnection; import me.mrletsplay.shareclientcore.connection.RemoteConnection;
import me.mrletsplay.shareclientcore.connection.WebSocketConnection; 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 * 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 // The plug-in ID
public static final String PLUGIN_ID = "ShareClient"; //$NON-NLS-1$ 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.addListener(this);
connection.setDisconnectListener(this);
updateView(); updateView();
return activeSession = new ShareSession(connection, sessionID); return activeSession = new ShareSession(connection, sessionID);
@ -180,7 +183,6 @@ public class ShareClient extends AbstractUIPlugin implements MessageListener, IS
} }
if (message instanceof FullSyncMessage sync) { if (message instanceof FullSyncMessage sync) {
// TODO: handle FULL_SYNC
ProjectRelativePath path; ProjectRelativePath path;
try { try {
path = ProjectRelativePath.of(sync.documentPath()); path = ProjectRelativePath.of(sync.documentPath());
@ -189,27 +191,36 @@ public class ShareClient extends AbstractUIPlugin implements MessageListener, IS
return; return;
} }
ShareClientDocumentListener listener = partListener.getListeners().get(path); ShareClientDocumentListener listener = partListener.getListener(path);
IWorkspace workspace = ResourcesPlugin.getWorkspace(); IWorkspace workspace = ResourcesPlugin.getWorkspace();
IWorkspaceRoot workspaceRoot = workspace.getRoot(); IWorkspaceRoot workspaceRoot = workspace.getRoot();
IProject project = workspaceRoot.getProject(path.projectName()); SharedProject sharedProject = activeSession.getSharedProject(path.projectName());
if(project == null) return; if(sharedProject == null) {
// TODO: make sure to not overwrite existing non-shared projects // New project, TODO: prompt user to choose location
if (!project.exists()) { IProject project = workspaceRoot.getProject(path.projectName());
IProjectDescription description = workspace.newProjectDescription(path.projectName()); if(project == null) return;
try {
project.create(description, null); // TODO: make sure to not overwrite existing non-shared projects
project.open(null); if (!project.exists()) {
} catch (CoreException e) { IProjectDescription description = workspace.newProjectDescription(path.projectName());
e.printStackTrace(); try {
MessageDialog.openError(null, "Share Client", "Failed to create project: " + e.toString()); project.create(description, null);
return; 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 { try {
if (!Files.exists(filePath)) { if (!Files.exists(filePath)) {
Files.createDirectories(filePath.getParent()); Files.createDirectories(filePath.getParent());
@ -218,10 +229,10 @@ public class ShareClient extends AbstractUIPlugin implements MessageListener, IS
// TODO: update sharedDocuments // TODO: update sharedDocuments
listener.setIgnoreChanges(true); if(listener != null) listener.setIgnoreChanges(true);
Files.write(filePath, sync.content()); Files.write(filePath, sync.content());
project.refreshLocal(IResource.DEPTH_INFINITE, null); sharedProject.getLocal().refreshLocal(IResource.DEPTH_INFINITE, null);
listener.setIgnoreChanges(false); if(listener != null) listener.setIgnoreChanges(false);
} catch (IOException | CoreException e) { } catch (IOException | CoreException e) {
e.printStackTrace(); e.printStackTrace();
MessageDialog.openError(null, "Share Client", "Failed to update file: " + e.toString()); MessageDialog.openError(null, "Share Client", "Failed to update file: " + e.toString());
@ -233,7 +244,7 @@ public class ShareClient extends AbstractUIPlugin implements MessageListener, IS
Map<ProjectRelativePath, Path> paths = new HashMap<>(); Map<ProjectRelativePath, Path> paths = new HashMap<>();
if (req.documentPath() == null) { if (req.documentPath() == null) {
// Sync entire (shared) workspace // Sync entire (shared) workspace
for (IProject project : activeSession.getSharedProjects()) { for (SharedProject project : activeSession.getSharedProjects()) {
var files = getProjectFiles(project); var files = getProjectFiles(project);
System.out.println(files); System.out.println(files);
if (files == null) return; if (files == null) return;
@ -247,18 +258,17 @@ public class ShareClient extends AbstractUIPlugin implements MessageListener, IS
return; return;
} }
IProject project = ResourcesPlugin.getWorkspace().getRoot().getProject(path.projectName()); SharedProject sharedProject = activeSession.getSharedProject(path.projectName());
if (project == null || !project.exists()) return; if (sharedProject == null) return;
if (!activeSession.getSharedProjects().contains(project)) return;
if (!path.relativePath().isEmpty()) { if (!path.relativePath().isEmpty()) {
Path projectLocation = project.getLocation().toPath(); Path projectLocation = sharedProject.getLocal().getLocation().toPath();
Path filePath = projectLocation.resolve(path.relativePath()).normalize(); Path filePath = projectLocation.resolve(path.relativePath()).normalize();
if (!filePath.startsWith(projectLocation)) return; if (!filePath.startsWith(projectLocation)) return;
paths.put(new ProjectRelativePath(project.getName(), path.relativePath()), filePath); paths.put(new ProjectRelativePath(sharedProject.getRemoteName(), path.relativePath()), filePath);
} else { } else {
// Sync entire project // Sync entire project
var files = getProjectFiles(project); var files = getProjectFiles(sharedProject);
if (files == null) return; if (files == null) return;
paths.putAll(files); 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) { 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(); RemoteConnection connection = activeSession.getConnection();
for(Map.Entry<ProjectRelativePath, Path> en : getProjectFiles(project).entrySet()) { for(Map.Entry<ProjectRelativePath, Path> en : getProjectFiles(shared).entrySet()) {
// TODO: add new document to sharedDocuments // TODO: add new document to sharedDocuments
if(!sendFullSyncOrChecksum(connection, AddressableMessage.BROADCAST_SITE_ID, en.getKey(), en.getValue(), false)) return; 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) { private Path resolvePath(ProjectRelativePath path) {
IProject project = ResourcesPlugin.getWorkspace().getRoot().getProject(path.projectName()); SharedProject sharedProject = activeSession.getSharedProject(path.projectName());
if (project == null || !project.exists()) return null; if(sharedProject == null) return null;
if (!activeSession.getSharedProjects().contains(project)) return null; Path projectLocation = sharedProject.getLocal().getLocation().toPath();
Path projectLocation = project.getLocation().toPath();
Path filePath = projectLocation.resolve(path.relativePath()).normalize(); Path filePath = projectLocation.resolve(path.relativePath()).normalize();
if (!filePath.startsWith(projectLocation)) return null; if (!filePath.startsWith(projectLocation)) return null;
return filePath; return filePath;
} }
private Map<ProjectRelativePath, Path> getProjectFiles(IProject project) { private Map<ProjectRelativePath, Path> getProjectFiles(SharedProject project) {
try { try {
Path projectLocation = project.getLocation().toPath(); Path projectLocation = project.getLocal().getLocation().toPath();
return Files.walk(projectLocation) return Files.walk(projectLocation)
.collect(Collectors.toMap( .collect(Collectors.toMap(
p -> new ProjectRelativePath(project.getName(), projectLocation.relativize(p).toString()), p -> new ProjectRelativePath(project.getRemoteName(), projectLocation.relativize(p).toString()),
Function.identity())); Function.identity()));
} catch (IOException e) { } catch (IOException e) {
e.printStackTrace(); e.printStackTrace();

View File

@ -0,0 +1,5 @@
package me.mrletsplay.shareclient.util;
import org.eclipse.core.resources.IProject;
public record ProjectAndPath(IProject project, String path) {}

View File

@ -22,7 +22,7 @@ public class ShareSession {
private RemoteConnection connection; private RemoteConnection connection;
private String sessionID; private String sessionID;
private List<IProject> sharedProjects; private List<SharedProject> sharedProjects;
private Map<ProjectRelativePath, SharedDocument> sharedDocuments; private Map<ProjectRelativePath, SharedDocument> sharedDocuments;
private List<Peer> peers; private List<Peer> peers;
@ -42,10 +42,38 @@ public class ShareSession {
return sessionID; return sessionID;
} }
public List<IProject> getSharedProjects() { public List<SharedProject> getSharedProjects() {
return sharedProjects; 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<ProjectRelativePath, SharedDocument> getSharedDocuments() { public Map<ProjectRelativePath, SharedDocument> getSharedDocuments() {
return sharedDocuments; return sharedDocuments;
} }
@ -63,8 +91,9 @@ public class ShareSession {
@Override @Override
public void onInsert(int index, char character) { 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(() -> { Display.getDefault().asyncExec(() -> {
ShareClientDocumentListener documentListener = ShareClient.getDefault().getPartListener().getListeners().get(path); ShareClientDocumentListener documentListener = ShareClient.getDefault().getPartListener().getListener(path);
if(documentListener != null) { if(documentListener != null) {
IDocument document = documentListener.getDocument(); IDocument document = documentListener.getDocument();
@ -84,7 +113,7 @@ public class ShareSession {
@Override @Override
public void onDelete(int index) { public void onDelete(int index) {
Display.getDefault().asyncExec(() -> { Display.getDefault().asyncExec(() -> {
ShareClientDocumentListener documentListener = ShareClient.getDefault().getPartListener().getListeners().get(path); ShareClientDocumentListener documentListener = ShareClient.getDefault().getPartListener().getListener(path);
if(documentListener != null) { if(documentListener != null) {
IDocument document = documentListener.getDocument(); IDocument document = documentListener.getDocument();

View File

@ -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;
}
}

View File

@ -6,18 +6,20 @@ import org.eclipse.jface.text.IDocumentListener;
import org.eclipse.swt.widgets.Display; import org.eclipse.swt.widgets.Display;
import me.mrletsplay.shareclient.ShareClient; import me.mrletsplay.shareclient.ShareClient;
import me.mrletsplay.shareclient.util.ProjectAndPath;
import me.mrletsplay.shareclient.util.ProjectRelativePath; import me.mrletsplay.shareclient.util.ProjectRelativePath;
import me.mrletsplay.shareclient.util.ShareSession; import me.mrletsplay.shareclient.util.ShareSession;
import me.mrletsplay.shareclient.util.SharedProject;
import me.mrletsplay.shareclientcore.document.SharedDocument; import me.mrletsplay.shareclientcore.document.SharedDocument;
public class ShareClientDocumentListener implements IDocumentListener { public class ShareClientDocumentListener implements IDocumentListener {
private ProjectRelativePath path; private ProjectAndPath path;
private IDocument document; private IDocument document;
private boolean ignoreChanges = false; private boolean ignoreChanges = false;
public ShareClientDocumentListener(ProjectRelativePath path, IDocument document) { public ShareClientDocumentListener(ProjectAndPath path, IDocument document) {
this.path = path; this.path = path;
this.document = document; this.document = document;
} }
@ -40,7 +42,10 @@ public class ShareClientDocumentListener implements IDocumentListener {
ShareSession session = ShareClient.getDefault().getActiveSession(); ShareSession session = ShareClient.getDefault().getActiveSession();
if(session == null) return; 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) { if(event.getLength() > 0) {
doc.localDelete(event.getOffset(), event.getLength()); doc.localDelete(event.getOffset(), event.getLength());
} }

View File

@ -15,13 +15,17 @@ import org.eclipse.ui.IWorkbenchPartReference;
import org.eclipse.ui.part.FileEditorInput; import org.eclipse.ui.part.FileEditorInput;
import org.eclipse.ui.texteditor.ITextEditor; 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.ProjectRelativePath;
import me.mrletsplay.shareclient.util.ShareSession;
import me.mrletsplay.shareclient.util.SharedProject;
public class ShareClientPartListener implements IPartListener2 { public class ShareClientPartListener implements IPartListener2 {
private Map<ProjectRelativePath, ShareClientDocumentListener> listeners = new HashMap<>(); private Map<ProjectAndPath, ShareClientDocumentListener> 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); if(listeners.containsKey(path)) return listeners.get(path);
ShareClientDocumentListener listener = new ShareClientDocumentListener(path, document); ShareClientDocumentListener listener = new ShareClientDocumentListener(path, document);
listeners.put(path, listener); listeners.put(path, listener);
@ -43,7 +47,8 @@ public class ShareClientPartListener implements IPartListener2 {
IProject project = file.getProject(); IProject project = file.getProject();
Path filePath = project.getLocation().toPath().relativize(file.getLocation().toPath()); 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); System.out.println("Opened editor: " + relPath);
document.addDocumentListener(createListener(relPath, document)); document.addDocumentListener(createListener(relPath, document));
} }
@ -58,8 +63,18 @@ public class ShareClientPartListener implements IPartListener2 {
addDocumentListener(partRef); addDocumentListener(partRef);
} }
public Map<ProjectRelativePath, ShareClientDocumentListener> getListeners() { public Map<ProjectAndPath, ShareClientDocumentListener> getListeners() {
return listeners; 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()));
}
} }

View File

@ -133,6 +133,7 @@ public class ShareView extends ViewPart {
public void run() { public void run() {
Clipboard clipboard = new Clipboard(Display.getDefault()); Clipboard clipboard = new Clipboard(Display.getDefault());
clipboard.setContents(new String[] {ShareClient.getDefault().getActiveSession().getSessionID()}, new Transfer[] {TextTransfer.getInstance()}); clipboard.setContents(new String[] {ShareClient.getDefault().getActiveSession().getSessionID()}, new Transfer[] {TextTransfer.getInstance()});
showMessage("The session ID has been copied to the clipboard");
} }
}; };