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>
<BLOCKQUOTE>
<P>When you open a shared project, Ghidra attempts to connect to the server that is
associated with the shared project. Depending on what user authentication mode the server is
using, you may have to enter a password. If the server is not running, you are still able to
work with your checked out files while you are offline. Other versioned files not checked out
are not accessible. When the server comes up, Ghidra will reconnect as necessary. You can
also attempt to connect "manually" by selecting the connection status button <IMG alt="" src=
"images/disconnected.gif" border="0"> on the Ghidra Project Window or on the <I><A href=
"help/topics/FrontEndPlugin/Project_Info.htm">Project Info</A></I> dialog. When the
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>
<P>When you open or view a shared project, Ghidra attempts to connect to the corresponding server.
Depending on which user authentication mode the server is using, you may have to enter a password.
If you choose not to connect or lose the connection to the repository server after opening
or viewing a project, shared files not
checked out will not be shown within the Ghidra Project Window, as they are unavailable for use.
Local project private files are not affected by the repository server connection and will always
be shown. If you subsequently connect to the repository server, Ghidra will refresh
the project views to reflect the current state.&nbsp;</P>
<BLOCKQUOTE>
<P>
<IMG SRC="help/shared/tip.png" />
The root folder node of the Project Data Tree view of a shared project will convey the
current connection status with green (connected) or red (disconnected) indicator.&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" />
You are authenticated only once per
Ghidra session; so if you open other project repositories managed by the same Ghidra Server,
you will be prompted only once for a password, as required.&nbsp;</P>
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>
<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/connected.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/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|
@@ -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/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/red_can.png||GHIDRA||||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/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.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.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 ProjectRepoConnectAction repoConnectAction;
private ProjectDataCutAction cutAction;
private ClearCutAction clearCutAction;
private ProjectDataCopyAction copyAction;
@@ -221,6 +222,7 @@ public class FrontEndPlugin extends Plugin
String owner = getName();
// Top of popup menu actions - no group
repoConnectAction = new ProjectRepoConnectAction(this, null);
openAction = new ProjectDataOpenDefaultToolAction(owner, null);
followLinkAction = new ProjectDataFollowLinkAction(this, null);
selectRealFileOrFolderAction = new ProjectDataSelectRealFileOrFolderAction(this, null);
@@ -251,6 +253,7 @@ public class FrontEndPlugin extends Plugin
groupName = "XRefresh";
refreshAction = new ProjectDataRefreshAction(owner, groupName);
tool.addAction(repoConnectAction);
tool.addAction(newFolderAction);
tool.addAction(cutAction);
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;
}
}
@@ -4,9 +4,9 @@
* 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.
@@ -21,23 +21,81 @@ import javax.swing.Icon;
import docking.tool.ToolConstants;
import generic.theme.GIcon;
import ghidra.framework.client.RemoteAdapterListener;
import ghidra.framework.client.RepositoryAdapter;
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 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 RepositoryAdapter repository;
private Status status;
private String toolTipText;
DomainFolderRootNode(String projectName, DomainFolder rootFolder, ProjectData projectData,
DomainFileFilter filter) {
super(rootFolder, filter);
this.projectName = projectName;
this.repository = getProjectData().getRepository();
if (repository != null) {
repository.addListener(this);
}
toolTipText = getToolTip(projectData);
}
@Override
public void dispose() {
if (repository != null) {
repository.removeListener(this);
}
super.dispose();
}
@Override
public String getName() {
if (projectName == null) {
@@ -58,21 +116,25 @@ public class DomainFolderRootNode extends DomainFolderNode {
@Override
public Icon getIcon(boolean expanded) {
return expanded ? OPEN_PROJECT : CLOSED_PROJECT;
status = Status.getStatus(expanded, repository);
return status.icon;
}
private String getToolTip(ProjectData projectData) {
RepositoryAdapter repository = projectData.getRepository();
File dir = projectData.getProjectLocator().getProjectDir();
ProjectLocator projectLocator = projectData.getProjectLocator();
File dir = projectLocator.getProjectDir();
String toolTip = dir.getAbsolutePath();
if (!getDomainFolder().isInWritableProject() && repository != null) {
if (!projectLocator.isTransient() && repository != null) {
ServerInfo info = repository.getServerInfo();
String serverName = "";
if (info != null) {
serverName = info.getServerName() + ", ";
}
toolTip += " [" + serverName + repository.getName() + "]";
String serverName = info.getServerName() + ":";
String statusText = repository.isConnected() ? "connected" : "disconnected";
toolTip += " [" + serverName + repository.getName() + ", " + statusText + "]";
}
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

@@ -4,9 +4,9 @@
* 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.
@@ -76,6 +76,7 @@ public class VersionControlScreenShots extends GhidraScreenShotGenerator {
UndoActionDialog d = waitForDialogComponent(UndoActionDialog.class);
captureDialog(d);
close(d);
}
@Test
@@ -95,6 +96,7 @@ public class VersionControlScreenShots extends GhidraScreenShotGenerator {
UndoActionDialog d = waitForDialogComponent(UndoActionDialog.class);
captureDialog(d);
close(d);
}
@Test