Skip to content

Commit

Permalink
[ROAD-957] Added option for custom CLI paths and auto-update disables (
Browse files Browse the repository at this point in the history
…#196)

* chore: move from shared project

* Removed WebBrowser

* fix: using label instead of HTML

* fix: remove redundant label

* chore: move back to shared project

* refactor: removed redundant code

* feat: add option to block CLI auto-updates

* Revert "chore: move back to shared project"

This reverts commit 15983b6.

* Added custom CLI path to view

* Added custom cli path to options

* Cli custom path with textbox

* fix divide by null

* change textbox to open-file dialog

* chore: move back to shared project

* fix: use custom cli instead of default one in SnykCli.cs

* chore: cleanup

* refactor: Added constructor for ChecksumVerificationException

* Remove redundant code

* fix: isCliDownloading not being set correctly

* fix: added finally block to guarantee isCliDownloading = false

* refactor: remove OnUiLoaded from SnykTasksService

* Remove redundant code

* refactor: make panels readonly

* fix: Set API token in options on SetupApiToken

* fix: specify Task in using directives and fix typo

* refactor: Made DisposeCancellationTokenSource static

* feat: Added properties to ChecksumVerificationException

* feat: Logging the hashes when ChecksumVerificationException is raised

* refactor: Removed RetryDownloadAsync

* refactor: removed callbacks and fixed progressbar staying visible during CLI download

* refactor: Authenticate throws FileNotFoundException

* refactor: renamed progressBar

* feat: show dog logo on message panel

* feat: show cli not found message

* refactor: use JTF instead of Task.Run

* feat: CLI custom path textbox enabled

* refactor: remove redundant tag

* refactor: add DownloadFailed event

* feat: implemented different flow for CLI download fails

* feat: removed Snyk logo from messagePanel

* feat: improved message to the user

* fix: fixed the case where download fails but CLI already exists

* fix: fire settings changed event after changing Custom CLI path

* feat: allow CLI custom executable selection only from the browse/clear buttons in settings

* fix: broken tests

* refactor: removed redundant code

* fix: Snyk.Common keeps getting recompiled

* fix: settings.json file gets deleted every time we build

* refactor: removed DoesCliExist(string)

* fix: cli auto-update false but cli is missing case

* fix: same thing but for real this time

* docs: changelog.md

* fix: settings.json deletion on pre-2022 extension

* chore: move settings.json instead of copy

* fix: token disposing nullifies the reference

* docs: changelog.md

* Revert "fix: token disposing nullifies the reference"

This reverts commit a43ba86.

* chore: removed unused directives

* feat: renamed labels in settings

* feat: changed settings UI

* chore: csproj reformatted by VS

* refactor: renamed CliAutoUpdate to BinariesAutoUpdate

* docs: added docs to SnykCliDownloader.GetCliFilePath
  • Loading branch information
Asaf Agami authored Jul 20, 2022
1 parent a157fb0 commit 9a05faa
Show file tree
Hide file tree
Showing 35 changed files with 702 additions and 437 deletions.
14 changes: 12 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,24 @@
# Snyk Changelog

## [1.1.25]

### Added
- Option to disable CLI auto-update.
- Option to select CLI custom path.
- Improved UI/UX when the CLI is missing.

### Fixed
- Several issues with auto-updating the CLI executable.

## [1.1.24]

### Fixed
- Extension errors on VS2017.
- Extension fails to load on VS2017.

## [1.1.23]

### Added
- Organization description information in settings.
- Organization description information in settings.

### Fixed
- Changing custom endpoint settings leads to authentication errors.
Expand Down
4 changes: 2 additions & 2 deletions Snyk.Common/Snyk.Common.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -70,12 +70,12 @@
</ItemGroup>
<ItemGroup>
<Content Include="appsettings.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>
<ItemGroup>
<Content Include="appsettings.development.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>
<ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
<StartAction>Program</StartAction>
<StartProgram Condition="'$(DevEnvDir)' != ''">$(DevEnvDir)devenv.exe</StartProgram>
<StartArguments>/rootsuffix Exp</StartArguments>
<DeployVsixExtensionFilesDependsOn>$(DeployVsixExtensionFilesDependsOn);SaveSettingsJsonFile</DeployVsixExtensionFilesDependsOn>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DebugSymbols>true</DebugSymbols>
Expand Down Expand Up @@ -129,4 +130,13 @@
<Target Name="AfterBuild">
</Target>
-->
<Target Name="SaveSettingsJsonFile" DependsOnTargets="GetVsixDeploymentPath">
<Message Condition="!Exists('$(VsixDeploymentPath)settings.json')" Importance="High" Text="settings.json does not exist, skipping step" />
<Message Condition="Exists('$(VsixDeploymentPath)settings.json')" Importance="High" Text="Saving settings.json file from $(VsixDeploymentPath)settings.json" />
<Move Condition="Exists('$(VsixDeploymentPath)settings.json')" SourceFiles="$(VsixDeploymentPath)settings.json" DestinationFiles="$(IntermediateOutputPath)settings.json" />
</Target>
<Target Name="AfterBuild" DependsOnTargets="GetVsixDeploymentPath">
<Message Condition="Exists('$(IntermediateOutputPath)settings.json')" Text="Copying settings.json back to $(VsixDeploymentPath)" Importance="High" />
<Move Condition="Exists('$(IntermediateOutputPath)settings.json')" DestinationFiles="$(VsixDeploymentPath)settings.json" SourceFiles="$(IntermediateOutputPath)settings.json" />
</Target>
</Project>
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,14 @@
/// </summary>
public class ChecksumVerificationException : Exception
{
/// <summary>
/// Initializes a new instance of the <see cref="ChecksumVerificationException"/> class.
/// </summary>
/// <param name="message">Exception message.</param>
public ChecksumVerificationException(string message)
: base(message)
public string ExpectedHash { get; }
public string ActualHash { get; }

public ChecksumVerificationException(string expectedHash, string actualHash)
: base($"Expected {expectedHash}, but downloaded file has {actualHash}")
{
this.ExpectedHash = expectedHash;
this.ActualHash = actualHash;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,36 +23,35 @@ public class SnykCliDownloader

private static readonly ILogger Logger = LogManager.ForContext<SnykCliDownloader>();

private string currentCliVersion;
private readonly string currentCliVersion;

private string expectedSha;

/// <summary>
/// Initializes a new instance of the <see cref="SnykCliDownloader"/> class.
/// </summary>
/// <param name="logger">ActivityLogger parameter.</param>
public SnykCliDownloader()
{
}

/// <summary>
/// Initializes a new instance of the <see cref="SnykCliDownloader"/> class.
/// </summary>
/// <param name="currentCliVersion">Initial CLI version parameter.</param>
/// <param name="logger">ActivityLogger parameter.</param>
public SnykCliDownloader(string currentCliVersion)
: this() => this.currentCliVersion = currentCliVersion;
public SnykCliDownloader(string currentCliVersion) => this.currentCliVersion = currentCliVersion;

/// <summary>
/// Callback on download finished event.
/// </summary>
public delegate void CliDownloadFinishedCallback();

/// <summary>
/// Gets the valid CLI path. When a custom CLI path is specified, it returns the custom path.
/// When the Custom CLI path is null or empty, it returns the default CLI path.
/// </summary>
/// <param name="customCliPath">The custom CLI path from the settings.</param>
/// <returns>If <paramref name="customCliPath"/> is null or empty, the default path would be returned.</returns>
private static string GetCliFilePath(string customCliPath) => string.IsNullOrEmpty(customCliPath)
? SnykCli.GetSnykCliDefaultPath()
: customCliPath;

/// <summary>
/// Request last cli information.
/// </summary>
/// <returns>Latest CLI relaese information.</returns>
/// <summary>
public LatestReleaseInfo GetLatestReleaseInfo()
{
Logger.Information("Enter GetLatestReleaseInfo method");
Expand All @@ -76,7 +75,6 @@ public LatestReleaseInfo GetLatestReleaseInfo()
/// Request last cli sha.
/// </summary>
/// <returns>CLI sha string.</returns>
/// <summary>
public string GetLatestCliSha()
{
Logger.Information("Enter GetLatestCliSha method");
Expand Down Expand Up @@ -158,7 +156,7 @@ public bool IsCliDownloadNeeded(DateTime lastCheckDate, string cliFileDestinatio
/// <param name="lastCheckDate">Last check date.</param>
/// <returns>True if new version CLI exists</returns>
public bool IsCliUpdateExists(DateTime lastCheckDate) => this.IsFourDaysPassedAfterLastCheck(lastCheckDate)
&& this.IsNewVersionAvailable(this.currentCliVersion, this.GetLatestReleaseInfo().Name);
&& this.IsNewVersionAvailable(this.currentCliVersion, this.GetLatestReleaseInfo().Version);

/// <summary>
/// Check is there a new version on the server and if there is, download it.
Expand All @@ -174,14 +172,16 @@ public async Task AutoUpdateCliAsync(
string filePath = null,
List<CliDownloadFinishedCallback> downloadFinishedCallbacks = null)
{
string fileDestinationPath = this.GetCliFilePath(filePath);
string fileDestinationPath = GetCliFilePath(filePath);

var isCliDownloadNeeded = this.IsCliDownloadNeeded(lastCheckDate, fileDestinationPath);

if (this.IsCliDownloadNeeded(lastCheckDate, fileDestinationPath))
if (isCliDownloadNeeded)
{
await this.DownloadAsync(
fileDestinationPath: fileDestinationPath,
progressWorker: progressWorker,
downloadFinishedCallbacks: downloadFinishedCallbacks);
progressWorker,
fileDestinationPath,
downloadFinishedCallbacks);
}
else
{
Expand All @@ -203,7 +203,7 @@ public async Task DownloadAsync(
{
Logger.Information("Enter Download method");

string cliFileDestinationPath = this.GetCliFilePath(fileDestinationPath);
string cliFileDestinationPath = GetCliFilePath(fileDestinationPath);

Logger.Information("CLI File Destination Path: {Path}", cliFileDestinationPath);

Expand All @@ -215,8 +215,6 @@ public async Task DownloadAsync(

LatestReleaseInfo latestReleaseInfo = this.GetLatestReleaseInfo();

string cliDownloadUrl = string.Format(LatestReleaseDownloadUrl, latestReleaseInfo.Version, SnykCli.CliFileName);

Logger.Information("Latest relase information: version {Version} and url {Url}", latestReleaseInfo.Version, latestReleaseInfo.Url);

progressWorker.CancelIfCancellationRequested();
Expand All @@ -239,14 +237,14 @@ public void VerifyCliFile(string cliPath)
{
if (!this.IsCliFileExists(cliPath))
{
throw new ChecksumVerificationException("Cli file not exists, can't verify checksum");
throw new FileNotFoundException($"Cli file not found in {cliPath}");
}

string currentSha = Sha256.Checksum(cliPath);

if (this.expectedSha.ToLower() != currentSha.ToLower())
{
throw new ChecksumVerificationException($"Expected {this.expectedSha}, but downloaded file has {currentSha}");
throw new ChecksumVerificationException(this.expectedSha, currentSha);
}
}

Expand All @@ -265,7 +263,7 @@ private void PrepareSnykCliDirectory()
}
}

private async Task DownloadAsync(
public async Task DownloadAsync(
ISnykProgressWorker progressWorker,
string cliFileDestinationPath,
string cliDownloadUrl,
Expand Down Expand Up @@ -293,6 +291,8 @@ private async Task DownloadFileAsync(
string cliFileDestinationPath,
List<CliDownloadFinishedCallback> downloadFinishedCallbacks = null)
{
const int bufferSize = 8192;

using (var client = new HttpClient())
{
client.Timeout = TimeSpan.FromMinutes(5);
Expand All @@ -303,13 +303,13 @@ private async Task DownloadFileAsync(

string tempCliFile = Path.GetTempFileName();

using (var fileStream = new FileStream(tempCliFile, FileMode.Create, FileAccess.Write, FileShare.ReadWrite, 8192, true))
using (var fileStream = new FileStream(tempCliFile, FileMode.Create, FileAccess.Write, FileShare.ReadWrite, bufferSize, true))
{
using (Stream contentStream = await response.Content.ReadAsStreamAsync())
{
var totalBytes = response.Content.Headers.ContentLength;
var totalBytes = response.Content.Headers.ContentLength ?? long.MaxValue; // Avoid dividing by null when calculating progress
var totalRead = 0L;
var buffer = new byte[8192];
var buffer = new byte[bufferSize];
var isMoreToRead = true;
var lastProgressPercentage = 0;

Expand Down Expand Up @@ -394,12 +394,10 @@ private int CliVersionAsInt(string cliVersion)
{
return int.Parse(cliVersion.Replace(".", string.Empty));
}
catch (FormatException e)
catch (FormatException)
{
return -1;
}
}

private string GetCliFilePath(string filePath) => string.IsNullOrEmpty(filePath) ? SnykCli.GetSnykCliPath() : filePath;
}
}
7 changes: 7 additions & 0 deletions Snyk.VisualStudio.Extension.Shared/CLI/ICli.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,5 +29,12 @@ public interface ICli
/// Unsets the API token stored in the config file in <code>~/.config/configstore/snyk.json</code>
/// </summary>
void UnsetApiToken();

/// <summary>
/// Checks if the CLI executable exists.
/// Checks the custom path specified in the settings, or the default path if the custom path is not specified.
/// </summary>
/// <returns>true if CLI executable is found, false otherwise.</returns>
bool IsCliFileFound();
}
}
41 changes: 28 additions & 13 deletions Snyk.VisualStudio.Extension.Shared/CLI/SnykCli.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,11 @@ public class SnykCli : ICli
/// <summary>
/// Initializes a new instance of the <see cref="SnykCli"/> class.
/// </summary>
public SnykCli() => this.ConsoleRunner = new SnykConsoleRunner();
public SnykCli(ISnykOptions options)
{
this.ConsoleRunner = new SnykConsoleRunner();
this.options = options;
}

/// <summary>
/// Gets or sets a value indicating whether <see cref="ISnykOptions"/> (settings).
Expand All @@ -39,7 +43,6 @@ public ISnykOptions Options
{
return this.options;
}

set
{
this.options = value;
Expand All @@ -53,13 +56,10 @@ public ISnykOptions Options
/// Get Snyk CLI file path.
/// </summary>
/// <returns>CLI path string.</returns>
public static string GetSnykCliPath() => Path.Combine(SnykDirectory.GetSnykAppDataDirectoryPath(), CliFileName);

/// <summary>
/// Check is CLI file exists in $UserDirectory\.AppData\Snyk.
/// </summary>
/// <returns>True if CLI file exists.</returns>
public static bool IsCliExists() => File.Exists(GetSnykCliPath());
public static string GetSnykCliDefaultPath()
{
return Path.Combine(SnykDirectory.GetSnykAppDataDirectoryPath(), CliFileName);
}

/// <summary>
/// Safely get Snyk API token from settings.
Expand All @@ -84,15 +84,23 @@ public string GetApiToken()
}

/// <inheritdoc/>
public void UnsetApiToken() => this.ConsoleRunner.Run(GetSnykCliPath(), "config unset api");
public void UnsetApiToken() => this.ConsoleRunner.Run(this.GetCliPath(), "config unset api");

/// <inheritdoc />
public bool IsCliFileFound()
{
var customPath = this.Options.CliCustomPath;
var path = string.IsNullOrEmpty(customPath) ? GetSnykCliDefaultPath() : customPath;
return File.Exists(path);
}

/// <summary>
/// Try get Snyk API token from snyk cli config or throw <see cref="InvalidTokenException"/>.
/// </summary>
/// <returns>API token string.</returns>
public string GetApiTokenOrThrowException()
{
string apiToken = this.ConsoleRunner.Run(GetSnykCliPath(), "config get api");
string apiToken = this.ConsoleRunner.Run(this.GetCliPath(), "config get api");

if (!Guid.IsValid(apiToken))
{
Expand Down Expand Up @@ -120,15 +128,15 @@ public string Authenticate()
environmentVariables.Add(ApiEnvironmentVariableName, this.Options.CustomEndpoint);
}

return this.ConsoleRunner.Run(GetSnykCliPath(), string.Join(" ", args), environmentVariables);
return this.ConsoleRunner.Run(this.GetCliPath(), string.Join(" ", args), environmentVariables);
}

/// <inheritdoc/>
public async Task<CliResult> ScanAsync(string basePath)
{
Logger.Information("Path to scan {BasePath}", basePath);

var cliPath = GetSnykCliPath();
var cliPath = this.GetCliPath();

Logger.Information("CLI path is {CliPath}", cliPath);

Expand All @@ -145,6 +153,13 @@ public async Task<CliResult> ScanAsync(string basePath)
return ConvertRawCliStringToCliResult(consoleResult);
}

private string GetCliPath()
{
var snykCliCustomPath = this.options?.CliCustomPath;
var cliPath = string.IsNullOrEmpty(snykCliCustomPath) ? GetSnykCliDefaultPath() : snykCliCustomPath;
return cliPath;
}

public StringDictionary BuildScanEnvironmentVariables()
{
var environmentVariables = new StringDictionary();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
{
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Diagnostics;
using System.Text;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
using System;
using Microsoft.VisualStudio.Shell;
using Snyk.VisualStudio.Extension.Shared.CLI;
using Snyk.VisualStudio.Extension.Shared.Service;

/// <summary>
/// Common class for <see cref="SnykScanCommand"/> and <see cref="SnykStopCurrentTaskCommand"/> task commands.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
using System;
using System.ComponentModel.Design;
using Microsoft.VisualStudio.Shell;
using Snyk.VisualStudio.Extension.Shared.UI.Notifications;
using Task = System.Threading.Tasks.Task;

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ namespace Microsoft.HtmlParser
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Text;
using System.Xml;
using Microsoft.HtmlConverter;

Expand Down
Loading

0 comments on commit 9a05faa

Please sign in to comment.