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.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<ProjectRelativePath, Path> 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<ProjectRelativePath, Path> en : getProjectFiles(project).entrySet()) {
for(Map.Entry<ProjectRelativePath, Path> 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<ProjectRelativePath, Path> getProjectFiles(IProject project) {
private Map<ProjectRelativePath, Path> 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();

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 String sessionID;
private List<IProject> sharedProjects;
private List<SharedProject> sharedProjects;
private Map<ProjectRelativePath, SharedDocument> sharedDocuments;
private List<Peer> peers;
@ -42,10 +42,38 @@ public class ShareSession {
return sessionID;
}
public List<IProject> getSharedProjects() {
public List<SharedProject> 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<ProjectRelativePath, SharedDocument> 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();

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 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());
}

View File

@ -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<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);
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<ProjectRelativePath, ShareClientDocumentListener> getListeners() {
public Map<ProjectAndPath, ShareClientDocumentListener> 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()));
}
}

View File

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