All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.sonarsource.sonarlint.omnisharp.OmnisharpServerController Maven / Gradle / Ivy

There is a newer version: 1.26.0.100277
Show newest version
/*
 * SonarOmnisharp
 * Copyright (C) 2021-2024 SonarSource SA
 * mailto:info AT sonarsource DOT com
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 3 of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 */
package org.sonarsource.sonarlint.omnisharp;

import java.io.IOException;
import java.nio.file.Path;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicBoolean;
import javax.annotation.Nullable;
import org.sonar.api.Startable;
import org.sonar.api.scanner.ScannerSide;
import org.sonar.api.utils.log.Logger;
import org.sonar.api.utils.log.Loggers;
import org.sonarsource.api.sonarlint.SonarLintSide;
import org.sonarsource.sonarlint.omnisharp.protocol.OmnisharpEndpoints;
import org.sonarsource.sonarlint.omnisharp.protocol.OmnisharpResponseProcessor;

import static java.util.stream.Collectors.joining;

@ScannerSide
@SonarLintSide(lifespan = "MODULE")
public class OmnisharpServerController implements Startable {

  private static final Logger LOG = Loggers.get(OmnisharpServerController.class);

  enum ServerState {
    PROCESS_STARTED,
    OMNISHARP_STARTED,
    STOPPING,
    STOPPED
  }

  private static class ServerStateMachine {
    private volatile ServerState state = ServerState.STOPPED;
    private ProcessWrapper processWrapper;
    private CompletableFuture terminationFuture = CompletableFuture.completedFuture(0);
    private CompletableFuture startFuture = failedNotStarted();
    private CompletableFuture loadProjectsFuture = failedNotStarted();

    private static CompletableFuture failedNotStarted() {
      return CompletableFuture.failedFuture(new IllegalStateException("OmniSharp not started"));
    }

    private static CompletableFuture failedToStart(Exception e) {
      return CompletableFuture.failedFuture(e);
    }

    public boolean isStopped() {
      return state == ServerState.STOPPED;
    }

    public boolean isOmnisharpStarted() {
      return state == ServerState.OMNISHARP_STARTED;
    }

    public synchronized void processStarted(ProcessWrapper processWrapper, CompletableFuture startFuture, CompletableFuture loadProjectsFuture,
      boolean loadProjectsOnDemand) {
      this.processWrapper = processWrapper;
      this.terminationFuture = processWrapper.getTerminationFuture().whenComplete((r, t) -> {
        LOG.info("Omnisharp process terminated");
        this.stopped();
      });
      this.state = ServerState.PROCESS_STARTED;
      this.startFuture = startFuture
        .whenComplete((r, t) -> {
          if (t != null) {
            loadProjectsFuture.completeExceptionally(t);
            processWrapper.destroyForcibly();
          } else {
            this.state = ServerState.OMNISHARP_STARTED;
            LOG.info("OmniSharp successfully started");
          }
        });
      if (loadProjectsOnDemand) {
        this.loadProjectsFuture = this.startFuture;
      } else {
        this.loadProjectsFuture = loadProjectsFuture
          .thenRun(() -> LOG.info("Projects successfully loaded"));
      }
    }

    public synchronized void processStartFailed(IOException e) {
      startFuture = failedToStart(e);
      loadProjectsFuture = failedToStart(e);
      this.state = ServerState.STOPPED;
    }

    public synchronized void stopped() {
      boolean stoppedNormally = this.state == ServerState.STOPPING;
      this.state = ServerState.STOPPED;
      if (stoppedNormally) {
        startFuture.cancel(true);
        loadProjectsFuture.cancel(true);
      } else {
        startFuture.completeExceptionally(new IllegalStateException("Process terminated unexpectedly"));
        loadProjectsFuture.completeExceptionally(new IllegalStateException("Process terminated unexpectedly"));
      }
      startFuture = failedNotStarted();
      loadProjectsFuture = failedNotStarted();
    }

    public void stopping() {
      this.state = ServerState.STOPPING;
    }

    public void waitForStop() throws InterruptedException, ExecutionException {
      this.processWrapper.waitForProcessToEndOrKill(1, TimeUnit.SECONDS);
      terminationFuture.get();
    }
  }

  private final ServerStateMachine stateMachine = new ServerStateMachine();

  private Path cachedProjectBaseDir;
  private Path cachedDotnetCliPath;
  private Path cachedMonoPath;
  private Path cachedMsBuildPath;
  private Path cachedSolutionPath;
  private boolean cachedUseNet6;
  private boolean cachedLoadProjectsOnDemand;

  private final OmnisharpEndpoints omnisharpEndpoints;

  private final OmnisharpResponseProcessor omnisharpResponseProcessor;

  private final OmnisharpCommandBuilder omnisharpCommandBuilder;

  public OmnisharpServerController(OmnisharpEndpoints omnisharpEndpoints, OmnisharpResponseProcessor omnisharpResponseProcessor, OmnisharpCommandBuilder omnisharpCommandBuilder) {
    this.omnisharpEndpoints = omnisharpEndpoints;
    this.omnisharpResponseProcessor = omnisharpResponseProcessor;
    this.omnisharpCommandBuilder = omnisharpCommandBuilder;
    omnisharpEndpoints.setServer(this);
  }

