diff --git a/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/protocol/LspServerTelemetryManager.java b/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/protocol/LspServerTelemetryManager.java index d82646afb16c..b008279cc46d 100644 --- a/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/protocol/LspServerTelemetryManager.java +++ b/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/protocol/LspServerTelemetryManager.java @@ -21,6 +21,7 @@ import com.google.gson.JsonArray; import com.google.gson.JsonObject; import com.google.gson.JsonPrimitive; +import java.lang.ref.WeakReference; import java.math.BigInteger; import java.nio.charset.StandardCharsets; import java.security.MessageDigest; @@ -28,25 +29,29 @@ import java.util.ArrayList; import java.util.Collection; import java.util.Collections; -import java.util.HashSet; +import java.util.Iterator; import java.util.List; import java.util.Map; -import java.util.Set; +import java.util.NavigableMap; +import java.util.TreeMap; import java.util.WeakHashMap; import java.util.concurrent.Future; -import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Function; +import java.util.logging.Level; +import java.util.logging.Logger; import java.util.stream.Collectors; import org.eclipse.lsp4j.ConfigurationItem; import org.eclipse.lsp4j.ConfigurationParams; import org.eclipse.lsp4j.MessageType; import org.eclipse.lsp4j.services.LanguageClient; +import org.netbeans.api.java.platform.JavaPlatform; import org.netbeans.api.java.queries.CompilerOptionsQuery; import org.netbeans.api.java.queries.CompilerOptionsQuery.Result; import org.netbeans.api.project.Project; import org.netbeans.api.project.ProjectManager; import org.netbeans.api.project.ui.ProjectProblems; +import org.netbeans.modules.java.platform.implspi.JavaPlatformProvider; import org.openide.filesystems.FileObject; -import org.openide.util.Exceptions; import org.openide.util.Lookup; /** @@ -55,130 +60,200 @@ */ public class LspServerTelemetryManager { - public final String SCAN_START_EVT = "SCAN_START_EVT"; - public final String SCAN_END_EVT = "SCAN_END_EVT"; - public final String WORKSPACE_INFO_EVT = "WORKSPACE_INFO_EVT"; + private static final Logger LOG = Logger.getLogger(LspServerTelemetryManager.class.getName()); + public static final String SCAN_START_EVT = "SCAN_START_EVT"; + public static final String SCAN_END_EVT = "SCAN_END_EVT"; + public static final String WORKSPACE_INFO_EVT = "workspaceChange"; - private final String ENABLE_PREVIEW = "--enable-preview"; - private final String STANDALONE_PRJ = "Standalone"; - private final WeakHashMap> clients = new WeakHashMap<>(); - private long lspServerIntiailizationTime; + private static final String ENABLE_PREVIEW = "--enable-preview"; - public synchronized void connect(LanguageClient client, Future future) { - clients.put(client, future); - lspServerIntiailizationTime = System.currentTimeMillis(); + public static enum ProjectType { + standalone, + maven, + gradle; } - public synchronized void sendTelemetry(TelemetryEvent event) { - Set toRemove = new HashSet<>(); - List toSendTelemetry = new ArrayList<>(); + private LspServerTelemetryManager() { + } + + public static LspServerTelemetryManager getInstance() { + return Singleton.instance; + } + + private static class Singleton { + + private static final LspServerTelemetryManager instance = new LspServerTelemetryManager(); + } + + private final WeakHashMap>> clients = new WeakHashMap<>(); + private volatile boolean telemetryEnabled = false; + private long lspServerIntializationTime; + public boolean isTelemetryEnabled() { + return telemetryEnabled; + } + + public void connect(LanguageClient client, Future future) { synchronized (clients) { - for (Map.Entry> entry : clients.entrySet()) { - if (entry.getValue().isDone()) { - toRemove.add(entry.getKey()); - } else { - toSendTelemetry.add(entry.getKey()); - } - } - clients.keySet().removeAll(toRemove); + clients.put(client, new WeakReference<>(future)); + telemetryEnabled = true; + lspServerIntializationTime = System.currentTimeMillis(); } + } - for (LanguageClient client : toSendTelemetry) { - client.telemetryEvent(event); + public void sendTelemetry(TelemetryEvent event) { + if (telemetryEnabled) { + ArrayList clientsCopy = new ArrayList<>(2); + synchronized (clients) { + Iterator>>> iterator = clients.entrySet().iterator(); + while (iterator.hasNext()) { + Map.Entry>> e = iterator.next(); + if (isInvalidClient(e.getValue())) { + iterator.remove(); + } else { + clientsCopy.add(e.getKey()); + } + } + if (clientsCopy.isEmpty()) { + telemetryEnabled = false; + } + } + clientsCopy.forEach(c -> sendTelemetryToValidClient(c, event)); } } - - public void sendTelemetry(LanguageClient client, TelemetryEvent event) { - boolean shouldSendTelemetry = false; - synchronized (clients) { - if(clients.containsKey(client)){ - if (clients.get(client).isDone()) { - clients.remove(client); - } else { - shouldSendTelemetry = true; + public void sendTelemetry(LanguageClient client, TelemetryEvent event) { + if (telemetryEnabled) { + WeakReference> closeListener = clients.get(client); + if (isInvalidClient(closeListener)) { + synchronized (clients) { + if (clients.remove(client, closeListener) && clients.isEmpty()) { + telemetryEnabled = false; + } } + } else { + sendTelemetryToValidClient(client, event); } } + } - if (shouldSendTelemetry) { + private void sendTelemetryToValidClient(LanguageClient client, TelemetryEvent event) { + try { client.telemetryEvent(event); + } catch (Exception e) { + LOG.log(Level.INFO, "telemetry send failed: {0}", e.getMessage()); } } - public void sendWorkspaceInfo(LanguageClient client, List workspaceClientFolders, Collection prjs, long timeToOpenPrjs) { + private boolean isInvalidClient(WeakReference> closeListener) { + Future close = closeListener == null ? null : closeListener.get(); + return close == null || close.isDone(); + } + + public void sendWorkspaceInfo(LanguageClient client, List workspaceClientFolders, Collection projects, long timeToOpenProjects) { JsonObject properties = new JsonObject(); JsonArray prjProps = new JsonArray(); - Map mp = prjs.stream() - .collect(Collectors.toMap(project -> project.getProjectDirectory().getPath(), project -> project)); + NavigableMap mp = projects.stream() + .collect(Collectors.toMap(project -> project.getProjectDirectory().getPath(), project -> project, (p1, p2) -> p1, TreeMap::new)); for (FileObject workspaceFolder : workspaceClientFolders) { try { - JsonObject obj = new JsonObject(); + boolean noProjectFound = true; String prjPath = workspaceFolder.getPath(); - String prjId = this.getPrjId(prjPath); - obj.addProperty("id", prjId); - - // In future if different JDK is used for different project then this can be updated - obj.addProperty("javaVersion", System.getProperty("java.version")); - - if (mp.containsKey(prjPath)) { - Project prj = mp.get(prjPath); - - ProjectManager.Result r = ProjectManager.getDefault().isProject2(prj.getProjectDirectory()); - String projectType = r.getProjectType(); - obj.addProperty("buildTool", (projectType.contains("maven") ? "MavenProject" : "GradleProject")); - - obj.addProperty("openedWithProblems", ProjectProblems.isBroken(prj)); - - boolean isPreviewFlagEnabled = this.isEnablePreivew(prj.getProjectDirectory(), projectType); - obj.addProperty("enablePreview", isPreviewFlagEnabled); - } else { - obj.addProperty("buildTool", this.STANDALONE_PRJ); - obj.addProperty("javaVersion", System.getProperty("java.version")); - obj.addProperty("openedWithProblems", false); - - boolean isPreviewFlagEnabled = this.isEnablePreivew(workspaceFolder, this.STANDALONE_PRJ); - obj.addProperty("enablePreview", isPreviewFlagEnabled); + String prjPathWithSlash = null; + for (Map.Entry p : mp.tailMap(prjPath, true).entrySet()) { + String projectPath = p.getKey(); + if (prjPathWithSlash == null) { + if (prjPath.equals(projectPath)) { + prjProps.add(createProjectInfo(prjPath, p.getValue(), workspaceFolder, client)); + noProjectFound = false; + break; + } + prjPathWithSlash = prjPath + '/'; + } + if (projectPath.startsWith(prjPathWithSlash)) { + prjProps.add(createProjectInfo(p.getKey(), p.getValue(), workspaceFolder, client)); + noProjectFound = false; + continue; + } + break; } - - prjProps.add(obj); - - } catch (NoSuchAlgorithmException ex) { - Exceptions.printStackTrace(ex); + if (noProjectFound) { + // No project found + prjProps.add(createProjectInfo(prjPath, null, workspaceFolder, client)); + } + } catch (NoSuchAlgorithmException e) { + LOG.log(Level.INFO, "NoSuchAlgorithmException while creating workspaceInfo event: {0}", e.getMessage()); + } catch (Exception e) { + LOG.log(Level.INFO, "Exception while creating workspaceInfo event: {0}", e.getMessage()); } } - properties.add("prjsInfo", prjProps); + properties.add("projectInfo", prjProps); - properties.addProperty("timeToOpenPrjs", timeToOpenPrjs); - properties.addProperty("numOfPrjsOpened", workspaceClientFolders.size()); - properties.addProperty("lspServerInitializationTime", System.currentTimeMillis() - this.lspServerIntiailizationTime); + properties.addProperty("projInitTimeTaken", timeToOpenProjects); + properties.addProperty("numProjects", workspaceClientFolders.size()); + properties.addProperty("lspInitTimeTaken", System.currentTimeMillis() - this.lspServerIntializationTime); - this.sendTelemetry(client, new TelemetryEvent(MessageType.Info.toString(), this.WORKSPACE_INFO_EVT, properties)); + this.sendTelemetry(client, new TelemetryEvent(MessageType.Info.toString(), LspServerTelemetryManager.WORKSPACE_INFO_EVT, properties)); } - - private boolean isEnablePreivew(FileObject source, String prjType) { - if (prjType.equals(this.STANDALONE_PRJ)) { - NbCodeLanguageClient client = Lookup.getDefault().lookup(NbCodeLanguageClient.class); + + private JsonObject createProjectInfo(String prjPath, Project prj, FileObject workspaceFolder, LanguageClient client) throws NoSuchAlgorithmException { + JsonObject obj = new JsonObject(); + String prjId = getPrjId(prjPath); + obj.addProperty("id", prjId); + FileObject projectDirectory; + ProjectType projectType; + if (prj == null) { + projectType = ProjectType.standalone; + projectDirectory = workspaceFolder; + } else { + projectType = getProjectType(prj); + projectDirectory = prj.getProjectDirectory(); + boolean projectHasProblems; + try { + projectHasProblems = ProjectProblems.isBroken(prj); + } catch (RuntimeException e) { + LOG.log(Level.INFO, "Exception while checking project problems for workspaceInfo event: {0}", e.getMessage()); + projectHasProblems = true; + } + obj.addProperty("isOpenedWithProblems", projectHasProblems); + } + String javaVersion = getProjectJavaVersion(); + obj.addProperty("javaVersion", javaVersion); + obj.addProperty("buildTool", projectType.name()); + boolean isPreviewFlagEnabled = isPreviewEnabled(projectDirectory, projectType, client); + obj.addProperty("isPreviewEnabled", isPreviewFlagEnabled); + return obj; + } + + public boolean isPreviewEnabled(FileObject source, ProjectType prjType) { + return isPreviewEnabled(source, prjType, null); + } + + public boolean isPreviewEnabled(FileObject source, ProjectType prjType, LanguageClient languageClient) { + if (prjType == ProjectType.standalone) { + NbCodeLanguageClient client = languageClient instanceof NbCodeLanguageClient ? (NbCodeLanguageClient) languageClient : null ; if (client == null) { - return false; + client = Lookup.getDefault().lookup(NbCodeLanguageClient.class); + if (client == null) { + return false; + } } - AtomicBoolean isEnablePreviewSet = new AtomicBoolean(false); + boolean[] isEnablePreviewSet = {false}; ConfigurationItem conf = new ConfigurationItem(); - conf.setSection(client.getNbCodeCapabilities().getAltConfigurationPrefix() + "runConfig.vmOptions"); - client.configuration(new ConfigurationParams(Collections.singletonList(conf))).thenAccept(c -> { - String config = ((JsonPrimitive) ((List) c).get(0)).getAsString(); - isEnablePreviewSet.set(config.contains(this.ENABLE_PREVIEW)); - }); - - return isEnablePreviewSet.get(); + conf.setSection(client.getNbCodeCapabilities().getConfigurationPrefix() + "runConfig.vmOptions"); + client.configuration(new ConfigurationParams(Collections.singletonList(conf))) + .thenAccept(c -> { + isEnablePreviewSet[0] = c != null && !c.isEmpty() + && ((JsonPrimitive) c.get(0)).getAsString().contains(ENABLE_PREVIEW); + }); + return isEnablePreviewSet[0]; } - + Result result = CompilerOptionsQuery.getOptions(source); - return result.getArguments().contains(this.ENABLE_PREVIEW); + return result.getArguments().contains(ENABLE_PREVIEW); } private String getPrjId(String prjPath) throws NoSuchAlgorithmException { @@ -187,15 +262,50 @@ private String getPrjId(String prjPath) throws NoSuchAlgorithmException { BigInteger number = new BigInteger(1, hash); - // Convert message digest into hex value StringBuilder hexString = new StringBuilder(number.toString(16)); - // Pad with leading zeros while (hexString.length() < 64) { hexString.insert(0, '0'); } return hexString.toString(); } - + + private String getProjectJavaVersion() { + final JavaPlatformProvider javaPlatformProvider = Lookup.getDefault().lookup(JavaPlatformProvider.class); + final JavaPlatform defaultPlatform = javaPlatformProvider == null ? null : javaPlatformProvider.getDefaultPlatform(); + final Map props = defaultPlatform == null ? null : defaultPlatform.getSystemProperties(); + final Function propLookup = props == null ? System::getProperty : props::get; + + return getJavaRuntimeVersion(propLookup) + ';' + getJavaVmVersion(propLookup) + ';' + getJavaVmName(propLookup); + } + + public static String getJavaRuntimeVersion(Function propertyLookup) { + String version = propertyLookup.apply("java.runtime.version"); + if (version == null) { + version = propertyLookup.apply("java.version"); + } + return version; + } + + public static String getJavaVmVersion(Function propertyLookup) { + String version = propertyLookup.apply("java.vendor.version"); + if (version == null) { + version = propertyLookup.apply("java.vm.version"); + if (version == null) { + version = propertyLookup.apply("java.version"); + } + } + return version; + } + + public static String getJavaVmName(Function propertyLookup) { + return propertyLookup.apply("java.vm.name"); + } + + public ProjectType getProjectType(Project prj) { + ProjectManager.Result r = ProjectManager.getDefault().isProject2(prj.getProjectDirectory()); + String projectType = r == null ? null : r.getProjectType(); + return projectType != null && projectType.contains(ProjectType.maven.name()) ? ProjectType.maven : ProjectType.gradle; + } } diff --git a/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/protocol/NbCodeClientCapabilities.java b/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/protocol/NbCodeClientCapabilities.java index 9134992f5f38..2c9c356cb25d 100644 --- a/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/protocol/NbCodeClientCapabilities.java +++ b/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/protocol/NbCodeClientCapabilities.java @@ -90,6 +90,11 @@ public final class NbCodeClientCapabilities { * Secondary prefix for configuration. */ private String altConfigurationPrefix = "java+."; + + /** + * Whether telemetry needs to be enabled. + */ + private Boolean wantsTelemetryEnabled = Boolean.FALSE; public ClientCapabilities getClientCapabilities() { return clientCaps; @@ -179,6 +184,10 @@ public void setAltConfigurationPrefix(String altConfigurationPrefix) { this.altConfigurationPrefix = altConfigurationPrefix; } + public boolean wantsTelemetryEnabled() { + return wantsTelemetryEnabled == Boolean.TRUE; + } + private NbCodeClientCapabilities withCapabilities(ClientCapabilities caps) { if (caps == null) { caps = new ClientCapabilities(); diff --git a/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/protocol/Server.java b/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/protocol/Server.java index 9430217f02c1..72e5b8d40738 100644 --- a/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/protocol/Server.java +++ b/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/protocol/Server.java @@ -165,7 +165,6 @@ public final class Server { * Special logger that logs LSP in/out messages. */ private static final Logger LSP_LOG = Logger.getLogger("org.netbeans.modules.java.lsp.server.lsptrace"); // NOI18N - private static final LspServerTelemetryManager LSP_SERVER_TELEMETRY = new LspServerTelemetryManager(); private static final ErrorsNotifier ERR_NOTIFIER = new ErrorsNotifier(); private Server() { @@ -189,7 +188,6 @@ public static NbLspServer launchServer(Pair io, LspSe ((LanguageClientAware) server).connect(remote); msgProcessor.attachClient(server.client); Future runningServer = serverLauncher.startListening(); - LSP_SERVER_TELEMETRY.connect(server.client, runningServer); ERR_NOTIFIER.connect(server, runningServer); return new NbLspServer(server, runningServer); } @@ -794,7 +792,7 @@ private void asyncOpenSelectedProjects1(CompletableFuture f, Project[ } f.complete(candidateMapping); List workspaceClientFolders = workspaceService.getClientWorkspaceFolders(); - LSP_SERVER_TELEMETRY.sendWorkspaceInfo(client, workspaceClientFolders, openedProjects, System.currentTimeMillis() - t); + LspServerTelemetryManager.getInstance().sendWorkspaceInfo(client, workspaceClientFolders, openedProjects, System.currentTimeMillis() - t); LOG.log(Level.INFO, "{0} projects opened in {1}ms", new Object[] { prjsRequested.length, (System.currentTimeMillis() - t) }); } else { LOG.log(Level.FINER, "{0}: Collecting projects to prime from: {1}", new Object[]{id, Arrays.asList(additionalProjects)}); @@ -955,6 +953,9 @@ private InitializeResult constructInitResponse(InitializeParams init, JavaSource public CompletableFuture initialize(InitializeParams init) { NbCodeClientCapabilities capa = NbCodeClientCapabilities.get(init); client.setClientCaps(capa); + if (capa != null && capa.wantsTelemetryEnabled()) { + LspServerTelemetryManager.getInstance().connect(client, lspSession.getLspServer().getRunningFuture()); + } hackConfigureGroovySupport(capa); hackNoReuseOfOutputsForAntProjects(); List projectCandidates = new ArrayList<>(); @@ -1454,13 +1455,13 @@ private CustomIndexerTelemetryFactory() { @Override public synchronized boolean scanStarted(Context context) { - LSP_SERVER_TELEMETRY.sendTelemetry(new TelemetryEvent(MessageType.Info.toString(), LSP_SERVER_TELEMETRY.SCAN_START_EVT, "nbls.scanStarted")); - return true; + LspServerTelemetryManager.getInstance().sendTelemetry(new TelemetryEvent(MessageType.Info.toString(), LspServerTelemetryManager.SCAN_START_EVT, "nbls.scanStarted")); + return true; } @Override public synchronized void scanFinished(Context context) { - LSP_SERVER_TELEMETRY.sendTelemetry(new TelemetryEvent(MessageType.Info.toString(),LSP_SERVER_TELEMETRY.SCAN_END_EVT,"nbls.scanFinished")); + LspServerTelemetryManager.getInstance().sendTelemetry(new TelemetryEvent(MessageType.Info.toString(), LspServerTelemetryManager.SCAN_END_EVT,"nbls.scanFinished")); ERR_NOTIFIER.notifyErrors(context.getRootURI()); }