Skip to content

Commit 0f53995

Browse files
SLVS-2583 Implement cancellation
1 parent 604beab commit 0f53995

21 files changed

+830
-73
lines changed
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
/*
2+
* SonarQube Ide VisualStudio Roslyn Plugin
3+
* Copyright (C) 2025-2025 SonarSource SA
4+
* mailto:info AT sonarsource DOT com
5+
*
6+
* This program is free software; you can redistribute it and/or
7+
* modify it under the terms of the GNU Lesser General Public
8+
* License as published by the Free Software Foundation; either
9+
* version 3 of the License, or (at your option) any later version.
10+
*
11+
* This program is distributed in the hope that it will be useful,
12+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14+
* Lesser General Public License for more details.
15+
*
16+
* You should have received a copy of the GNU Lesser General Public License
17+
* along with this program; if not, write to the Free Software Foundation,
18+
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19+
*/
20+
package org.sonarsource.sonarlint.visualstudio.roslyn;
21+
22+
import org.sonar.api.Startable;
23+
import org.sonar.api.scanner.ScannerSide;
24+
import org.sonarsource.api.sonarlint.SonarLintSide;
25+
26+
import java.util.concurrent.Executors;
27+
import java.util.concurrent.ScheduledExecutorService;
28+
import java.util.concurrent.ScheduledFuture;
29+
import java.util.concurrent.TimeUnit;
30+
31+
@ScannerSide
32+
@SonarLintSide(lifespan = SonarLintSide.INSTANCE)
33+
public class AnalysisCancellationService implements Startable {
34+
private final ScheduledExecutorService scheduledExecutorService;
35+
36+
public AnalysisCancellationService() {
37+
scheduledExecutorService = Executors.newScheduledThreadPool(5);
38+
}
39+
40+
public void registerAnalysis(AnalysisTracker analysisTracker) {
41+
AnalysisPollingRunnable task = new AnalysisPollingRunnable(analysisTracker);
42+
task.selfFuture = scheduledExecutorService.scheduleAtFixedRate(task, 100, 100, TimeUnit.MILLISECONDS);
43+
}
44+
45+
@Override
46+
public void start() {
47+
// do nothing, executor created in constructor
48+
}
49+
50+
@Override
51+
public void stop() {
52+
scheduledExecutorService.shutdownNow();
53+
}
54+
55+
private static class AnalysisPollingRunnable implements Runnable{
56+
private final AnalysisTracker analysisCompletion;
57+
ScheduledFuture<?> selfFuture;
58+
59+
public AnalysisPollingRunnable(AnalysisTracker analysisCompletion) {
60+
this.analysisCompletion = analysisCompletion;
61+
}
62+
63+
@Override
64+
public void run() {
65+
if (analysisCompletion.cancelIfNeeded()) {
66+
selfFuture.cancel(false);
67+
}
68+
}
69+
}
70+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/*
2+
* SonarQube Ide VisualStudio Roslyn Plugin
3+
* Copyright (C) 2025-2025 SonarSource SA
4+
* mailto:info AT sonarsource DOT com
5+
*
6+
* This program is free software; you can redistribute it and/or
7+
* modify it under the terms of the GNU Lesser General Public
8+
* License as published by the Free Software Foundation; either
9+
* version 3 of the License, or (at your option) any later version.
10+
*
11+
* This program is distributed in the hope that it will be useful,
12+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14+
* Lesser General Public License for more details.
15+
*
16+
* You should have received a copy of the GNU Lesser General Public License
17+
* along with this program; if not, write to the Free Software Foundation,
18+
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19+
*/
20+
package org.sonarsource.sonarlint.visualstudio.roslyn;
21+
22+
import java.io.Closeable;
23+
import java.util.UUID;
24+
25+
public interface AnalysisTracker extends Closeable {
26+
UUID getAnalysisId();
27+
28+
boolean cancelIfNeeded();
29+
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
/*
2+
* SonarQube Ide VisualStudio Roslyn Plugin
3+
* Copyright (C) 2025-2025 SonarSource SA
4+
* mailto:info AT sonarsource DOT com
5+
*
6+
* This program is free software; you can redistribute it and/or
7+
* modify it under the terms of the GNU Lesser General Public
8+
* License as published by the Free Software Foundation; either
9+
* version 3 of the License, or (at your option) any later version.
10+
*
11+
* This program is distributed in the hope that it will be useful,
12+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14+
* Lesser General Public License for more details.
15+
*
16+
* You should have received a copy of the GNU Lesser General Public License
17+
* along with this program; if not, write to the Free Software Foundation,
18+
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19+
*/
20+
package org.sonarsource.sonarlint.visualstudio.roslyn;
21+
22+
import org.sonar.api.batch.sensor.SensorContext;
23+
import org.sonarsource.sonarlint.visualstudio.roslyn.http.HttpAnalysisRequestHandler;
24+
25+
import java.util.UUID;
26+
27+
public class AnalysisTrackerImpl implements AnalysisTracker {
28+
private final UUID analysisId;
29+
private boolean isCompleted;
30+
private SensorContext sensorContext;
31+
private final HttpAnalysisRequestHandler handler;
32+
33+
public AnalysisTrackerImpl(SensorContext sensorContext, HttpAnalysisRequestHandler handler, AnalysisCancellationService analysisCancellationService) {
34+
this.sensorContext = sensorContext;
35+
this.handler = handler;
36+
this.analysisId = UUID.randomUUID();
37+
analysisCancellationService.registerAnalysis(this);
38+
}
39+
40+
@Override
41+
public UUID getAnalysisId() {
42+
return analysisId;
43+
}
44+
45+
@Override
46+
public synchronized boolean cancelIfNeeded() {
47+
if (isCompleted){
48+
return true;
49+
}
50+
51+
if (sensorContext.isCancelled()) {
52+
handler.cancelAnalysis(analysisId);
53+
setCompletedState();
54+
return true;
55+
}
56+
57+
return false;
58+
}
59+
60+
@Override
61+
public synchronized void close() {
62+
setCompletedState();
63+
}
64+
65+
private synchronized void setCompletedState() {
66+
isCompleted = true;
67+
sensorContext = null;
68+
}
69+
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
/*
2+
* SonarQube Ide VisualStudio Roslyn Plugin
3+
* Copyright (C) 2025-2025 SonarSource SA
4+
* mailto:info AT sonarsource DOT com
5+
*
6+
* This program is free software; you can redistribute it and/or
7+
* modify it under the terms of the GNU Lesser General Public
8+
* License as published by the Free Software Foundation; either
9+
* version 3 of the License, or (at your option) any later version.
10+
*
11+
* This program is distributed in the hope that it will be useful,
12+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14+
* Lesser General Public License for more details.
15+
*
16+
* You should have received a copy of the GNU Lesser General Public License
17+
* along with this program; if not, write to the Free Software Foundation,
18+
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19+
*/
20+
package org.sonarsource.sonarlint.visualstudio.roslyn;
21+
22+
import org.sonar.api.batch.rule.ActiveRule;
23+
import org.sonar.api.scanner.ScannerSide;
24+
import org.sonar.api.batch.sensor.SensorContext;
25+
import org.sonarsource.api.sonarlint.SonarLintSide;
26+
import org.sonarsource.sonarlint.visualstudio.roslyn.http.AnalyzerInfoDto;
27+
import org.sonarsource.sonarlint.visualstudio.roslyn.http.HttpAnalysisRequestHandler;
28+
import org.sonarsource.sonarlint.visualstudio.roslyn.protocol.RoslynIssue;
29+
30+
import java.util.Collection;
31+
import java.util.Map;
32+
33+
/**
34+
* Factory for creating AnalysisManager instances.
35+
* This class has instance-based scope in SonarLint.
36+
*/
37+
@ScannerSide
38+
@SonarLintSide
39+
public class RemoteAnalysisService {
40+
41+
private final AnalysisCancellationService analysisCancellationService;
42+
private final HttpAnalysisRequestHandler httpAnalysisRequestHandler;
43+
private final SensorContext sensorContext;
44+
45+
public RemoteAnalysisService(
46+
AnalysisCancellationService analysisCancellationService,
47+
HttpAnalysisRequestHandler httpAnalysisRequestHandler,
48+
SensorContext sensorContext) {
49+
this.analysisCancellationService = analysisCancellationService;
50+
this.httpAnalysisRequestHandler = httpAnalysisRequestHandler;
51+
this.sensorContext = sensorContext;
52+
}
53+
54+
public Collection<RoslynIssue> analyze(
55+
Collection<String> inputFiles,
56+
Collection<ActiveRule> activeRules,
57+
Map<String, String> analysisProperties,
58+
AnalyzerInfoDto analyzerInfo) {
59+
try (var tracker = new AnalysisTrackerImpl(sensorContext, httpAnalysisRequestHandler, analysisCancellationService)) {
60+
return httpAnalysisRequestHandler.analyze(inputFiles, activeRules, analysisProperties, analyzerInfo, tracker.getAnalysisId());
61+
}
62+
}
63+
}

sonarqube-ide-visualstudio-roslyn-plugin/src/main/java/org/sonarsource/sonarlint/visualstudio/roslyn/SqvsRoslynPlugin.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import org.sonar.api.SonarProduct;
2424
import org.sonarsource.sonarlint.visualstudio.roslyn.http.HttpAnalysisRequestHandler;
2525
import org.sonarsource.sonarlint.visualstudio.roslyn.http.HttpClientHandler;
26+
import org.sonarsource.sonarlint.visualstudio.roslyn.http.HttpClientProvider;
2627
import org.sonarsource.sonarlint.visualstudio.roslyn.http.JsonRequestBuilder;
2728

2829
public class SqvsRoslynPlugin implements Plugin {
@@ -33,9 +34,12 @@ public void define(Context context) {
3334
context.addExtensions(
3435
SqvsRoslynSensor.class,
3536
JsonRequestBuilder.class,
37+
HttpClientProvider.class,
3638
HttpClientHandler.class,
39+
AnalysisCancellationService.class,
3740
InstanceConfigurationProvider.class,
3841
AnalysisPropertiesProvider.class,
42+
RemoteAnalysisService.class,
3943
HttpAnalysisRequestHandler.class);
4044
}
4145

sonarqube-ide-visualstudio-roslyn-plugin/src/main/java/org/sonarsource/sonarlint/visualstudio/roslyn/SqvsRoslynSensor.java

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -36,23 +36,24 @@
3636
import org.sonar.api.utils.log.Logger;
3737
import org.sonar.api.utils.log.Loggers;
3838
import org.sonarsource.sonarlint.visualstudio.roslyn.http.AnalyzerInfoDto;
39-
import org.sonarsource.sonarlint.visualstudio.roslyn.http.HttpAnalysisRequestHandler;
4039
import org.sonarsource.sonarlint.visualstudio.roslyn.protocol.RoslynIssue;
4140
import org.sonarsource.sonarlint.visualstudio.roslyn.protocol.RoslynIssueLocation;
4241
import org.sonarsource.sonarlint.visualstudio.roslyn.protocol.RoslynIssueQuickFix;
4342

4443
public class SqvsRoslynSensor implements Sensor {
4544

4645
private static final Logger LOG = Loggers.get(SqvsRoslynSensor.class);
47-
private final HttpAnalysisRequestHandler httpRequestHandler;
4846
private final InstanceConfigurationProvider instanceConfigurationProvider;
4947
private final AnalysisPropertiesProvider analysisPropertiesProvider;
48+
private final RemoteAnalysisService remoteAnalysisService;
5049

51-
public SqvsRoslynSensor(HttpAnalysisRequestHandler httpRequestHandler, InstanceConfigurationProvider instanceConfigurationProvider,
52-
AnalysisPropertiesProvider analysisPropertiesProvider) {
53-
this.httpRequestHandler = httpRequestHandler;
50+
public SqvsRoslynSensor(
51+
InstanceConfigurationProvider instanceConfigurationProvider,
52+
AnalysisPropertiesProvider analysisPropertiesProvider,
53+
RemoteAnalysisService remoteAnalysisService) {
5454
this.instanceConfigurationProvider = instanceConfigurationProvider;
5555
this.analysisPropertiesProvider = analysisPropertiesProvider;
56+
this.remoteAnalysisService = remoteAnalysisService;
5657
}
5758

5859
private static void handle(SensorContext context, RoslynIssue roslynIssue) {
@@ -140,14 +141,15 @@ private void analyze(SensorContext context, FilePredicate predicate) {
140141
var activeRules = getActiveRules(context);
141142
var analysisProperties = analysisPropertiesProvider.getAnalysisProperties();
142143
var analyzerInfo = getAnalyzerInfo();
143-
var roslynIssues = httpRequestHandler.analyze(inputFiles, activeRules, analysisProperties, analyzerInfo);
144+
var roslynIssues = remoteAnalysisService.analyze(inputFiles, activeRules, analysisProperties, analyzerInfo);
144145
for (var roslynIssue : roslynIssues) {
145146
try {
146147
handle(context, roslynIssue);
147148
} catch (Exception exception) {
148149
LOG.error(String.format("Issue %s can not be saved due to ", roslynIssue.getRuleId()), exception.fillInStackTrace());
149150
}
150151
}
152+
151153
}
152154

153155
private Collection<String> getFilePaths(SensorContext context, FilePredicate predicate) {

sonarqube-ide-visualstudio-roslyn-plugin/src/main/java/org/sonarsource/sonarlint/visualstudio/roslyn/http/AnalysisRequestDto.java

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,11 @@
2323
import java.util.Collection;
2424
import java.util.Map;
2525

26-
public record AnalysisRequestDto(@SerializedName("FileNames") Collection<String> fileNames, @SerializedName("ActiveRules") Collection<ActiveRuleDto> activeRules,
27-
@SerializedName("AnalysisProperties") Map<String, String> analysisProperties, @SerializedName("AnalyzerInfo") AnalyzerInfoDto analyzerInfo) {
28-
26+
public record AnalysisRequestDto(
27+
@SerializedName("FileNames") Collection<String> fileNames,
28+
@SerializedName("ActiveRules") Collection<ActiveRuleDto> activeRules,
29+
@SerializedName("AnalysisProperties") Map<String, String> analysisProperties,
30+
@SerializedName("AnalyzerInfo") AnalyzerInfoDto analyzerInfo,
31+
@SerializedName("AnalysisId") java.util.UUID analysisId) {
2932
}
33+
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
/*
2+
* SonarQube Ide VisualStudio Roslyn Plugin
3+
* Copyright (C) 2025-2025 SonarSource SA
4+
* mailto:info AT sonarsource DOT com
5+
*
6+
* This program is free software; you can redistribute it and/or
7+
* modify it under the terms of the GNU Lesser General Public
8+
* License as published by the Free Software Foundation; either
9+
* version 3 of the License, or (at your option) any later version.
10+
*
11+
* This program is distributed in the hope that it will be useful,
12+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14+
* Lesser General Public License for more details.
15+
*
16+
* You should have received a copy of the GNU Lesser General Public License
17+
* along with this program; if not, write to the Free Software Foundation,
18+
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19+
*/
20+
package org.sonarsource.sonarlint.visualstudio.roslyn.http;
21+
22+
import com.google.gson.annotations.SerializedName;
23+
24+
public record CancellationRequestDto(@SerializedName("AnalysisId") java.util.UUID analysisId) {
25+
}

sonarqube-ide-visualstudio-roslyn-plugin/src/main/java/org/sonarsource/sonarlint/visualstudio/roslyn/http/HttpAnalysisRequestHandler.java

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@
2424
import java.util.ArrayList;
2525
import java.util.Collection;
2626
import java.util.Map;
27+
import java.util.UUID;
28+
2729
import org.sonar.api.batch.rule.ActiveRule;
2830
import org.sonar.api.scanner.ScannerSide;
2931
import org.sonar.api.utils.log.Logger;
@@ -41,10 +43,15 @@ public HttpAnalysisRequestHandler(HttpClientHandler httpClientFactory) {
4143
this.httpClientFactory = httpClientFactory;
4244
}
4345

44-
public Collection<RoslynIssue> analyze(Collection<String> fileNames, Collection<ActiveRule> activeRules, Map<String, String> analysisProperties, AnalyzerInfoDto analyzerInfo) {
46+
public Collection<RoslynIssue> analyze(
47+
Collection<String> fileNames,
48+
Collection<ActiveRule> activeRules,
49+
Map<String, String> analysisProperties,
50+
AnalyzerInfoDto analyzerInfo,
51+
UUID analysisId) {
4552
Collection<RoslynIssue> roslynIssues = new ArrayList<>();
4653
try {
47-
var response = httpClientFactory.sendRequest(fileNames, activeRules, analysisProperties, analyzerInfo);
54+
var response = httpClientFactory.sendAnalyzeRequest(fileNames, activeRules, analysisProperties, analyzerInfo, analysisId);
4855
if (response.statusCode() != HttpURLConnection.HTTP_OK) {
4956
LOG.error("Response from server is {}.", response.statusCode());
5057
return roslynIssues;
@@ -66,4 +73,18 @@ public Collection<RoslynIssue> analyze(Collection<String> fileNames, Collection<
6673

6774
return roslynIssues;
6875
}
76+
77+
public void cancelAnalysis(UUID analysisId) {
78+
var requestFuture = httpClientFactory.sendCancelRequest(analysisId);
79+
80+
requestFuture.exceptionally(e -> {
81+
LOG.error("Failed to cancel analysis due to: {}", e.getMessage(), e);
82+
return null;
83+
}).thenApply(response -> {
84+
if (response.statusCode() != HttpURLConnection.HTTP_OK) {
85+
LOG.error("Response from cancel request is {}.", response.statusCode());
86+
}
87+
return null;
88+
});
89+
}
6990
}

0 commit comments

Comments
 (0)