001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.actions; 003 004import static org.openstreetmap.josm.gui.help.HelpUtil.ht; 005import static org.openstreetmap.josm.tools.I18n.tr; 006import static org.openstreetmap.josm.tools.Utils.getSystemEnv; 007import static org.openstreetmap.josm.tools.Utils.getSystemProperty; 008 009import java.awt.Dimension; 010import java.awt.DisplayMode; 011import java.awt.GraphicsDevice; 012import java.awt.GraphicsEnvironment; 013import java.awt.Toolkit; 014import java.awt.event.ActionEvent; 015import java.awt.event.KeyEvent; 016import java.awt.geom.AffineTransform; 017import java.io.PrintWriter; 018import java.io.StringWriter; 019import java.lang.management.ManagementFactory; 020import java.util.ArrayList; 021import java.util.Arrays; 022import java.util.Collection; 023import java.util.LinkedHashMap; 024import java.util.List; 025import java.util.ListIterator; 026import java.util.Locale; 027import java.util.Map; 028import java.util.Map.Entry; 029import java.util.Optional; 030import java.util.Set; 031import java.util.stream.Collectors; 032 033import javax.swing.UIManager; 034 035import org.openstreetmap.josm.data.Preferences; 036import org.openstreetmap.josm.data.Version; 037import org.openstreetmap.josm.data.osm.DataSet; 038import org.openstreetmap.josm.data.osm.DatasetConsistencyTest; 039import org.openstreetmap.josm.data.preferences.sources.MapPaintPrefHelper; 040import org.openstreetmap.josm.data.preferences.sources.PresetPrefHelper; 041import org.openstreetmap.josm.data.preferences.sources.SourcePrefHelper; 042import org.openstreetmap.josm.data.preferences.sources.ValidatorPrefHelper; 043import org.openstreetmap.josm.gui.ExtendedDialog; 044import org.openstreetmap.josm.gui.MainApplication; 045import org.openstreetmap.josm.gui.bugreport.DebugTextDisplay; 046import org.openstreetmap.josm.gui.util.GuiHelper; 047import org.openstreetmap.josm.io.OsmApi; 048import org.openstreetmap.josm.plugins.PluginHandler; 049import org.openstreetmap.josm.spi.preferences.Config; 050import org.openstreetmap.josm.tools.Logging; 051import org.openstreetmap.josm.tools.PlatformHookUnixoid; 052import org.openstreetmap.josm.tools.PlatformManager; 053import org.openstreetmap.josm.tools.Shortcut; 054import org.openstreetmap.josm.tools.Utils; 055import org.openstreetmap.josm.tools.bugreport.BugReportSender; 056 057/** 058 * Opens a dialog with useful status information like version numbers for Java, JOSM and plugins 059 * Also includes preferences with stripped username and password. 060 * 061 * @author xeen 062 */ 063public final class ShowStatusReportAction extends JosmAction { 064 065 /** 066 * Localized description text for this action 067 */ 068 public static final String ACTION_DESCRIPTION = tr("Show status report with useful information that can be attached to bugs"); 069 070 /** 071 * Constructs a new {@code ShowStatusReportAction} 072 */ 073 public ShowStatusReportAction() { 074 super( 075 tr("Show Status Report"), 076 "misc/statusreport", 077 ACTION_DESCRIPTION, 078 Shortcut.registerShortcut("help:showstatusreport", tr("Help: {0}", 079 tr("Show Status Report")), KeyEvent.CHAR_UNDEFINED, Shortcut.NONE), true, "help/showstatusreport", false); 080 081 setHelpId(ht("/Action/ShowStatusReport")); 082 } 083 084 /** 085 * Replies the report header (software and system info) 086 * @return The report header (software and system info) 087 */ 088 public static String getReportHeader() { 089 StringWriter stringWriter = new StringWriter(256); 090 PrintWriter text = new PrintWriter(stringWriter); 091 String runtimeVersion = getSystemProperty("java.runtime.version"); 092 text.println(Version.getInstance().getReleaseAttributes()); 093 text.format("Identification: %s%n", Version.getInstance().getAgentString()); 094 String buildNumber = PlatformManager.getPlatform().getOSBuildNumber(); 095 if (!buildNumber.isEmpty()) { 096 text.format("OS Build number: %s%n", buildNumber); 097 } 098 text.format(Locale.ROOT, "Memory Usage: %d MB / %d MB (%d MB allocated, but free)%n", 099 Runtime.getRuntime().totalMemory() / 1024 / 1024, 100 Runtime.getRuntime().maxMemory() / 1024 / 1024, 101 Runtime.getRuntime().freeMemory() / 1024 / 1024); 102 text.format("Java version: %s, %s, %s%n", 103 runtimeVersion != null ? runtimeVersion : getSystemProperty("java.version"), 104 getSystemProperty("java.vendor"), 105 getSystemProperty("java.vm.name")); 106 text.format("Look and Feel: %s%n", 107 Optional.ofNullable(UIManager.getLookAndFeel()).map(laf -> laf.getClass().getName()).orElse("null")); 108 if (!GraphicsEnvironment.isHeadless()) { 109 text.append("Screen:"); 110 for (GraphicsDevice gd : GraphicsEnvironment.getLocalGraphicsEnvironment().getScreenDevices()) { 111 text.append(" ").append(gd.getIDstring()); 112 DisplayMode dm = gd.getDisplayMode(); 113 if (dm != null) { 114 AffineTransform transform = gd.getDefaultConfiguration().getDefaultTransform(); 115 // Java 11: use DisplayMode#toString 116 text.format(Locale.ROOT, " %d\u00D7%d (scaling %.2f\u00D7%.2f)", 117 dm.getWidth(), dm.getHeight(), transform.getScaleX(), transform.getScaleY()); 118 } 119 } 120 text.println(); 121 } 122 text.format("Maximum Screen Size: %s%n", toString(GuiHelper.getMaximumScreenSize())); 123 if (!GraphicsEnvironment.isHeadless()) { 124 Dimension bestCursorSize16 = Toolkit.getDefaultToolkit().getBestCursorSize(16, 16); 125 Dimension bestCursorSize32 = Toolkit.getDefaultToolkit().getBestCursorSize(32, 32); 126 text.format("Best cursor sizes: %s→%s, %s→%s%n", 127 toString(new Dimension(16, 16)), toString(bestCursorSize16), 128 toString(new Dimension(32, 32)), toString(bestCursorSize32)); 129 } 130 131 for (String name : Arrays.asList("LANG", "LC_ALL")) { 132 String value = getSystemEnv(name); 133 if (value != null) { 134 text.format("Environment variable %s: %s%n", name, value); 135 } 136 } 137 for (String name : Arrays.asList("file.encoding", "sun.jnu.encoding")) { 138 String value = getSystemProperty(name); 139 if (value != null) { 140 text.format("System property %s: %s%n", name, value); 141 } 142 } 143 text.format("Locale info: %s%n", Locale.getDefault().toString()); 144 text.format("Numbers with default locale: %s -> %d%n", Integer.toString(1_234_567_890), 1_234_567_890); 145 146 if (PlatformManager.isPlatformUnixoid()) { 147 PlatformHookUnixoid platform = (PlatformHookUnixoid) PlatformManager.getPlatform(); 148 // Add desktop environment 149 platform.getDesktopEnvironment().ifPresent(desktop -> text.format("Desktop environment: %s%n", desktop)); 150 // Add Java package details 151 String packageDetails = platform.getJavaPackageDetails(); 152 if (packageDetails != null) { 153 text.format("Java package: %s%n", packageDetails); 154 } 155 // Add WebStart package details if run from JNLP 156 if (Utils.isRunningWebStart()) { 157 String webStartDetails = platform.getWebStartPackageDetails(); 158 if (webStartDetails != null) { 159 text.format("WebStart package: %s%n", webStartDetails); 160 } 161 } 162 // Add Gnome Atk wrapper details if found 163 String atkWrapperDetails = platform.getAtkWrapperPackageDetails(); 164 if (atkWrapperDetails != null) { 165 text.format("Java ATK Wrapper package: %s%n", atkWrapperDetails); 166 } 167 // Add dependencies details if found 168 for (String p : new String[] { 169 "apache-commons-compress", "libcommons-compress-java", 170 "apache-commons-jcs-core", 171 "apache-commons-logging", "libcommons-logging-java", 172 "fonts-noto", 173 "jsonp", 174 "metadata-extractor2", 175 "signpost-core", "liboauth-signpost-java", 176 "svgsalamander" 177 }) { 178 String details = PlatformHookUnixoid.getPackageDetails(p); 179 if (details != null) { 180 text.format("%s: %s%n", p, details); 181 } 182 } 183 } 184 try { 185 // Build a new list of VM parameters to modify it below if needed (default implementation returns an UnmodifiableList instance) 186 List<String> vmArguments = new ArrayList<>(ManagementFactory.getRuntimeMXBean().getInputArguments()); 187 for (ListIterator<String> it = vmArguments.listIterator(); it.hasNext();) { 188 String value = it.next(); 189 if (value.contains("=")) { 190 String[] param = value.split("=", 2); 191 // Hide some parameters for privacy concerns 192 if (param[0].toLowerCase(Locale.ENGLISH).startsWith("-dproxy")) { 193 it.set(param[0]+"=xxx"); 194 } else if ("-Djnlpx.vmargs".equals(param[0])) { 195 // Remove jnlpx.vmargs (base64 encoded copy of VM arguments already included in clear) 196 it.remove(); 197 } else { 198 // Replace some paths for readability and privacy concerns 199 String val = paramCleanup(param[1]); 200 if (!val.equals(param[1])) { 201 it.set(param[0] + '=' + val); 202 } 203 } 204 } else if (value.startsWith("-X")) { 205 // Remove arguments like -Xbootclasspath/a, -Xverify:remote, that can be very long and unhelpful 206 it.remove(); 207 } 208 } 209 if (!vmArguments.isEmpty()) { 210 text.format("VM arguments: %s%n", vmArguments.toString().replace("\\\\", "\\")); 211 } 212 } catch (SecurityException e) { 213 Logging.trace(e); 214 } 215 List<String> commandLineArgs = MainApplication.getCommandLineArgs(); 216 if (!commandLineArgs.isEmpty()) { 217 text.format("Program arguments: %s%n", Arrays.toString(paramCleanup(commandLineArgs).toArray())); 218 } 219 DataSet dataset = MainApplication.getLayerManager().getActiveDataSet(); 220 if (dataset != null) { 221 String result = DatasetConsistencyTest.runTests(dataset); 222 if (result.isEmpty()) { 223 text.println("Dataset consistency test: No problems found"); 224 } else { 225 text.println(); 226 text.println("Dataset consistency test:"); 227 text.println(result); 228 } 229 } 230 text.println(); 231 appendCollection(text, "Plugins", Utils.transform(PluginHandler.getBugReportInformation(), i -> "+ " + i)); 232 appendCollection(text, "Tagging presets", getCustomUrls(PresetPrefHelper.INSTANCE)); 233 appendCollection(text, "Map paint styles", getCustomUrls(MapPaintPrefHelper.INSTANCE)); 234 appendCollection(text, "Validator rules", getCustomUrls(ValidatorPrefHelper.INSTANCE)); 235 appendCollection(text, "Last errors/warnings", Utils.transform(Logging.getLastErrorAndWarnings(), i -> "- " + i)); 236 237 String osmApi = OsmApi.getOsmApi().getServerUrl(); 238 if (!Config.getUrls().getDefaultOsmApiUrl().equals(osmApi.trim())) { 239 text.format("OSM API: %s%n", osmApi); 240 } 241 242 text.println(); 243 return stringWriter.toString(); 244 } 245 246 private static String toString(Dimension dimension) { 247 return dimension.width + "\u00D7" + dimension.height; 248 } 249 250 private static Collection<String> getCustomUrls(SourcePrefHelper helper) { 251 final Set<String> defaultUrls = helper.getDefault().stream() 252 .map(i -> i.url) 253 .collect(Collectors.toSet()); 254 return helper.get().stream() 255 .filter(i -> !defaultUrls.contains(i.url)) 256 .map(i -> (i.active ? "+ " : "- ") + i.url) 257 .collect(Collectors.toList()); 258 } 259 260 private static List<String> paramCleanup(Collection<String> params) { 261 return params.stream() 262 .map(ShowStatusReportAction::paramCleanup) 263 .collect(Collectors.toList()); 264 } 265 266 /** 267 * Fill map with anonymized name to the actual used path. 268 * @return map that maps shortened name to full directory path 269 */ 270 static Map<String, String> getAnonimicDirectorySymbolMap() { 271 /** maps the anonymized name to the actual used path */ 272 Map<String, String> map = new LinkedHashMap<>(); 273 map.put(PlatformManager.isPlatformWindows() ? "%JAVA_HOME%" : "${JAVA_HOME}", getSystemEnv("JAVA_HOME")); 274 map.put("<java.home>", getSystemProperty("java.home")); 275 map.put("<josm.pref>", Config.getDirs().getPreferencesDirectory(false).toString()); 276 map.put("<josm.userdata>", Config.getDirs().getUserDataDirectory(false).toString()); 277 map.put("<josm.cache>", Config.getDirs().getCacheDirectory(false).toString()); 278 map.put(PlatformManager.isPlatformWindows() ? "%UserProfile%" : "${HOME}", getSystemProperty("user.home")); 279 return map; 280 } 281 282 /** 283 * Shortens and removes private informations from a parameter used for status report. 284 * @param param parameter to cleanup 285 * @return shortened/anonymized parameter 286 */ 287 static String paramCleanup(String param) { 288 final String userName = getSystemProperty("user.name"); 289 final String userNameAlt = "<user.name>"; 290 291 String val = param; 292 for (Entry<String, String> entry : getAnonimicDirectorySymbolMap().entrySet()) { 293 val = paramReplace(val, entry.getValue(), entry.getKey()); 294 } 295 if (userName != null && userName.length() >= 3) { 296 val = paramReplace(val, userName, userNameAlt); 297 } 298 return val; 299 } 300 301 private static String paramReplace(String str, String target, String replacement) { 302 return target == null ? str : str.replace(target, replacement); 303 } 304 305 private static void appendCollection(PrintWriter text, String label, Collection<String> col) { 306 if (!col.isEmpty()) { 307 text.append(col.stream().map(o -> paramCleanup(o) + '\n') 308 .collect(Collectors.joining("", label + ":\n", "\n"))); 309 } 310 } 311 312 private static String valueCleanup(Object value) { 313 String valueString = value.toString(); 314 if (valueString.length() > 512 && value instanceof Collection<?>) { 315 valueString = ((Collection<?>) value).stream().map(v -> { 316 if (v instanceof Map<?, ?>) { 317 LinkedHashMap<Object, Object> map = new LinkedHashMap<>(((Map<?, ?>) v)); 318 map.computeIfPresent("icon", (k, icon) -> Utils.shortenString(icon.toString(), 32)); // see #19058 319 return map.toString(); 320 } else { 321 return String.valueOf(v); 322 } 323 }).collect(Collectors.joining(",\n ", "[", "\n]")); 324 } 325 return paramCleanup(valueString); 326 } 327 328 @Override 329 public void actionPerformed(ActionEvent e) { 330 StringBuilder text = new StringBuilder(); 331 String reportHeader = getReportHeader(); 332 text.append(reportHeader); 333 334 Preferences.main().getAllSettings().forEach((key, setting) -> { 335 if ("file-open.history".equals(key) 336 || "download.overpass.query".equals(key) 337 || "download.overpass.queries".equals(key) 338 || key.contains("username") 339 || key.contains("password") 340 || key.contains("access-token")) { 341 // Remove sensitive information from status report 342 return; 343 } 344 text.append(paramCleanup(key)) 345 .append('=') 346 .append(valueCleanup(setting.getValue())) 347 .append('\n'); 348 }); 349 350 DebugTextDisplay ta = new DebugTextDisplay(text.toString()); 351 352 ExtendedDialog ed = new ExtendedDialog(MainApplication.getMainFrame(), 353 tr("Status Report"), 354 tr("Copy to clipboard and close"), tr("Report bug"), tr("Close")); 355 ed.setButtonIcons("copy", "bug", "cancel"); 356 ed.configureContextsensitiveHelp("/Action/ShowStatusReport", true); 357 ed.setContent(ta, false); 358 ed.setMinimumSize(new Dimension(380, 200)); 359 ed.setPreferredSize(new Dimension(700, MainApplication.getMainFrame().getHeight()-50)); 360 361 switch (ed.showDialog().getValue()) { 362 case 1: ta.copyToClipboard(); break; 363 case 2: BugReportSender.reportBug(reportHeader); break; 364 default: // do nothing 365 } 366 GuiHelper.destroyComponents(ed, false); 367 } 368}