diff --git a/Build.Docker.ps1 b/Build.Docker.ps1 index 4fc6c30d..88e62dc5 100644 --- a/Build.Docker.ps1 +++ b/Build.Docker.ps1 @@ -10,6 +10,13 @@ function Execute-Tests { & dotnet test ./test/SeqCli.Tests/SeqCli.Tests.csproj -c Release /p:Configuration=Release /p:Platform=x64 /p:VersionPrefix=$version if ($LASTEXITCODE -ne 0) { exit 1 } + + cd ./test/SeqCli.EndToEnd/ + docker pull datalust/seq:latest + $env:ENDTOEND_USE_DOCKER_SEQ="Y" + & dotnet run + if ($LASTEXITCODE -ne 0) { exit 1 } + cd ../.. } function Build-DockerImage diff --git a/Build.ps1 b/Build.ps1 index 8a11f5af..2ec9db41 100644 --- a/Build.ps1 +++ b/Build.ps1 @@ -25,9 +25,9 @@ function Create-ArtifactDir function Publish-Archives($version) { - $rids = @("linux-x64", "osx-x64", "win-x64") + $rids = @("linux-x64", "linux-musl-x64", "osx-x64", "win-x64") foreach ($rid in $rids) { - & dotnet publish src/SeqCli/SeqCli.csproj -c Release -f $framework -r $rid /p:VersionPrefix=$version /p:SeqCliRid=$rid /p:ShowLinkerSizeComparison=true + & dotnet publish ./src/SeqCli/SeqCli.csproj -c Release -f $framework -r $rid /p:VersionPrefix=$version /p:SeqCliRid=$rid /p:ShowLinkerSizeComparison=true if($LASTEXITCODE -ne 0) { exit 4 } # Make sure the archive contains a reasonable root filename @@ -51,6 +51,11 @@ function Publish-Archives($version) } } +function Publish-DotNetTool($version) +{ + dotnet pack ./src/SeqCli/SeqCli.csproj -c Release --output ./artifacts /p:VersionPrefix=$version +} + Push-Location $PSScriptRoot $version = @{ $true = $env:APPVEYOR_BUILD_VERSION; $false = "99.99.99" }[$env:APPVEYOR_BUILD_VERSION -ne $NULL]; @@ -60,6 +65,7 @@ Clean-Output Create-ArtifactDir Restore-Packages Publish-Archives($version) +Publish-DotNetTool($version) Execute-Tests Pop-Location diff --git a/README.md b/README.md index f12e6959..abd498f8 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,11 @@ The [Seq](https://datalust.co/seq) client command-line app. Supports logging (`s ## Getting started -Install or unzip the [release for your operating system](https://github.com/datalust/seqcli/releases). +Install or unzip the [release for your operating system](https://github.com/datalust/seqcli/releases). Or, if you have `dotnet` installed, `seqcli` can be installed as a global tool using: + +``` +dotnet tool install --global seqcli +``` To set a default server URL, run: @@ -443,6 +447,7 @@ seqcli signal import -i ./Exceptions.json | `-s`, `--server=VALUE` | The URL of the Seq server; by default the `connection.serverUrl` config value will be used | | `-a`, `--apikey=VALUE` | The API key to use when connecting to the server; by default the `connection.apiKey` config value will be used | | `--profile=VALUE` | A connection profile to use; by default the `connection.serverUrl` and `connection.apiKey` config values will be used | +| `--merge` | Update signals that have ids matching those in the imported data; the default is to always create new signals | ### `signal create` diff --git a/SeqCli.Runtime.targets b/SeqCli.Runtime.targets index ed4e9b56..8f9e8f8e 100644 --- a/SeqCli.Runtime.targets +++ b/SeqCli.Runtime.targets @@ -7,7 +7,7 @@ true $(DefineConstants);WINDOWS - + true true $(DefineConstants);LINUX;UNIX diff --git a/appveyor.yml b/appveyor.yml index dc077af7..bf52b72b 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,4 +1,4 @@ -version: 2020.2.{build} +version: 2020.3.{build} skip_tags: true image: - Visual Studio 2019 @@ -12,6 +12,7 @@ test: off artifacts: - path: artifacts/seqcli-*.zip - path: artifacts/seqcli-*.tar.gz +- path: artifacts/seqcli.*.nupkg for: - @@ -26,10 +27,19 @@ for: - ps: ./Build.ps1 -shortver "$($env:APPVEYOR_BUILD_VERSION)" deploy: + + - provider: NuGet + api_key: + secure: EVhhkxsbCh3YQw4IO7/d2paIjtjWiZGqs2pZl1l2Gkdz4DluOomXHA+wkPWHscj3 + skip_symbols: true + artifact: /seqcli\..*\.nupkg/ + on: + branch: main + - provider: GitHub auth_token: secure: Bo3ypKpKFxinjR9ShkNekNvkob2iklHJU+UlYyfHtcFFIAa58SV2TkEd0xWxz633 - artifact: /seqcli-.*\.(zip|tar\.gz)/ + artifact: /seqcli-.*\.(nupkg|zip|tar\.gz)/ tag: v$(appveyor_build_version) on: branch: main diff --git a/seqcli.sln b/seqcli.sln index 5fce66ea..c49689d6 100644 --- a/seqcli.sln +++ b/seqcli.sln @@ -24,10 +24,9 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{3587B633-0 EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "asset", "asset", "{438A0DA5-F3CF-4FCE-B43A-B6DA2981D4AE}" ProjectSection(SolutionItems) = preProject - asset\SeqCli-Icon-128px.ico = asset\SeqCli-Icon-128px.ico - asset\SeqCli-Icon-128px.png = asset\SeqCli-Icon-128px.png - asset\SeqCli-Icon.svg = asset\SeqCli-Icon.svg asset\SeqCliLicense.rtf = asset\SeqCliLicense.rtf + asset\SeqCli.ico = asset\SeqCli.ico + asset\SeqCli.png = asset\SeqCli.png EndProjectSection EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SeqCli", "src\SeqCli\SeqCli.csproj", "{EBDBEED2-A1BC-4269-B6B0-EA908CE4D687}" diff --git a/src/SeqCli/Cli/Commands/Signal/ImportCommand.cs b/src/SeqCli/Cli/Commands/Signal/ImportCommand.cs index ef65baf8..ceaaa842 100644 --- a/src/SeqCli/Cli/Commands/Signal/ImportCommand.cs +++ b/src/SeqCli/Cli/Commands/Signal/ImportCommand.cs @@ -32,7 +32,9 @@ class ImportCommand : Command readonly FileInputFeature _fileInputFeature; readonly EntityOwnerFeature _entityOwner; readonly ConnectionFeature _connection; - + + bool _merge; + readonly JsonSerializer _serializer = JsonSerializer.Create( new JsonSerializerSettings{ Converters = { new StringEnumConverter() } @@ -42,6 +44,12 @@ public ImportCommand(SeqConnectionFactory connectionFactory, SeqCliConfig config { if (config == null) throw new ArgumentNullException(nameof(config)); _connectionFactory = connectionFactory ?? throw new ArgumentNullException(nameof(connectionFactory)); + + Options.Add( + "merge", + "Update signals that have ids matching those in the imported data; the default is to always create new signals", + _ => _merge = true); + _fileInputFeature = Enable(new FileInputFeature("File to import")); _entityOwner = Enable(new EntityOwnerFeature("signal", "import")); _connection = Enable(); @@ -51,31 +59,55 @@ protected override async Task Run() { var connection = _connectionFactory.Connect(_connection); - using (var input = _fileInputFeature.OpenInput()) + using var input = _fileInputFeature.OpenInput(); + var line = await input.ReadLineAsync(); + while (line != null) { - var line = input.ReadLine(); - while (line != null) + if (!string.IsNullOrWhiteSpace(line)) { - if (!string.IsNullOrWhiteSpace(line)) + // Explicitly copying fields here ensures we don't try copying links or ids; for other + // entity types it'll ensure we notice places that "referential integrity" has to be + // maintained. + var src = _serializer.Deserialize(new JsonTextReader(new StringReader(line))); + if (src == null) continue; + + SignalEntity dest; + if (_merge) { - // Explicitly copying fields here ensures we don't try copying links or ids; for other - // entity types it'll ensure we notice places that "referential integrity" has to be - // maintained. - var src = _serializer.Deserialize(new JsonTextReader(new StringReader(line))); - var dest = await connection.Signals.TemplateAsync(); - dest.Title = src.Title; - dest.Description = src.Description; - dest.ExplicitGroupName = src.ExplicitGroupName; - dest.Grouping = src.Grouping; - dest.IsProtected = src.IsProtected; - dest.Filters = src.Filters; - dest.Columns = src.Columns; - dest.OwnerId = _entityOwner.OwnerId; - await connection.Signals.AddAsync(dest); + try + { + dest = await connection.Signals.FindAsync(src.Id); + } + catch (Exception) + { + dest = await connection.Signals.TemplateAsync(); + } + } + else + { + dest = await connection.Signals.TemplateAsync(); } + + dest.Title = src.Title; + dest.Description = src.Description; + dest.ExplicitGroupName = src.ExplicitGroupName; + dest.Grouping = src.Grouping; + dest.IsProtected = src.IsProtected; + dest.Filters = src.Filters; + dest.Columns = src.Columns; + dest.OwnerId = _entityOwner.OwnerId; - line = input.ReadLine(); + if (_merge && dest.Id != null) + { + await connection.Signals.UpdateAsync(dest); + } + else + { + await connection.Signals.AddAsync(dest); + } } + + line = await input.ReadLineAsync(); } return 0; diff --git a/src/SeqCli/SeqCli.csproj b/src/SeqCli/SeqCli.csproj index 8626d317..1f1a7669 100644 --- a/src/SeqCli/SeqCli.csproj +++ b/src/SeqCli/SeqCli.csproj @@ -4,12 +4,14 @@ netcoreapp3.1 seqcli ..\..\asset\SeqCli.ico - win-x64;linux-x64;osx-x64 + win-x64;linux-x64;linux-musl-x64;osx-x64 True True x64 8 + true + seqcli diff --git a/test/SeqCli.EndToEnd/SeqCli.EndToEnd.csproj b/test/SeqCli.EndToEnd/SeqCli.EndToEnd.csproj index efd79dcb..d73fd8e7 100644 --- a/test/SeqCli.EndToEnd/SeqCli.EndToEnd.csproj +++ b/test/SeqCli.EndToEnd/SeqCli.EndToEnd.csproj @@ -10,11 +10,13 @@ - PreserveNewest + + + \ No newline at end of file diff --git a/test/SeqCli.EndToEnd/Support/CaptiveProcess.cs b/test/SeqCli.EndToEnd/Support/CaptiveProcess.cs index c0f5c44f..1c44dced 100644 --- a/test/SeqCli.EndToEnd/Support/CaptiveProcess.cs +++ b/test/SeqCli.EndToEnd/Support/CaptiveProcess.cs @@ -9,6 +9,8 @@ namespace SeqCli.EndToEnd.Support public sealed class CaptiveProcess : ITestProcess, IDisposable { readonly bool _captureOutput; + readonly string _stopCommandFullExePath; + readonly string _stopCommandArgs; readonly Process _process; readonly ManualResetEvent _outputComplete = new ManualResetEvent(false); readonly ManualResetEvent _errorComplete = new ManualResetEvent(false); @@ -20,10 +22,14 @@ public CaptiveProcess( string fullExePath, string args = null, IDictionary environment = null, - bool captureOutput = true) + bool captureOutput = true, + string stopCommandFullExePath = null, + string stopCommandArgs = null) { if (fullExePath == null) throw new ArgumentNullException(nameof(fullExePath)); _captureOutput = captureOutput; + _stopCommandFullExePath = stopCommandFullExePath; + _stopCommandArgs = stopCommandArgs; var startInfo = new ProcessStartInfo { @@ -109,6 +115,23 @@ public void Dispose() { _process.Kill(); WaitForExit(); + if (_stopCommandFullExePath != null) + { + var stopCommandStartInfo = new ProcessStartInfo + { + UseShellExecute = false, + WindowStyle = ProcessWindowStyle.Hidden, + RedirectStandardError = true, + RedirectStandardOutput = true, + CreateNoWindow = true, + ErrorDialog = false, + FileName = _stopCommandFullExePath, + Arguments = _stopCommandArgs ?? "" + }; + + using var stopCommandProcess = Process.Start(stopCommandStartInfo); + stopCommandProcess?.WaitForExit(); + } } catch { diff --git a/test/SeqCli.EndToEnd/Support/TestConfiguration.cs b/test/SeqCli.EndToEnd/Support/TestConfiguration.cs index 618f0f2c..2b1536c9 100644 --- a/test/SeqCli.EndToEnd/Support/TestConfiguration.cs +++ b/test/SeqCli.EndToEnd/Support/TestConfiguration.cs @@ -6,7 +6,9 @@ namespace SeqCli.EndToEnd.Support { public class TestConfiguration { - public string ServerListenUrl { get; } = "http://localhost:9989"; + public int ServerListenPort { get; } = 9989; + + public string ServerListenUrl => $"http://localhost:{ServerListenPort}"; string EquivalentBaseDirectory { get; } = AppDomain.CurrentDomain.BaseDirectory .Replace(Path.Combine("test", "SeqCli.EndToEnd"), Path.Combine("src", "SeqCli")); @@ -30,7 +32,10 @@ public CaptiveProcess SpawnServerProcess(string storagePath) if (storagePath == null) throw new ArgumentNullException(nameof(storagePath)); var commandWithArgs = $"run --listen=\"{ServerListenUrl}\" --storage=\"{storagePath}\""; - + if (Environment.GetEnvironmentVariable("ENDTOEND_USE_DOCKER_SEQ") == "Y") + { + return new CaptiveProcess("docker", $"run --name seq -it --rm -e ACCEPT_EULA=Y -p {ServerListenPort}:80 datalust/seq:latest", stopCommandFullExePath: "docker", stopCommandArgs: "stop seq"); + } return new CaptiveProcess("seq", commandWithArgs); } }