diff --git a/.travis.yml b/.travis.yml index 527db46..2926e1c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,25 +4,20 @@ dotnet: 2.0.0 install: - export FrameworkPathOverride=$(dirname $(which mono))/../lib/mono/4.5/ - #- curl -L -o nuget.exe https://dist.nuget.org/win-x86-commandline/latest/nuget.exe - dotnet restore script: - - dotnet build -c Debug --no-restore -v m + #- dotnet build -c Debug --no-restore -v m #- dotnet test Jenkins.Net.Tests/Jenkins.Net.Tests.csproj -c Debug --no-build --filter Category=Unit - - dotnet pack Jenkins.Net/Jenkins.Net.csproj -c Debug --no-build --no-restore -o bin + - dotnet pack Jenkins.Net/Jenkins.Net.csproj -c Debug -o bin -v m + - dotnet publish Jenkins.Net.Console/Jenkins.Net.Console.csproj -c Release -f net45 -o bin -v m -p:PublishSingleFile=true deploy: provider: releases api_key: $GITHUB_APIKEY - file: "Jenkins.Net/bin/jenkinsnet.*.nupkg" + file: + - "Jenkins.Net/bin/jenkinsnet.*.nupkg" + - "Jenkins.Net.Console/bin/Jenkins.Net.Console.exe" skip_cleanup: true on: tags: true - -# deploy: -# skip_cleanup: true -# provider: script -# script: dotnet nuget push Jenkins.Net/bin/jenkinsnet.*.nupkg -k $NUGET_APIKEY -s $NUGET_SOURCE -# on: -# branch: master diff --git a/Jenkins.Net.Console/Arguments.cs b/Jenkins.Net.Console/Arguments.cs new file mode 100644 index 0000000..71e1fb6 --- /dev/null +++ b/Jenkins.Net.Console/Arguments.cs @@ -0,0 +1,52 @@ +using System; +using JenkinsNET.Console.Internal; +using System.Threading; +using System.Threading.Tasks; +using SysConsole = System.Console; + +namespace JenkinsNET.Console +{ + internal class Arguments : ArgumentsGroup + { + public RunArguments RunGroup {get;} + public Actions Action {get; private set;} + + + public Arguments() + { + RunGroup = new RunArguments(); + + Map("run").ToGroup(RunGroup, () => Action = Actions.Run); + Map("-help", "-?").ToAction(v => Action = Actions.Help); + } + + public Task RunAsync(CancellationToken token = default) + { + switch (Action) { + case Actions.Run: + return RunGroup.RunAsync(token); + default: + PrintHelp(); + break; + } + + return Task.CompletedTask; + } + + private static void PrintHelp() + { + SysConsole.ResetColor(); + SysConsole.WriteLine("Arguments:"); + SysConsole.ForegroundColor = ConsoleColor.White; + SysConsole.WriteLine(" run ..."); + SysConsole.WriteLine(" -help | -?"); + } + + public enum Actions + { + Undefined, + Run, + Help, + } + } +} diff --git a/Jenkins.Net.Console/Internal/ArgumentsGroup.cs b/Jenkins.Net.Console/Internal/ArgumentsGroup.cs new file mode 100644 index 0000000..f9fe6b2 --- /dev/null +++ b/Jenkins.Net.Console/Internal/ArgumentsGroup.cs @@ -0,0 +1,107 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using SysConsole = System.Console; + +namespace JenkinsNET.Console.Internal +{ + internal class ArgumentsGroup + { + private readonly Dictionary groupMap; + private readonly Dictionary> actionMap; + + + public ArgumentsGroup() + { + groupMap = new Dictionary(StringComparer.OrdinalIgnoreCase); + actionMap = new Dictionary>(StringComparer.OrdinalIgnoreCase); + } + + public void Parse(string[] args) + { + for (var i = 0; i < args.Length; i++) { + var arg = args[i]; + + if (i == 0 && groupMap.TryGetValue(arg, out var groupItem)) { + var subArgs = args.Skip(1).ToArray(); + groupItem.Action?.Invoke(); + groupItem.Group.Parse(subArgs); + return; + } + + var x = arg.IndexOfAny(new[] {'=', ':'}); + var key = x >= 0 ? arg.Substring(0, x) : arg; + var value = x >= 0 ? arg.Substring(x + 1) : null; + + if (actionMap.TryGetValue(key, out var action)) { + action.Invoke(value); + continue; + } + + SysConsole.ForegroundColor = ConsoleColor.DarkYellow; + SysConsole.WriteLine($"Unknown argument '{arg}'!"); + } + } + + public ArgumentActionBuilder Map(params string[] args) + { + return new ArgumentActionBuilder(args, this); + } + + public class ArgumentActionBuilder + { + private readonly string[] arguments; + private readonly ArgumentsGroup parentGroup; + + + public ArgumentActionBuilder(string[] arguments, ArgumentsGroup parentGroup) + { + this.arguments = arguments; + this.parentGroup = parentGroup; + } + + public ArgumentActionBuilder ToGroup(ArgumentsGroup group, Action action = null) + { + foreach (var arg in arguments) + parentGroup.groupMap[arg] = new ArgumentGroupAction(group, action); + + return this; + } + + public ArgumentActionBuilder ToAction(Action action) + { + foreach (var arg in arguments) + parentGroup.actionMap[arg] = action; + + return this; + } + + public ArgumentActionBuilder ToAction(Action action, T defaultValue = default) + { + foreach (var arg in arguments) { + parentGroup.actionMap[arg] = value => { + var valueT = value != null + ? (T)Convert.ChangeType(value, typeof(T)) + : defaultValue; + + action.Invoke(valueT); + }; + } + + return this; + } + } + + private class ArgumentGroupAction + { + public ArgumentsGroup Group {get;} + public Action Action {get;} + + public ArgumentGroupAction(ArgumentsGroup group, Action action) + { + this.Group = group; + this.Action = action; + } + } + } +} diff --git a/Jenkins.Net.Console/Internal/ExceptionExtensions.cs b/Jenkins.Net.Console/Internal/ExceptionExtensions.cs new file mode 100644 index 0000000..6e5fa1a --- /dev/null +++ b/Jenkins.Net.Console/Internal/ExceptionExtensions.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace JenkinsNET.Console.Internal +{ + internal static class ExceptionExtensions + { + public static IEnumerable UnfoldExceptions(this Exception error) + { + var e = error; + while (e != null) { + yield return e; + e = e.InnerException; + } + } + + public static string UnfoldMessages(this Exception error) + { + var errors = error.UnfoldExceptions().Select(x => x.Message); + return string.Join(" ", errors); + } + } +} diff --git a/Jenkins.Net.Console/Jenkins.Net.Console.csproj b/Jenkins.Net.Console/Jenkins.Net.Console.csproj new file mode 100644 index 0000000..527165a --- /dev/null +++ b/Jenkins.Net.Console/Jenkins.Net.Console.csproj @@ -0,0 +1,21 @@ + + + Exe + net472 + JenkinsNET.Console + 1.0.5 + 1.0.5 + 1.0.5 + latest + JenkinsNet.Console + + + + + + + + + + + diff --git a/Jenkins.Net.Console/Program.cs b/Jenkins.Net.Console/Program.cs new file mode 100644 index 0000000..c9ccf73 --- /dev/null +++ b/Jenkins.Net.Console/Program.cs @@ -0,0 +1,37 @@ +using JenkinsNET.Console.Internal; +using System; +using System.Threading.Tasks; +using SysConsole = System.Console; + +namespace JenkinsNET.Console +{ + internal static class Program + { + public static async Task Main(string[] args) + { + SysConsole.ForegroundColor = ConsoleColor.White; + SysConsole.WriteLine("Jenkins.NET Console"); + SysConsole.WriteLine(); + + try { + var arguments = new Arguments(); + arguments.Parse(args); + + await arguments.RunAsync(); + + return 0; + } + catch (Exception error) { + SysConsole.ForegroundColor = ConsoleColor.Red; + SysConsole.WriteLine("[ERROR] "); + SysConsole.ForegroundColor = ConsoleColor.DarkRed; + SysConsole.WriteLine(error.UnfoldMessages()); + + return 1; + } + finally { + SysConsole.ResetColor(); + } + } + } +} diff --git a/Jenkins.Net.Console/Properties/launchSettings.json b/Jenkins.Net.Console/Properties/launchSettings.json new file mode 100644 index 0000000..6e793c1 --- /dev/null +++ b/Jenkins.Net.Console/Properties/launchSettings.json @@ -0,0 +1,8 @@ +{ + "profiles": { + "Jenkins.Net.Console": { + "commandName": "Project", + "commandLineArgs": "run -job=\"Test Job\" -p:Arg1=Hello -p:Arg2=\"World!\"" + } + } +} \ No newline at end of file diff --git a/Jenkins.Net.Console/RunArguments.cs b/Jenkins.Net.Console/RunArguments.cs new file mode 100644 index 0000000..88f3db1 --- /dev/null +++ b/Jenkins.Net.Console/RunArguments.cs @@ -0,0 +1,88 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using JenkinsNET.Console.Internal; +using System.Threading; +using System.Threading.Tasks; +using JenkinsNET.Utilities; +using SysConsole = System.Console; + +namespace JenkinsNET.Console +{ + internal class RunArguments : ArgumentsGroup + { + public Dictionary JobParameters {get;} + public string JobName {get; private set;} + public bool ShowHelp {get; private set;} + + + public RunArguments() + { + Map("-job").ToAction(v => JobName = v); + Map("-p").ToAction(AddProperty); + Map("-help", "-?").ToAction(v => ShowHelp = v, false); + + JobParameters = new Dictionary(); + } + + public async Task RunAsync(CancellationToken token) + { + if (ShowHelp) { + PrintHelp(); + return; + } + + SysConsole.ForegroundColor = ConsoleColor.Cyan; + SysConsole.WriteLine($"Starting Job \"{JobName}\"..."); + + var client = new JenkinsClient { + BaseUrl = "http://localhost:8080/", + // TODO: Load from local file + }; + + var runner = new JenkinsJobRunner(client) { + MonitorConsoleOutput = true, + // TODO: Setup + }; + + runner.StatusChanged += () => { + SysConsole.ForegroundColor = ConsoleColor.White; + SysConsole.WriteLine($"[STATUS] {runner.Status}"); + }; + + runner.ConsoleOutputChanged += text => { + SysConsole.ResetColor(); + SysConsole.Write(text); + }; + + if (JobParameters.Any()) { + await runner.RunWithParametersAsync(JobName, JobParameters, token); + } + else { + await runner.RunAsync(JobName, token); + } + } + + private void AddProperty(string property) + { + var x = property.IndexOf('='); + if (x < 0) throw new ApplicationException($"No value specified in property string '{property}'!"); + + var key = property.Substring(0, x); + var value = property.Substring(x + 1); + JobParameters[key] = value; + } + + private static void PrintHelp() + { + SysConsole.ResetColor(); + SysConsole.Write("Arguments: "); + SysConsole.ForegroundColor = ConsoleColor.DarkCyan; + SysConsole.WriteLine("run"); + SysConsole.ForegroundColor = ConsoleColor.White; + SysConsole.WriteLine(" -job "); + SysConsole.WriteLine(" -p:="); + SysConsole.WriteLine(" -help | -?"); + } + } +} diff --git a/Jenkins.Net.sln b/Jenkins.Net.sln index b2b7a6a..256dfff 100644 --- a/Jenkins.Net.sln +++ b/Jenkins.Net.sln @@ -7,6 +7,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jenkins.Net", "Jenkins.Net\ EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jenkins.Net.Tests", "Jenkins.Net.Tests\Jenkins.Net.Tests.csproj", "{C39D7FDD-F85E-4FEF-8E30-A23B04DD1787}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jenkins.Net.Console", "Jenkins.Net.Console\Jenkins.Net.Console.csproj", "{DC01C670-176F-4587-B574-7FA73795F335}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -21,6 +23,10 @@ Global {C39D7FDD-F85E-4FEF-8E30-A23B04DD1787}.Debug|Any CPU.Build.0 = Debug|Any CPU {C39D7FDD-F85E-4FEF-8E30-A23B04DD1787}.Release|Any CPU.ActiveCfg = Release|Any CPU {C39D7FDD-F85E-4FEF-8E30-A23B04DD1787}.Release|Any CPU.Build.0 = Release|Any CPU + {DC01C670-176F-4587-B574-7FA73795F335}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DC01C670-176F-4587-B574-7FA73795F335}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DC01C670-176F-4587-B574-7FA73795F335}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DC01C670-176F-4587-B574-7FA73795F335}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/Jenkins.Net/Jenkins.Net.csproj b/Jenkins.Net/Jenkins.Net.csproj index 88db5c6..5ae3860 100644 --- a/Jenkins.Net/Jenkins.Net.csproj +++ b/Jenkins.Net/Jenkins.Net.csproj @@ -1,8 +1,6 @@  - net40;net45;netstandard2.0 - - + netstandard2.0;net40;net45 JenkinsNET jenkinsnet null511 diff --git a/Jenkins.Net/Utilities/JenkinsJobRunner.cs b/Jenkins.Net/Utilities/JenkinsJobRunner.cs index 2110c61..61c1d4e 100644 --- a/Jenkins.Net/Utilities/JenkinsJobRunner.cs +++ b/Jenkins.Net/Utilities/JenkinsJobRunner.cs @@ -28,10 +28,11 @@ namespace JenkinsNET.Utilities /// public class JenkinsJobRunner { - public JenkinsClient Client {get;} protected ProgressiveTextReader textReader; protected bool isJobStarted; + public JenkinsClient Client {get;} + /// /// Occurs when the status of the running Jenkins Job changes. /// @@ -127,7 +128,7 @@ public JenkinsBuildBase Run(string jobName) /// /// /// - public async Task RunAsync(string jobName) + public async Task RunAsync(string jobName, CancellationToken token = default) { if (isJobStarted) throw new JenkinsNetException("This JobRunner instance has already been started! Separate JenkinsJobRunner instances are required to run multiple jobs."); isJobStarted = true; @@ -135,12 +136,10 @@ public async Task RunAsync(string jobName) SetStatus(JenkinsJobStatus.Pending); var queueStartTime = DateTime.Now; - var buildResult = await Client.Jobs.BuildAsync(jobName); + var buildResult = await Client.Jobs.BuildAsync(jobName, token); + if (buildResult == null) throw new JenkinsJobBuildException("An empty build response was returned!"); - if (buildResult == null) - throw new JenkinsJobBuildException("An empty build response was returned!"); - - return await ProcessAsync(jobName, buildResult, queueStartTime); + return await ProcessAsync(jobName, buildResult, queueStartTime, token); } #endif @@ -161,9 +160,7 @@ public JenkinsBuildBase RunWithParameters(string jobName, IDictionary /// /// - public async Task RunWithParametersAsync(string jobName, IDictionary jobParameters) + public async Task RunWithParametersAsync(string jobName, IDictionary jobParameters, CancellationToken token = default) { if (isJobStarted) throw new JenkinsNetException("This JobRunner instance has already been started! Separate JenkinsJobRunner instances are required to run multiple jobs."); isJobStarted = true; @@ -185,12 +182,10 @@ public async Task RunWithParametersAsync(string jobName, IDict SetStatus(JenkinsJobStatus.Pending); var queueStartTime = DateTime.Now; - var buildResult = await Client.Jobs.BuildWithParametersAsync(jobName, jobParameters); - - if (buildResult == null) - throw new JenkinsJobBuildException("An empty build response was returned!"); + var buildResult = await Client.Jobs.BuildWithParametersAsync(jobName, jobParameters, token); + if (buildResult == null) throw new JenkinsJobBuildException("An empty build response was returned!"); - return await ProcessAsync(jobName, buildResult, queueStartTime); + return await ProcessAsync(jobName, buildResult, queueStartTime, token); } #endif @@ -251,7 +246,7 @@ private JenkinsBuildBase Process(string jobName, JenkinsBuildResult buildResult, /// /// /// - private async Task ProcessAsync(string jobName, JenkinsBuildResult buildResult, DateTime queueStartTime) + private async Task ProcessAsync(string jobName, JenkinsBuildResult buildResult, DateTime queueStartTime, CancellationToken token = default) { QueueItemNumber = buildResult.GetQueueItemNumber(); if (!QueueItemNumber.HasValue) throw new JenkinsNetException("Queue-Item number not found!"); @@ -259,16 +254,17 @@ private async Task ProcessAsync(string jobName, JenkinsBuildRe SetStatus(JenkinsJobStatus.Queued); while (true) { + token.ThrowIfCancellationRequested(); if (!QueueItemNumber.HasValue) throw new JenkinsNetException("Queue-Item number not found!"); - var queueItem = await Client.Queue.GetItemAsync(QueueItemNumber.Value); + var queueItem = await Client.Queue.GetItemAsync(QueueItemNumber.Value, token); BuildNumber = queueItem?.Executable?.Number; if (BuildNumber.HasValue) break; if (QueueTimeout > 0 && DateTime.Now.Subtract(queueStartTime).TotalSeconds > QueueTimeout) throw new JenkinsNetException("Timeout occurred while waiting for build to start!"); - await Task.Delay(PollInterval); + await Task.Delay(PollInterval, token); } SetStatus(JenkinsJobStatus.Building); @@ -279,22 +275,25 @@ private async Task ProcessAsync(string jobName, JenkinsBuildRe JenkinsBuildBase buildItem = null; while (string.IsNullOrEmpty(buildItem?.Result)) { + token.ThrowIfCancellationRequested(); if (!BuildNumber.HasValue) throw new JenkinsNetException("Build number not found!"); - buildItem = await Client.Builds.GetAsync(jobName, BuildNumber.Value.ToString()); + buildItem = await Client.Builds.GetAsync(jobName, BuildNumber.Value.ToString(), token); if (!string.IsNullOrEmpty(buildItem?.Result)) break; if (BuildTimeout > 0 && DateTime.Now.Subtract(buildStartTime).TotalSeconds > BuildTimeout) throw new JenkinsNetException("Timeout occurred while waiting for build to complete!"); if (MonitorConsoleOutput && !textReader.IsComplete) - await textReader.UpdateAsync(); + await textReader.UpdateAsync(token); - await Task.Delay(PollInterval); + await Task.Delay(PollInterval, token); } while (MonitorConsoleOutput && !textReader.IsComplete) { - await textReader.UpdateAsync(); + token.ThrowIfCancellationRequested(); + + await textReader.UpdateAsync(token); } SetStatus(JenkinsJobStatus.Complete); diff --git a/Jenkins.Net/Utilities/ProgressiveTextReader.cs b/Jenkins.Net/Utilities/ProgressiveTextReader.cs index c002b1e..7b1b597 100644 --- a/Jenkins.Net/Utilities/ProgressiveTextReader.cs +++ b/Jenkins.Net/Utilities/ProgressiveTextReader.cs @@ -1,6 +1,7 @@ using System; #if NET_ASYNC +using System.Threading; using System.Threading.Tasks; #endif @@ -78,11 +79,11 @@ public void Update() /// Retrieves and appends any additional text returned /// by the running Jenkins Job asynchronously. /// - public async Task UpdateAsync() + public async Task UpdateAsync(CancellationToken token = default) { if (IsComplete) return; - var result = await client.Builds.GetProgressiveTextAsync(jobName, buildNumber, readPos); + var result = await client.Builds.GetProgressiveTextAsync(jobName, buildNumber, readPos, token); if (result.Size > 0) { Text += result.Text;