GP-5333 Added repo connection status/action to root project data tree node

This commit is contained in:
ghidra1
2025-09-29 15:34:08 -04:00
parent 2b5ba24327
commit 367e71f707
10 changed files with 210 additions and 31 deletions
Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 20 KiB

@@ -40,27 +40,38 @@
<H2><A name="ConnectToServer"></A>Connect to the Server</H2> <H2><A name="ConnectToServer"></A>Connect to the Server</H2>
<BLOCKQUOTE> <BLOCKQUOTE>
<P>When you open a shared project, Ghidra attempts to connect to the server that is <P>When you open or view a shared project, Ghidra attempts to connect to the corresponding server.
associated with the shared project. Depending on what user authentication mode the server is Depending on which user authentication mode the server is using, you may have to enter a password.
using, you may have to enter a password. If the server is not running, you are still able to If you choose not to connect or lose the connection to the repository server after opening
work with your checked out files while you are offline. Other versioned files not checked out or viewing a project, shared files not
are not accessible. When the server comes up, Ghidra will reconnect as necessary. You can checked out will not be shown within the Ghidra Project Window, as they are unavailable for use.
also attempt to connect "manually" by selecting the connection status button <IMG alt="" src= Local project private files are not affected by the repository server connection and will always
"images/disconnected.gif" border="0"> on the Ghidra Project Window or on the <I><A href= be shown. If you subsequently connect to the repository server, Ghidra will refresh
"help/topics/FrontEndPlugin/Project_Info.htm">Project Info</A></I> dialog. When the the project views to reflect the current state.&nbsp;</P>
connection is successful, the connection status button changes to <IMG alt="" src=
"images/connected.gif" border="0">.&nbsp;</P>
<P>If you lose the connection to the server after having started Ghidra, shared files not
checked out "disappear" from the Ghidra Project Window, as they are unavailable. Private
files remain intact and are not affected by the server connection.</P>
<BLOCKQUOTE> <BLOCKQUOTE>
<P> <P>
<IMG SRC="help/shared/tip.png" /> <IMG SRC="help/shared/tip.png" />
You are authenticated only once per The root folder node of the Project Data Tree view of a shared project will convey the
Ghidra session; so if you open other project repositories managed by the same Ghidra Server, current connection status with green (connected) or red (disconnected) indicator.&nbsp;</P>
you will be prompted only once for a password, as required.&nbsp;</P> </BLOCKQUOTE>
<P>If applicable, and
not currently connected to the shared repository server, a manual connection may be re-attempted by
clicking the <B>Connect Shared Repository</B> popup action on the root node of a shared project.
For the active project there is also a Connect status button in the lower-right corner of the
project window. When this button shows the disconnected state
<IMG alt="" src="images/disconnected.gif" border="0"> it may be clicked to attempt a connection.
This may also be done from the <I><A href="help/topics/FrontEndPlugin/Project_Info.htm">Project
Info</A></I> dialog. When the active project repository connection is successful, the connection
status button changes to <IMG alt="" src="images/connected.gif" border="0">.&nbsp;</P>
<BLOCKQUOTE>
<P>
<IMG SRC="help/shared/tip.png" />
Successfully connecting to a Ghidra Server which corresponds to multiple named repositories will cause
all associated viewed projects within Ghidra to become connected or automatically connect
when subsequently opened.&nbsp;</P>
</BLOCKQUOTE> </BLOCKQUOTE>
<H3><A name="Troubleshooting"></A>Troubleshooting a Failed Connection</H3> <H3><A name="Troubleshooting"></A>Troubleshooting a Failed Connection</H3>
@@ -26,6 +26,7 @@ src/main/resources/images/checkNotLatest.gif||GHIDRA||reviewed||END|
src/main/resources/images/checkex.png||GHIDRA||reviewed||END| src/main/resources/images/checkex.png||GHIDRA||reviewed||END|
src/main/resources/images/connected.gif||GHIDRA||reviewed||END| src/main/resources/images/connected.gif||GHIDRA||reviewed||END|
src/main/resources/images/disconnected.gif||GHIDRA||reviewed||END| src/main/resources/images/disconnected.gif||GHIDRA||reviewed||END|
src/main/resources/images/green_can.png||GHIDRA||||END|
src/main/resources/images/link.png||Crystal Clear Icons - LGPL 2.1||||END| src/main/resources/images/link.png||Crystal Clear Icons - LGPL 2.1||||END|
src/main/resources/images/lock.png||FAMFAMFAM Icons - CC 2.5|||famfamfam silk icon set|END| src/main/resources/images/lock.png||FAMFAMFAM Icons - CC 2.5|||famfamfam silk icon set|END|
src/main/resources/images/monitor.png||FAMFAMFAM Icons - CC 2.5|||silk|END| src/main/resources/images/monitor.png||FAMFAMFAM Icons - CC 2.5|||silk|END|
@@ -34,6 +35,7 @@ src/main/resources/images/page_delete.png||FAMFAMFAM Icons - CC 2.5||||END|
src/main/resources/images/page_edit.png||FAMFAMFAM Icons - CC 2.5||||END| src/main/resources/images/page_edit.png||FAMFAMFAM Icons - CC 2.5||||END|
src/main/resources/images/plasma.png||Oxygen Icons - LGPL 3.0|||Oxygen icon theme (dual license; LGPL or CC-SA-3.0)|END| src/main/resources/images/plasma.png||Oxygen Icons - LGPL 3.0|||Oxygen icon theme (dual license; LGPL or CC-SA-3.0)|END|
src/main/resources/images/plugin.png||GHIDRA||reviewed||END| src/main/resources/images/plugin.png||GHIDRA||reviewed||END|
src/main/resources/images/red_can.png||GHIDRA||||END|
src/main/resources/images/small_hijack.gif||GHIDRA||reviewed||END| src/main/resources/images/small_hijack.gif||GHIDRA||reviewed||END|
src/main/resources/images/undo_hijack.png||GHIDRA||reviewed||END| src/main/resources/images/undo_hijack.png||GHIDRA||reviewed||END|
src/main/resources/images/unknownFile.gif||GHIDRA||reviewed||END| src/main/resources/images/unknownFile.gif||GHIDRA||reviewed||END|
@@ -9,6 +9,12 @@ icon.project.data.file.ghidra.hijacked = small_hijack.gif
icon.project.data.file.ghidra.read.only = user-busy.png [size(8,8)] icon.project.data.file.ghidra.read.only = user-busy.png [size(8,8)]
icon.project.data.file.ghidra.not.latest = checkNotLatest.gif icon.project.data.file.ghidra.not.latest = checkNotLatest.gif
icon.project.root.repo.connected = green_can.png
icon.project.root.repo.connected.overlay = EMPTY_ICON[size(18,16)]{icon.project.root.repo.connected[move(12,8)]} // lower-left of 16x16 icon
icon.project.root.repo.disconnected = red_can.png
icon.project.root.repo.disconnected.overlay = EMPTY_ICON[size(18,16)]{icon.project.root.repo.disconnected[move(12,8)]} // lower-left of 16x16 icon
icon.content.handler.link = link.png icon.content.handler.link = link.png
icon.content.handler.link.overlay = EMPTY_ICON[size(16,16)]{icon.content.handler.link[move(0,8)]} // lower-left of 16x16 icon icon.content.handler.link.overlay = EMPTY_ICON[size(16,16)]{icon.content.handler.link[move(0,8)]} // lower-left of 16x16 icon
@@ -129,6 +129,7 @@ public class FrontEndPlugin extends Plugin
private FrontEndProvider frontEndProvider; private FrontEndProvider frontEndProvider;
private ProjectRepoConnectAction repoConnectAction;
private ProjectDataCutAction cutAction; private ProjectDataCutAction cutAction;
private ClearCutAction clearCutAction; private ClearCutAction clearCutAction;
private ProjectDataCopyAction copyAction; private ProjectDataCopyAction copyAction;
@@ -221,6 +222,7 @@ public class FrontEndPlugin extends Plugin
String owner = getName(); String owner = getName();
// Top of popup menu actions - no group // Top of popup menu actions - no group
repoConnectAction = new ProjectRepoConnectAction(this, null);
openAction = new ProjectDataOpenDefaultToolAction(owner, null); openAction = new ProjectDataOpenDefaultToolAction(owner, null);
followLinkAction = new ProjectDataFollowLinkAction(this, null); followLinkAction = new ProjectDataFollowLinkAction(this, null);
selectRealFileOrFolderAction = new ProjectDataSelectRealFileOrFolderAction(this, null); selectRealFileOrFolderAction = new ProjectDataSelectRealFileOrFolderAction(this, null);
@@ -251,6 +253,7 @@ public class FrontEndPlugin extends Plugin
groupName = "XRefresh"; groupName = "XRefresh";
refreshAction = new ProjectDataRefreshAction(owner, groupName); refreshAction = new ProjectDataRefreshAction(owner, groupName);
tool.addAction(repoConnectAction);
tool.addAction(newFolderAction); tool.addAction(newFolderAction);
tool.addAction(cutAction); tool.addAction(cutAction);
tool.addAction(clearCutAction); tool.addAction(clearCutAction);
@@ -0,0 +1,93 @@
/* ###
* IP: GHIDRA
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ghidra.framework.main;
import java.io.IOException;
import javax.swing.Icon;
import docking.action.MenuData;
import generic.theme.GIcon;
import ghidra.framework.client.*;
import ghidra.framework.main.datatable.FrontendProjectTreeAction;
import ghidra.framework.main.datatable.ProjectDataContext;
import ghidra.framework.main.datatree.DataTree;
import ghidra.framework.model.DomainFolder;
import ghidra.framework.model.ProjectData;
import ghidra.util.HelpLocation;
/**
* {@link ProjectRepoConnectAction} action allows the user to initiate a shared repository
* connection for a root shared project data tree node that is not currently connected.
*/
public class ProjectRepoConnectAction extends FrontendProjectTreeAction {
private static final Icon CONNECT_ICON = new GIcon("icon.frontend.project.connected");
private FrontEndPlugin plugin;
public ProjectRepoConnectAction(FrontEndPlugin plugin, String group) {
super("Connect Shared Repository", plugin.getName());
this.plugin = plugin;
setPopupMenuData(
new MenuData(new String[] { "Connect Shared Repository" }, CONNECT_ICON, group));
setHelpLocation(new HelpLocation("VersionControl", "Connect_Shared_Repository"));
}
@Override
protected void actionPerformed(ProjectDataContext context) {
RepositoryAdapter repository = getDisconnectedRepository(context);
if (repository != null) {
try {
repository.connect();
}
catch (NotConnectedException e) {
// don't think this can happen
}
catch (IOException e) {
ClientUtil.handleException(repository, e, "Repository Connection",
plugin.getTool().getToolFrame());
}
}
}
@Override
protected boolean isEnabledForContext(ProjectDataContext context) {
return getDisconnectedRepository(context) != null;
}
private RepositoryAdapter getDisconnectedRepository(ProjectDataContext context) {
if (!(context.getComponent() instanceof DataTree)) {
return null;
}
if (context.getFolderCount() != 1 || context.getFileCount() != 0) {
return null;
}
DomainFolder domainFolder = context.getSelectedFolders().get(0);
if (domainFolder.getParent() != null) {
return null;
}
ProjectData projectData = domainFolder.getProjectData();
if (projectData.getProjectLocator().isTransient()) {
return null; // Transient projects are always connected
}
RepositoryAdapter repository = projectData.getRepository();
if (repository != null && !repository.isConnected()) {
return repository;
}
return null;
}
}
@@ -21,23 +21,81 @@ import javax.swing.Icon;
import docking.tool.ToolConstants; import docking.tool.ToolConstants;
import generic.theme.GIcon; import generic.theme.GIcon;
import ghidra.framework.client.RemoteAdapterListener;
import ghidra.framework.client.RepositoryAdapter; import ghidra.framework.client.RepositoryAdapter;
import ghidra.framework.model.*; import ghidra.framework.model.*;
import resources.MultiIcon;
public class DomainFolderRootNode extends DomainFolderNode implements RemoteAdapterListener {
public class DomainFolderRootNode extends DomainFolderNode {
private static final Icon CLOSED_PROJECT = new GIcon("icon.datatree.node.domain.folder.closed"); private static final Icon CLOSED_PROJECT = new GIcon("icon.datatree.node.domain.folder.closed");
private static final Icon OPEN_PROJECT = new GIcon("icon.datatree.node.domain.folder.open"); private static final Icon OPEN_PROJECT = new GIcon("icon.datatree.node.domain.folder.open");
private static final Icon CONNECTED_OVERLAY =
new GIcon("icon.project.root.repo.connected.overlay");
private static final Icon DISCONNECTED_OVERLAY =
new GIcon("icon.project.root.repo.disconnected.overlay");
private static enum Status {
OPEN(true),
CLOSED(false),
OPEN_CONNECTED(true, true),
CLOSED_CONNECTED(false, true),
OPEN_DISCONNECTED(true, false),
CLOSED_DISCONNECTED(false, false);
final Icon icon;
private Status(boolean isOpen) {
icon = isOpen ? OPEN_PROJECT : CLOSED_PROJECT;
}
private Status(boolean isOpen, boolean isConnected) {
MultiIcon multiIcon = new MultiIcon(isOpen ? OPEN_PROJECT : CLOSED_PROJECT);
multiIcon.addIcon(isConnected ? CONNECTED_OVERLAY : DISCONNECTED_OVERLAY);
icon = multiIcon;
}
static Status getStatus(boolean isOpen, RepositoryAdapter repository) {
if (isOpen) {
if (repository == null) {
return OPEN;
}
return repository.isConnected() ? OPEN_CONNECTED : OPEN_DISCONNECTED;
}
if (repository == null) {
return CLOSED;
}
return repository.isConnected() ? CLOSED_CONNECTED : CLOSED_DISCONNECTED;
}
}
private String projectName; private String projectName;
private RepositoryAdapter repository;
private Status status;
private String toolTipText; private String toolTipText;
DomainFolderRootNode(String projectName, DomainFolder rootFolder, ProjectData projectData, DomainFolderRootNode(String projectName, DomainFolder rootFolder, ProjectData projectData,
DomainFileFilter filter) { DomainFileFilter filter) {
super(rootFolder, filter); super(rootFolder, filter);
this.projectName = projectName; this.projectName = projectName;
this.repository = getProjectData().getRepository();
if (repository != null) {
repository.addListener(this);
}
toolTipText = getToolTip(projectData); toolTipText = getToolTip(projectData);
} }
@Override
public void dispose() {
if (repository != null) {
repository.removeListener(this);
}
super.dispose();
}
@Override @Override
public String getName() { public String getName() {
if (projectName == null) { if (projectName == null) {
@@ -58,21 +116,25 @@ public class DomainFolderRootNode extends DomainFolderNode {
@Override @Override
public Icon getIcon(boolean expanded) { public Icon getIcon(boolean expanded) {
return expanded ? OPEN_PROJECT : CLOSED_PROJECT; status = Status.getStatus(expanded, repository);
return status.icon;
} }
private String getToolTip(ProjectData projectData) { private String getToolTip(ProjectData projectData) {
RepositoryAdapter repository = projectData.getRepository(); ProjectLocator projectLocator = projectData.getProjectLocator();
File dir = projectData.getProjectLocator().getProjectDir(); File dir = projectLocator.getProjectDir();
String toolTip = dir.getAbsolutePath(); String toolTip = dir.getAbsolutePath();
if (!getDomainFolder().isInWritableProject() && repository != null) { if (!projectLocator.isTransient() && repository != null) {
ServerInfo info = repository.getServerInfo(); ServerInfo info = repository.getServerInfo();
String serverName = ""; String serverName = info.getServerName() + ":";
if (info != null) { String statusText = repository.isConnected() ? "connected" : "disconnected";
serverName = info.getServerName() + ", "; toolTip += " [" + serverName + repository.getName() + ", " + statusText + "]";
}
toolTip += " [" + serverName + repository.getName() + "]";
} }
return toolTip; return toolTip;
} }
@Override
public void connectionStateChanged(Object adapter) {
toolTipText = getToolTip(getProjectData());
}
} }
Binary file not shown.

After

Width:  |  Height:  |  Size: 249 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 262 B

@@ -76,6 +76,7 @@ public class VersionControlScreenShots extends GhidraScreenShotGenerator {
UndoActionDialog d = waitForDialogComponent(UndoActionDialog.class); UndoActionDialog d = waitForDialogComponent(UndoActionDialog.class);
captureDialog(d); captureDialog(d);
close(d);
} }
@Test @Test
@@ -95,6 +96,7 @@ public class VersionControlScreenShots extends GhidraScreenShotGenerator {
UndoActionDialog d = waitForDialogComponent(UndoActionDialog.class); UndoActionDialog d = waitForDialogComponent(UndoActionDialog.class);
captureDialog(d); captureDialog(d);
close(d);
} }
@Test @Test