diff --git a/debug/org.eclipse.debug.core/core/org/eclipse/debug/core/Launch.java b/debug/org.eclipse.debug.core/core/org/eclipse/debug/core/Launch.java index ad366baf37a..e63150a30a9 100644 --- a/debug/org.eclipse.debug.core/core/org/eclipse/debug/core/Launch.java +++ b/debug/org.eclipse.debug.core/core/org/eclipse/debug/core/Launch.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2000, 2018 IBM Corporation and others. + * Copyright (c) 2000, 2026 IBM Corporation and others. * * This program and the accompanying materials * are made available under the terms of the Eclipse Public License 2.0 @@ -25,15 +25,19 @@ import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock; +import org.eclipse.core.runtime.CoreException; import org.eclipse.core.runtime.IStatus; import org.eclipse.core.runtime.MultiStatus; import org.eclipse.core.runtime.PlatformObject; +import org.eclipse.core.runtime.preferences.IEclipsePreferences; +import org.eclipse.core.runtime.preferences.InstanceScope; import org.eclipse.debug.core.model.IDebugTarget; import org.eclipse.debug.core.model.IDisconnect; import org.eclipse.debug.core.model.IProcess; import org.eclipse.debug.core.model.ISourceLocator; import org.eclipse.debug.internal.core.DebugCoreMessages; import org.eclipse.debug.internal.core.LaunchManager; +import org.osgi.service.prefs.BackingStoreException; /** * A launch is the result of launching a debug session @@ -475,7 +479,24 @@ protected void fireChanged() { * properly created/initialized. */ protected void fireTerminate() { - setAttribute(DebugPlugin.ATTR_TERMINATE_TIMESTAMP, Long.toString(System.currentTimeMillis())); + String timeStamp = Long.toString(System.currentTimeMillis()); + setAttribute(DebugPlugin.ATTR_TERMINATE_TIMESTAMP, timeStamp); + ILaunchConfiguration launchConfig = getLaunchConfiguration(); + if (launchConfig != null) { + try { + if (launchConfig.isLocal()) { + ILaunchConfigurationWorkingCopy launchCopy = launchConfig.getWorkingCopy(); + launchCopy.setAttribute(DebugPlugin.ATTR_TERMINATE_TIMESTAMP, timeStamp); + launchCopy.doSave(); + } else { + IEclipsePreferences prefs = InstanceScope.INSTANCE.getNode(DebugPlugin.getUniqueIdentifier()); + prefs.put(launchConfig.getName(), timeStamp); + prefs.flush(); + } + } catch (CoreException | BackingStoreException e) { + DebugPlugin.log(e); + } + } if (!fSuppressChange) { ((LaunchManager)getLaunchManager()).fireUpdate(this, LaunchManager.TERMINATE); ((LaunchManager)getLaunchManager()).fireUpdate(new ILaunch[] {this}, LaunchManager.TERMINATE); diff --git a/debug/org.eclipse.debug.tests/src/org/eclipse/debug/tests/launching/LaunchConfigurationTests.java b/debug/org.eclipse.debug.tests/src/org/eclipse/debug/tests/launching/LaunchConfigurationTests.java index d675e66742b..fc724831f33 100644 --- a/debug/org.eclipse.debug.tests/src/org/eclipse/debug/tests/launching/LaunchConfigurationTests.java +++ b/debug/org.eclipse.debug.tests/src/org/eclipse/debug/tests/launching/LaunchConfigurationTests.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2000, 2025 IBM Corporation and others. + * Copyright (c) 2000, 2026 IBM Corporation and others. * * This program and the accompanying materials * are made available under the terms of the Eclipse Public License 2.0 @@ -2039,4 +2039,100 @@ public void launchConfigurationAdded(ILaunchConfiguration configuration) { } + @Test + public void testSharedTerminateTimeStampPersistence() throws Exception { + IProject project = getProject(); + ILaunchConfigurationWorkingCopy workingCopy = newConfiguration(project, "testSharedTerminateTimestamp"); + Set terminatedLaunches = Collections.synchronizedSet(new HashSet<>()); + + ILaunchesListener2 listener = new ILaunchesListener2() { + @Override + public void launchesRemoved(ILaunch[] launches) { + } + + @Override + public void launchesChanged(ILaunch[] launches) { + } + + @Override + public void launchesAdded(ILaunch[] launches) { + } + + @Override + public void launchesTerminated(ILaunch[] launches) { + terminatedLaunches.addAll(Arrays.asList(launches)); + } + }; + + DebugPlugin.getDefault().getLaunchManager().addLaunchListener(listener); + ILaunch launch = workingCopy.launch(ILaunchManager.DEBUG_MODE, null); + IProcess process = null; + + try { + process = DebugPlugin.newProcess(launch, new MockProcess(0), "test"); + waitWhile(() -> !terminatedLaunches.contains(launch), () -> "Launch termination event did not occur"); + IEclipsePreferences prefs = InstanceScope.INSTANCE.getNode(DebugPlugin.getUniqueIdentifier()); + String stamp = prefs.get(workingCopy.getName(), null); + assertNotNull(stamp, "Missing persisted terminate timestamp"); + long timestamp = Long.parseLong(stamp); + assertTrue(timestamp <= System.currentTimeMillis(), "Terminate timestamp should be before current time"); + } finally { + DebugPlugin.getDefault().getLaunchManager().removeLaunchListener(listener); + if (launch != null) { + getLaunchManager().removeLaunch(launch); + } + if (process != null) { + process.terminate(); + } + } + } + + @Test + public void testLocalTerminateTimeStampPersistence() throws Exception { + ILaunchConfigurationWorkingCopy workingCopy = newConfiguration(null, "testLocalTerminateTimestamp"); + Set terminatedLaunches = Collections.synchronizedSet(new HashSet<>()); + + ILaunchesListener2 listener = new ILaunchesListener2() { + @Override + public void launchesRemoved(ILaunch[] launches) { + } + + @Override + public void launchesChanged(ILaunch[] launches) { + } + + @Override + public void launchesAdded(ILaunch[] launches) { + } + + @Override + public void launchesTerminated(ILaunch[] launches) { + terminatedLaunches.addAll(Arrays.asList(launches)); + } + }; + DebugPlugin.getDefault().getLaunchManager().addLaunchListener(listener); + ILaunch launch = workingCopy.launch(ILaunchManager.DEBUG_MODE, null); + IProcess process = null; + + try { + process = DebugPlugin.newProcess(launch, new MockProcess(0), "test"); + waitWhile(() -> !terminatedLaunches.contains(launch), () -> "Launch termination event did not occur"); + ILaunchConfiguration config = launch.getLaunchConfiguration(); + assertNotNull(config); + assertTrue(config.isLocal()); + String timeStamp = config.getAttribute(DebugPlugin.ATTR_TERMINATE_TIMESTAMP, (String) null); + assertNotNull(timeStamp, "Terminate timestamp was not persisted"); + long timestamp = Long.parseLong(timeStamp); + assertTrue(timestamp <= System.currentTimeMillis(), "Terminate timestamp should be before current time"); + } finally { + DebugPlugin.getDefault().getLaunchManager().removeLaunchListener(listener); + if (launch != null) { + getLaunchManager().removeLaunch(launch); + } + if (process != null) { + process.terminate(); + } + } + } + } diff --git a/debug/org.eclipse.debug.ui/ui/org/eclipse/debug/internal/ui/actions/ActionMessages.java b/debug/org.eclipse.debug.ui/ui/org/eclipse/debug/internal/ui/actions/ActionMessages.java index 606467dee19..8942b51694a 100644 --- a/debug/org.eclipse.debug.ui/ui/org/eclipse/debug/internal/ui/actions/ActionMessages.java +++ b/debug/org.eclipse.debug.ui/ui/org/eclipse/debug/internal/ui/actions/ActionMessages.java @@ -261,5 +261,12 @@ public class ActionMessages extends NLS { public static String ExpressionPasteDialog; public static String ExpressionPasteRemember; public static String ExpressionPastePromptButton; + public static String LaunchActionToolTip_Seconds; + public static String LaunchActionToolTip_Minutes; + public static String LaunchActionToolTip_OneMinute; + public static String LaunchActionToolTip_OneHour; + public static String LaunchActionToolTip_Hours; + public static String LaunchActionToolTip_OneDay; + public static String LaunchActionToolTip_Days; } \ No newline at end of file diff --git a/debug/org.eclipse.debug.ui/ui/org/eclipse/debug/internal/ui/actions/ActionMessages.properties b/debug/org.eclipse.debug.ui/ui/org/eclipse/debug/internal/ui/actions/ActionMessages.properties index 24c158b608b..e16e1bb711c 100644 --- a/debug/org.eclipse.debug.ui/ui/org/eclipse/debug/internal/ui/actions/ActionMessages.properties +++ b/debug/org.eclipse.debug.ui/ui/org/eclipse/debug/internal/ui/actions/ActionMessages.properties @@ -245,4 +245,15 @@ ExpressionPasteSingleButton=Single Expression ExpressionPastePromptButton=Prompt ExpressionPasteTitle=Paste Multiline Expression ExpressionPasteDialog=You are pasting a multiline expression. Choose whether to paste it as a single combined expression or as separate multiple expressions. -ExpressionPasteRemember=Remember my preference next time \ No newline at end of file +ExpressionPasteRemember=Remember my preference next time + +LaunchActionToolTip_Seconds = a moment ago +LaunchActionToolTip_OneMinute = 1 min ago +LaunchActionToolTip_Minutes = {0} mins ago +LaunchActionToolTip_OneHour = 1 hour ago +LaunchActionToolTip_Hours = {0} hours ago +LaunchActionToolTip_OneDay = 1 day ago +LaunchActionToolTip_Days = {0} days ago + + + diff --git a/debug/org.eclipse.debug.ui/ui/org/eclipse/debug/ui/actions/AbstractLaunchHistoryAction.java b/debug/org.eclipse.debug.ui/ui/org/eclipse/debug/ui/actions/AbstractLaunchHistoryAction.java index 0a5d52c57b8..66c65de56b1 100644 --- a/debug/org.eclipse.debug.ui/ui/org/eclipse/debug/ui/actions/AbstractLaunchHistoryAction.java +++ b/debug/org.eclipse.debug.ui/ui/org/eclipse/debug/ui/actions/AbstractLaunchHistoryAction.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2000, 2016 IBM Corporation and others. + * Copyright (c) 2000, 2026 IBM Corporation and others. * * This program and the accompanying materials * are made available under the terms of the Eclipse Public License 2.0 @@ -22,6 +22,8 @@ import org.eclipse.core.resources.IResource; import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.preferences.IEclipsePreferences; +import org.eclipse.core.runtime.preferences.InstanceScope; import org.eclipse.debug.core.DebugPlugin; import org.eclipse.debug.core.ILaunch; import org.eclipse.debug.core.ILaunchConfiguration; @@ -50,6 +52,7 @@ import org.eclipse.jface.viewers.ISelection; import org.eclipse.jface.viewers.IStructuredSelection; import org.eclipse.jface.viewers.StructuredSelection; +import org.eclipse.osgi.util.NLS; import org.eclipse.swt.SWT; import org.eclipse.swt.events.MenuAdapter; import org.eclipse.swt.events.MenuEvent; @@ -353,6 +356,8 @@ protected void fillMenu(Menu menu) { LaunchAction action= new LaunchAction(launch, getMode()); if (checkIfLaunchActive(launch, launches)) { action.setText(action.getText() + " \u2699"); //$NON-NLS-1$ + } else { + addRecentLaunchTimeTooltip(launch, action); } addToMenu(menu, action, accelerator); accelerator++; @@ -368,6 +373,8 @@ protected void fillMenu(Menu menu) { LaunchAction action= new LaunchAction(launch, getMode()); if (checkIfLaunchActive(launch, launches)) { action.setText(action.getText() + " \u2699"); //$NON-NLS-1$ + } else { + addRecentLaunchTimeTooltip(launch, action); } addToMenu(menu, action, accelerator); accelerator++; @@ -627,4 +634,71 @@ private LaunchConfigurationManager getLaunchConfigurationManager() { protected String getLaunchGroupIdentifier() { return fLaunchGroup.getIdentifier(); } + + /** + * Adds a tooltip showing how long ago the given launch was terminated if + * there's a valid terminate timestamp attribute. + * + * @param launch launch configuration + * @param launchAction launch action to update + */ + private void addRecentLaunchTimeTooltip(ILaunchConfiguration launch, LaunchAction launchAction) { + try { + String timeStamp; + if (launch.isLocal()) { + timeStamp = launch.getAttribute(DebugPlugin.ATTR_TERMINATE_TIMESTAMP, ""); //$NON-NLS-1$ + } else { + IEclipsePreferences prefs = InstanceScope.INSTANCE.getNode(DebugPlugin.getUniqueIdentifier()); + timeStamp = prefs.get(launch.getName(), ""); //$NON-NLS-1$ + } + + if (!timeStamp.isEmpty()) { + long timestamp = Long.parseLong(timeStamp); + launchAction.setToolTipText(getRelativeTime(timestamp)); + } + } catch (CoreException | NumberFormatException e) { + DebugUIPlugin.log(e); + } + } + + /** + * Returns a human-readable relative time string for the given timestamp. + * + * @param timestamp timestamp in milliseconds since the epoch + * @return relative time string + */ + private String getRelativeTime(long timestamp) { + long diffMillis = Math.max(0L, System.currentTimeMillis() - timestamp); + long seconds = diffMillis / 1000; + + if (seconds < 60) { + return ActionMessages.LaunchActionToolTip_Seconds; + } + + long minutes = seconds / 60; + + if (minutes == 1) { + return ActionMessages.LaunchActionToolTip_OneMinute; + } + if (minutes < 60) { + return NLS.bind(ActionMessages.LaunchActionToolTip_Minutes, minutes); + } + + long hours = minutes / 60; + + if (hours == 1) { + return ActionMessages.LaunchActionToolTip_OneHour; + } + if (hours < 24) { + return NLS.bind(ActionMessages.LaunchActionToolTip_Hours, hours); + } + + long days = hours / 24; + + if (days == 1) { + return ActionMessages.LaunchActionToolTip_OneDay; + } + return NLS.bind(ActionMessages.LaunchActionToolTip_Days, days); + } + }