  public synchronized void lazyStart(Path projectBaseDir, boolean useNet6, boolean loadProjectsOnDemand, @Nullable Path dotnetCliPath, @Nullable Path monoPath,
    @Nullable Path msBuildPath,
    @Nullable Path solutionPath, int serverStartupTimeoutSec, int loadProjectsTimeoutSec)
    throws InterruptedException {
    AtomicBoolean shouldRestart = new AtomicBoolean(false);
    this.cachedProjectBaseDir = checkIfRestartRequired(cachedProjectBaseDir, projectBaseDir, "project basedir", shouldRestart);
    this.cachedDotnetCliPath = checkIfRestartRequired(cachedDotnetCliPath, dotnetCliPath, "dotnet CLI path", shouldRestart);
    this.cachedMonoPath = checkIfRestartRequired(cachedMonoPath, monoPath, "Mono location", shouldRestart);
    this.cachedMsBuildPath = checkIfRestartRequired(cachedMsBuildPath, msBuildPath, "MSBuild path", shouldRestart);
    this.cachedSolutionPath = checkIfRestartRequired(cachedSolutionPath, solutionPath, "solution path", shouldRestart);
    this.cachedUseNet6 = checkIfRestartRequired(cachedUseNet6, useNet6, "flavor of OmniSharp", shouldRestart);
    this.cachedLoadProjectsOnDemand = checkIfRestartRequired(cachedLoadProjectsOnDemand, loadProjectsOnDemand, "load projects on demand setting", shouldRestart);
    if (shouldRestart.get()) {
      stopServer();
    }
    if (stateMachine.isStopped()) {
      startServer(serverStartupTimeoutSec, loadProjectsTimeoutSec);
    }
    try {
      stateMachine.startFuture.get();
    } catch (ExecutionException e) {
      if (e.getCause() instanceof TimeoutException) {
        throw new IllegalStateException("Timeout waiting for Omnisharp server to start");
      }
      throw new IllegalStateException("Unable to start the Omnisharp server: " + e.getCause().getMessage(), e.getCause());
    }

  }

  public CompletableFuture whenReady() {
    return stateMachine.loadProjectsFuture;
  }

  private  G checkIfRestartRequired(@Nullable G oldValue, @Nullable G newValue, String label, AtomicBoolean shouldRestart) {
    if (stateMachine.isOmnisharpStarted() && !Objects.equals(oldValue, newValue)) {
      shouldRestart.set(true);
      LOG.info("Using a different {}, OmniSharp has to be restarted", label);
    }
    return newValue;
  }

  public boolean isOmnisharpStarted() {
    return stateMachine.isOmnisharpStarted();
  }

  private void startServer(int serverStartupTimeoutSec, int loadProjectsTimeoutSec) {
    var startFuture = new CompletableFuture()
      .orTimeout(serverStartupTimeoutSec, TimeUnit.SECONDS);
    var loadProjectsFuture = new CompletableFuture()
      .orTimeout(loadProjectsTimeoutSec, TimeUnit.SECONDS);
    ProcessBuilder processBuilder;
    if (cachedUseNet6) {
      processBuilder = omnisharpCommandBuilder.buildNet6(cachedProjectBaseDir, cachedDotnetCliPath, cachedMsBuildPath, cachedSolutionPath, cachedLoadProjectsOnDemand);
    } else {
      processBuilder = omnisharpCommandBuilder.build(cachedProjectBaseDir, cachedMonoPath, cachedMsBuildPath, cachedSolutionPath, cachedLoadProjectsOnDemand);
    }

    LOG.info("Starting OmniSharp...");
    LOG.debug(processBuilder.command().stream().collect(joining(" ")));
    try {
      var startedProcess = ProcessWrapper.start(processBuilder,
        s -> omnisharpResponseProcessor.handleOmnisharpOutput(startFuture, loadProjectsFuture, s), LOG::error);
      stateMachine.processStarted(startedProcess, startFuture, loadProjectsFuture, cachedLoadProjectsOnDemand);
    } catch (IOException e) {
      LOG.warn("Unable to start OmniSharp", e);
      stateMachine.processStartFailed(e);
    }
  }

  @Override
  public void start() {
    // Nothing to do
  }

  @Override
  public void stop() {
    stopServer();
  }

  public synchronized void stopServer() {
    if (!stateMachine.isStopped()) {
      stateMachine.stopping();
      LOG.info("Stopping OmniSharp");
      omnisharpEndpoints.stopServer();
      try {
        stateMachine.waitForStop();
        LOG.info("OmniSharp stopped");
      } catch (InterruptedException e) {
        LOG.debug("Interrupted!", e);
        Thread.currentThread().interrupt();
      } catch (ExecutionException e) {
        LOG.error("Could not stop Omnisharp properly", e);
        throw new IllegalStateException("Could not stop Omnisharp properly", e.getCause());
      }
    }
  }

  public synchronized boolean writeRequestOnStdIn(String str) {
    if (stateMachine.isStopped()) {
      LOG.debug("Server stopped, ignoring request");
      return false;
    }
    try {
      stateMachine.processWrapper.writeLnStdIn(str);
      return true;
    } catch (IOException e) {
      throw new IllegalStateException("Unable to write in Omnisharp stdin", e);
    }
  }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy