diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000000..fd74dc7cb9 --- /dev/null +++ b/LICENSE @@ -0,0 +1,20 @@ +The MIT License (MIT) +Copyright (c) Microsoft Corporation + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/src/Agent.Listener/Configuration/ConfigurationStore.cs b/src/Agent.Listener/Configuration/ConfigurationStore.cs index e00ecdacf1..06662d98d2 100644 --- a/src/Agent.Listener/Configuration/ConfigurationStore.cs +++ b/src/Agent.Listener/Configuration/ConfigurationStore.cs @@ -3,6 +3,7 @@ using System.Diagnostics; using System.IO; using Newtonsoft.Json; +using Microsoft.VisualStudio.Services.Agent.Util; namespace Microsoft.VisualStudio.Services.Agent.Configuration { @@ -83,7 +84,7 @@ public CredentialData GetCredentials() { if (_creds == null) { - _creds = Load(_credFilePath); + _creds = IOUtil.LoadObject(_credFilePath); } return _creds; @@ -93,7 +94,7 @@ public AgentSettings GetSettings() { if (_settings == null) { - _settings = Load(_configFilePath); + _settings = IOUtil.LoadObject(_configFilePath); } return _settings; @@ -102,35 +103,14 @@ public AgentSettings GetSettings() public void SaveCredential(CredentialData credential) { Trace.Info("Saving {0} credential @ {1}", credential.Scheme, _credFilePath); - Save(credential, _credFilePath); - Trace.Info("Saved."); + IOUtil.SaveObject(credential, _credFilePath); + Trace.Info("Credentials Saved."); } public void SaveSettings(AgentSettings settings) { - Save(settings, _configFilePath); - } - - private void Save(Object obj, string path) - { - Trace.Info("Saving to {0}", path); - - string json = JsonConvert.SerializeObject(obj, Formatting.Indented); - File.WriteAllText (path, json); - Trace.Info("Written."); - } - - private T Load(string path) - { - // TODO: convert many of these Info statements to Verbose - - Trace.Info("Loading config from {0}", path); - - string json = File.ReadAllText(path); - Trace.Info("Loaded. Length: {0}", json.Length); - T config = JsonConvert.DeserializeObject(json); - Trace.Info("Loaded."); - return config; + IOUtil.SaveObject(settings, _configFilePath); + Trace.Info("Settings Saved."); } } } \ No newline at end of file diff --git a/src/Agent.Listener/Configuration/AgentCredential.cs b/src/Agent.Listener/Configuration/CredentialProvider.cs similarity index 63% rename from src/Agent.Listener/Configuration/AgentCredential.cs rename to src/Agent.Listener/Configuration/CredentialProvider.cs index a81e7d7c63..337a9cc7f4 100644 --- a/src/Agent.Listener/Configuration/AgentCredential.cs +++ b/src/Agent.Listener/Configuration/CredentialProvider.cs @@ -1,6 +1,8 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using Microsoft.VisualStudio.Services.Client; +using Microsoft.VisualStudio.Services.Common; namespace Microsoft.VisualStudio.Services.Agent.Configuration { @@ -28,8 +30,7 @@ public CredentialProvider(string scheme) public CredentialData CredentialData { get; set; } - // TODO: (bryanmac) abstract GetVSSCredential which knows how to instantiate based off data - + public abstract VssCredentials GetVssCredentials(IHostContext context); public abstract void ReadCredential(IHostContext context, Dictionary args, bool enforceSupplied); } @@ -37,6 +38,27 @@ public sealed class PersonalAccessToken : CredentialProvider { public PersonalAccessToken(): base("PAT") {} + public override VssCredentials GetVssCredentials(IHostContext context) + { + TraceSource trace = context.GetTrace("PersonalAccessToken"); + trace.Info("GetVssCredentials()"); + + if (CredentialData == null || !CredentialData.Data.ContainsKey("token")) + { + throw new InvalidOperationException("Must call ReadCredential first."); + } + + string token = CredentialData.Data["token"]; + trace.Info("token retrieved: {0} chars", token.Length); + + // PAT uses a basic credential + VssBasicCredential loginCred = new VssBasicCredential("VstsAgent", token); + VssCredentials creds = new VssClientCredentials(loginCred); + trace.Verbose("cred created"); + + return creds; + } + public override void ReadCredential(IHostContext context, Dictionary args, bool enforceSupplied) { TraceSource trace = context.GetTrace("PersonalAccessToken"); @@ -59,7 +81,31 @@ public override void ReadCredential(IHostContext context, Dictionary args, bool enforceSupplied) { var wizard = context.GetService(); @@ -81,5 +127,5 @@ public override void ReadCredential(IHostContext context, Dictionary RunAsync(IHostContext context) private static void PrintUsage() { - string usage = @" -usage: -Agent.Listener [command(s)] [arguments] [options] - -It is common to just run Agent or Agent.Listener with no arguments for an interactive configuration. -You will be prompted and walked through all options. - - -Commands: ------------------------------------------------------------------------------ -(none) Interactively configure and then run the agent. - You will be prompted for data. -configure Configure the agent and exit. -unconfigure Unconfigure the agent. -run Runs the agent interactively. must be configured. - - -Options: ------------------------------------------------------------------------------ ---unattend Unattended config. You will not be prompted. - All answers must be supplied on cli. ---nostart Do not start the agent after interactive configuration. ---auth Auth type. Valid options are PAT (Personal Access Token) and - ALT (Alternate Credentials) - - -Options by Auth Type: ------------------------------------------------------------------------------ -PAT ---token Personal Access Token data. Best to paste value in. - -ALT ---username alternate username ---password alternate password - "; + string usage = StringUtil.Loc("ListenerHelp"); Console.WriteLine(usage); + Console.WriteLine(StringUtil.Loc("Test", "Hello")); Environment.Exit(0); } } diff --git a/src/Agent.Listener/Worker.cs b/src/Agent.Listener/Worker.cs index ac29bde98d..4de3556978 100644 --- a/src/Agent.Listener/Worker.cs +++ b/src/Agent.Listener/Worker.cs @@ -60,7 +60,7 @@ public Worker() public void LaunchProcess(String pipeHandleOut, String pipeHandleIn, string workingFolder) { - string workerFileName = Path.Combine(AssemblyUtil.AssemblyDirectory, WorkerProcessName); + string workerFileName = Path.Combine(IOUtil.GetBinPath(), WorkerProcessName); _processInvoker = HostContext.GetService(); _processInvoker.Exited += _processInvoker_Exited; State = WorkerState.Starting; diff --git a/src/Agent.Listener/WorkerManager.cs b/src/Agent.Listener/WorkerManager.cs index 8918ce7d5b..c45fd9a720 100644 --- a/src/Agent.Listener/WorkerManager.cs +++ b/src/Agent.Listener/WorkerManager.cs @@ -28,7 +28,7 @@ public async Task Run(JobRequestMessage jobRequestMessage) _jobsInProgress[jobRequestMessage.JobId] = worker; worker.ProcessChannel.StartServer( (pipeHandleOut, pipeHandleIn) => { - worker.LaunchProcess(pipeHandleOut, pipeHandleIn, AssemblyUtil.AssemblyDirectory); + worker.LaunchProcess(pipeHandleOut, pipeHandleIn, IOUtil.GetBinPath()); } ); await worker.ProcessChannel.SendAsync(jobRequestMessage, HostContext.CancellationToken); diff --git a/src/Agent.Listener/_project.json b/src/Agent.Listener/_project.json index c53984c1c6..2762d6d597 100644 --- a/src/Agent.Listener/_project.json +++ b/src/Agent.Listener/_project.json @@ -32,6 +32,8 @@ "dependencies": { "NETStandard.Library": "1.0.0-rc3-23721", "Microsoft.VisualStudio.Services.Agent": "", + "Microsoft.VisualStudio.Services.Client": "0.5.0", + "Microsoft.VisualStudio.Services.Common": "0.5.0", "Newtonsoft.Json": "7.0.1", "System.Diagnostics.TraceSource": "4.0.0-rc3-23721" }, diff --git a/src/Microsoft.VisualStudio.Services.Agent/Service.cs b/src/Microsoft.VisualStudio.Services.Agent/AgentService.cs similarity index 93% rename from src/Microsoft.VisualStudio.Services.Agent/Service.cs rename to src/Microsoft.VisualStudio.Services.Agent/AgentService.cs index 6c6812e4dc..b8aa7d7d9d 100644 --- a/src/Microsoft.VisualStudio.Services.Agent/Service.cs +++ b/src/Microsoft.VisualStudio.Services.Agent/AgentService.cs @@ -20,6 +20,8 @@ public interface IAgentService public class AgentService { + public static string Version { get { return "2.0.0"; }} + protected IHostContext HostContext { get; private set; } protected TraceSource Trace { get; private set; } diff --git a/src/Microsoft.VisualStudio.Services.Agent/Util/ApiUtil.cs b/src/Microsoft.VisualStudio.Services.Agent/Util/ApiUtil.cs new file mode 100644 index 0000000000..79e24bc193 --- /dev/null +++ b/src/Microsoft.VisualStudio.Services.Agent/Util/ApiUtil.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Net.Http.Headers; +using Microsoft.VisualStudio.Services.Agent; +using Microsoft.VisualStudio.Services.Client; +using Microsoft.VisualStudio.Services.Common; +using Microsoft.VisualStudio.Services.WebApi; + +namespace Microsoft.VisualStudio.Services.Agent.Util +{ + public static class ApiUtil + { + public static VssConnection CreateConnection(Uri serverUri, VssCredentials credentials) + { + VssClientHttpRequestSettings settings = VssClientHttpRequestSettings.Default.Clone(); + settings.MaxRetryRequest = 5; + + var headerValues = new List(); + headerValues.Add(new ProductInfoHeaderValue("VstsAgent", AgentService.Version)); + VssConnection connection = new VssConnection(serverUri, credentials, settings); + return connection; + //connection.ConnectAsync().SyncResult(); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.VisualStudio.Services.Agent/Util/AssemblyDirectory.cs b/src/Microsoft.VisualStudio.Services.Agent/Util/AssemblyDirectory.cs deleted file mode 100644 index 27f534435e..0000000000 --- a/src/Microsoft.VisualStudio.Services.Agent/Util/AssemblyDirectory.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System.IO; -using System.Reflection; - -namespace Microsoft.VisualStudio.Services.Agent.Util -{ - public static class AssemblyUtil - { - public static string AssemblyDirectory - { - get - { - return Path.GetDirectoryName(typeof(AssemblyUtil).GetTypeInfo().Assembly.Location); - } - } - } -} diff --git a/src/Microsoft.VisualStudio.Services.Agent/Util/IOUtil.cs b/src/Microsoft.VisualStudio.Services.Agent/Util/IOUtil.cs new file mode 100644 index 0000000000..92ce3f725d --- /dev/null +++ b/src/Microsoft.VisualStudio.Services.Agent/Util/IOUtil.cs @@ -0,0 +1,28 @@ +using System; +using System.IO; +using Newtonsoft.Json; + +namespace Microsoft.VisualStudio.Services.Agent.Util +{ + public static class IOUtil + { + public static void SaveObject(Object obj, string path) + { + string json = JsonConvert.SerializeObject(obj, Formatting.Indented); + File.WriteAllText (path, json); + } + + public static T LoadObject(string path) + { + string json = File.ReadAllText(path); + T obj = JsonConvert.DeserializeObject(json); + return obj; + } + + public static string GetBinPath() + { + var currentAssemblyLocation = System.Reflection.Assembly.GetEntryAssembly().Location; + return new DirectoryInfo(currentAssemblyLocation).Parent.FullName.ToString(); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.VisualStudio.Services.Agent/Util/StringUtil.cs b/src/Microsoft.VisualStudio.Services.Agent/Util/StringUtil.cs index 14e8dfc9a6..fa7fe9694b 100644 --- a/src/Microsoft.VisualStudio.Services.Agent/Util/StringUtil.cs +++ b/src/Microsoft.VisualStudio.Services.Agent/Util/StringUtil.cs @@ -1,11 +1,18 @@ using System; +using System.Collections.Generic; using System.Globalization; - +using System.IO; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Newtonsoft.Json.Linq; namespace Microsoft.VisualStudio.Services.Agent.Util { public static class StringUtil { + private static Dictionary _locStrings; + public static String Format(String format, params Object[] args) { if (String.IsNullOrEmpty(format)) @@ -30,5 +37,66 @@ public static String Format(String format, params Object[] args) return message; } + + public static string Loc(string locKey, params Object[] args) + { + // + // TODO: Replace this custom little loc impl with proper one after confirming OSX/Linux support + // + string locStr = locKey; + + try + { + EnsureLoaded(); + + if (_locStrings.ContainsKey(locKey)) + { + Object item = _locStrings[locKey]; + + Type t = item.GetType(); + Console.WriteLine(t.ToString()); + + if (t == typeof(string)) + { + string str = _locStrings[locKey].ToString(); + locStr = StringUtil.Format(str, args); + } + else if (t == typeof(JArray)) + { + string[] lines = ((JArray)item).ToObject(); + StringBuilder sb = new StringBuilder(); + foreach (string line in lines) + { + sb.Append(line); + sb.Append(Environment.NewLine); + //locStr += (line + Environment.NewLine); + } + locStr = sb.ToString(); + } + } + else + { + locStr = StringUtil.Format("notFound:{0}", locKey); + } + } + catch (Exception e) + { + // loc strings shouldn't take down agent. any failures returns loc key + } + + return locStr; + } + + private static void EnsureLoaded() + { + if (_locStrings == null) + { + string stringsPath = Path.Combine(IOUtil.GetBinPath(), + CultureInfo.CurrentCulture.Name, + "strings.json"); + + _locStrings = IOUtil.LoadObject>(stringsPath); + } + } } } \ No newline at end of file diff --git a/src/Microsoft.VisualStudio.Services.Agent/_project.json b/src/Microsoft.VisualStudio.Services.Agent/_project.json index 1763279df4..82edd00867 100644 --- a/src/Microsoft.VisualStudio.Services.Agent/_project.json +++ b/src/Microsoft.VisualStudio.Services.Agent/_project.json @@ -32,6 +32,7 @@ "Microsoft.VisualStudio.Services.Common": "0.5.0", "Microsoft.VisualStudio.Services.WebApi": "0.5.0", "Microsoft.TeamFoundation.DistributedTask.WebApi": "0.5.0", + "Newtonsoft.Json": "7.0.1", "System.Diagnostics.TraceSource": "4.0.0-rc3-23721", "System.Diagnostics.TextWriterTraceListener": "4.0.0-rc3-23721", "System.IO": "4.1.0-rc3-23721", diff --git a/src/Misc/layoutbin/en-US/strings.json b/src/Misc/layoutbin/en-US/strings.json new file mode 100644 index 0000000000..90647a94e0 --- /dev/null +++ b/src/Misc/layoutbin/en-US/strings.json @@ -0,0 +1,70 @@ +{ + "Test": "TestValue {0} World", + "ListenerHelp": [ +"Visual Studio Team Services Agent", +"Copyright (c) Microsoft Corporation", +"", +"Run from the directory above the bin agent folder to allow for updates", +"", +"Alternatively, you can run the convenience cmd or shell script wrappers.", +"Arguments will be passed along", +"", +"Win : run.cmd [command(s)] [arguments] [options]", +"Unix : ./run.sh [command(s)] [arguments] [options]", +"", +"Win : configure.cmd [command(s)] [arguments] [options]", +"Unix : ./configure.sh [command(s)] [arguments] [options]", +"", +"usage:", +"bin/Agent.Listener [command(s)] [arguments] [options]", +"", +"It is common to just run Agent or Agent.Listener with no arguments for an interactive configuration.", +"You will be prompted and walked through all options.", +"", +"", +"Commands:", +"-----------------------------------------------------------------------------", +"(none) Interactively configure and then run the agent.", +" You will be prompted for data.", +"configure Configure the agent and exit.", +"unconfigure Unconfigure the agent.", +"run Runs the agent interactively. must be configured.", +"", +"", +"Configure Arguments:", +"-----------------------------------------------------------------------------", +"--url url of the server. Examples:", +" https://myaccount.visualstudion.com or http://onprem:8080/tfs", +"--agent agent name", +"--pool pool name for agent to join", +"--replace replace the agent in a pool. if another agent is listening", +" by that name, it will start failing with a conflict", +"", +"Options:", +"-----------------------------------------------------------------------------", +"--unattend Unattended config. You will not be prompted.", +" All answers must be supplied on cli.", +"--nostart Do not start the agent after interactive configuration.", +"--auth Auth type. Valid options are PAT (Personal Access Token) and", +" ALT (Alternate Credentials)", +"", +"", +"Arguments by Auth Type:", +"-----------------------------------------------------------------------------", +"PAT:", +"--token Personal Access Token data. Best to paste value in.", +"", +"ALT:", +"--username alternate username", +"--password alternate password", +"", +"Examples:", +"-----------------------------------------------------------------------------", +"Run with no arguments to start. will configure if not configured yet.", +"$ bin/Agent.Listener", +"", +"Unattend configuration. Remember to check return codes", +"bin/Agent.Listener configure --url https://myaccount.visualstudio.com --agent myagent --pool default --nostart --auth PAT --token o4u5...", +"bin/Agent.Listener run" + ] +} \ No newline at end of file diff --git a/src/Misc/layoutroot/run.sh b/src/Misc/layoutroot/run.sh new file mode 100644 index 0000000000..443bdc9c05 --- /dev/null +++ b/src/Misc/layoutroot/run.sh @@ -0,0 +1 @@ +./bin/Agent.Listener $* \ No newline at end of file diff --git a/src/Test/L0/AgentCredentialL0.cs b/src/Test/L0/AgentCredentialL0.cs index 7764e233d8..17f941b816 100644 --- a/src/Test/L0/AgentCredentialL0.cs +++ b/src/Test/L0/AgentCredentialL0.cs @@ -1,12 +1,25 @@ using System.Collections.Generic; - +using System.Diagnostics; using Microsoft.VisualStudio.Services.Agent.Configuration; +using Microsoft.VisualStudio.Services.Client; +using Microsoft.VisualStudio.Services.Common; namespace Microsoft.VisualStudio.Services.Agent.Tests { public class TestAgentCredential : CredentialProvider { public TestAgentCredential(): base("TEST") {} + public override VssCredentials GetVssCredentials(IHostContext context) + { + TraceSource trace = context.GetTrace("PersonalAccessToken"); + trace.Info("GetVssCredentials()"); + + VssBasicCredential loginCred = new VssBasicCredential("test", "password"); + VssCredentials creds = new VssClientCredentials(loginCred); + trace.Verbose("cred created"); + + return creds; + } public override void ReadCredential(IHostContext context, Dictionary args, bool isUnattended) { } diff --git a/src/Test/_project.json b/src/Test/_project.json index 879562cf10..e15dd5b634 100644 --- a/src/Test/_project.json +++ b/src/Test/_project.json @@ -35,6 +35,11 @@ "xunit.netcore.extensions": "1.0.0-prerelease-00153", "xunit.runner.utility": "2.1.0", "Agent.Listener": { "target": "project" }, + "Microsoft.TeamFoundation.Common": "0.5.0", + "Microsoft.TeamFoundation.Core.WebApi": "0.5.0", + "Microsoft.VisualStudio.Services.Client": "0.5.0", + "Microsoft.VisualStudio.Services.Common": "0.5.0", + "Microsoft.VisualStudio.Services.WebApi": "0.5.0", "Microsoft.VisualStudio.Services.Agent": { "target": "project" }, "Agent.Worker": { "target": "project" } }, diff --git a/src/dev.sh b/src/dev.sh index ff6a256fd5..490da49125 100755 --- a/src/dev.sh +++ b/src/dev.sh @@ -107,7 +107,10 @@ function layout () for bin_copy_dir in ${bin_layout_dirs[@]} do copyBin ${bin_copy_dir} - done + done + + cp -Rf ./Misc/layoutroot/* ${LAYOUT_DIR} + cp -Rf ./Misc/layoutbin/* ${LAYOUT_DIR}/bin } function update ()