I have created a windows service using TOPSHELF dlls but I want to make it be able to receive parameters after installing it.
Say my folderpath is the place from where the service will read the file.
Here is my Start method.
public void Start()
{
_log.Info("SampleService is Started");
_timer.AutoReset = true;
_timer.Enabled = true;
_timer.Start();
//String folderpath I want to use this as a start parameter.
GetAndConvertFileIntoXML(folderPath);
}
Oliver has a great answer if you wanted to go down that path. It's not something I expect to see in Topshelf (even if the patch shows up in the mail).
We don't have a way to do this in Topshelf because it's complicated to understand the context you use. We believe you should use app.config to manage this instead of command line parameters.
Let me explain the experience problem I haven't figured out: service install --myparam=one - Great! We modify the IMAGE_PATH like Oliver's patch does. Now, what should it be if you install an update with just service install? Should we try and keep that parameter? What if it's tied to the instance name and you just forgot it? In addition, there's no visibility into what arguments are set for a given service.
If someone helps me come up with a way to make this experience clear to users, I'd be apt to ship it with Topshelf. In the mean time, our guidance is use app.config.
When you manually install a windows service by using the command line you can provide additional arguments that should be added to your application start easily:
sc create MyService binPath="notepad d:\myFile.txt"
Unfortunately doesn't Topshelf support this. So i took the sources and added that feature on my own. I sent the needed patch to the project (not by pull request, but by mail) but they never answered me or built the feature into the code.
After applying that patch (see bottom) you get a new method SetArguments() which let's define you additional parameters that your service should append to the exe path that's been called at start of your service. If you like to provide this argument also to the application when you call it with the install keyword, then it gets a little bit complicated, but it works. First the code needed to forward the desired command line argument to the service installer:
var host = HostFactory.New(x =>
{
x.AddCommandLineDefinition("configuration", filename =>
{
configFileName = CreateFullPathName(filename);
x.SetArguments("-configuration \"" + configFileName + "\"");
});
...
});
If you now call your program with the command line
MyServiceApplication install -configuration ".\Some Sub Folder\My File.txt"
you can see within the services.msc that under path to executable of your service the parameter has been added and will be filled when the service gets started.
Patch file
Index: src/Topshelf/Runtime/HostSettings.cs
===================================================================
--- src/Topshelf/Runtime/HostSettings.cs (original)
+++ src/Topshelf/Runtime/HostSettings.cs (add SetArguments)
## -38,6 +38,11 ##
string InstanceName { get; }
/// <summary>
+ /// The additional arguments that should be appended when the service is installed
+ /// </summary>
+ string Arguments { get; }
+
+ /// <summary>
/// Returns the Windows service name, including the instance name, which is registered with the SCM Example: myservice$bob
/// </summary>
/// <returns> </returns>
Index: src/Topshelf/Runtime/Windows/HostServiceInstaller.cs
===================================================================
--- src/Topshelf/Runtime/Windows/HostServiceInstaller.cs (original)
+++ src/Topshelf/Runtime/Windows/HostServiceInstaller.cs (add SetArguments)
## -114,6 +114,9 ##
if (!string.IsNullOrEmpty(settings.Name))
arguments += string.Format(" -servicename \"{0}\"", settings.Name);
+ if (!string.IsNullOrEmpty(settings.Arguments))
+ arguments += " " + settings.Arguments;
+
return new HostInstaller(settings, arguments, installers);
}
Index: src/Topshelf/Runtime/Windows/WindowsHostSettings.cs
===================================================================
--- src/Topshelf/Runtime/Windows/WindowsHostSettings.cs (original)
+++ src/Topshelf/Runtime/Windows/WindowsHostSettings.cs (add SetArguments)
## -1,14 +1,14 ##
// Copyright 2007-2012 Chris Patterson, Dru Sellers, Travis Smith, et. al.
-//
-// Licensed under the Apache License, Version 2.0 (the "License"); you may not use
-// this file except in compliance with the License. You may obtain a copy of the
-// License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software distributed
-// under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
-// CONDITIONS OF ANY KIND, either express or implied. See the License for the
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not use
+// this file except in compliance with the License. You may obtain a copy of the
+// License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software distributed
+// under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
+// CONDITIONS OF ANY KIND, either express or implied. See the License for the
// specific language governing permissions and limitations under the License.
namespace Topshelf.Runtime.Windows
{
## -19,8 +19,8 ##
HostSettings
{
public const string InstanceSeparator = "$";
- string _description;
- string _displayName;
+ private string _description;
+ private string _displayName;
/// <summary>
/// Creates a new WindowsServiceDescription using empty strings for the properties. The class is required to have names by the consumers.
## -49,6 +49,8 ##
_description = "";
}
+ public string Arguments { get; set; }
+
public string Name { get; set; }
public string DisplayName
## -68,7 +70,6 ##
set { _displayName = value; }
}
-
public string Description
{
get
## -80,7 +81,6 ##
set { _description = value; }
}
-
public string InstanceName { get; set; }
public string ServiceName
Index: src/Topshelf/Topshelf.csproj
===================================================================
--- src/Topshelf/Topshelf.csproj (original)
+++ src/Topshelf/Topshelf.csproj (add SetArguments)
## -112,6 +112,7 ##
<Compile Include="Configuration\HostConfigurators\SudoConfigurator.cs" />
<Compile Include="Configuration\HostConfigurators\UninstallHostConfiguratorAction.cs" />
<Compile Include="Configuration\HostConfigurators\UnknownCommandLineOptionHostConfigurator.cs" />
+ <Compile Include="Configuration\Options\ArgumentsOption.cs" />
<Compile Include="Configuration\Options\AutostartOption.cs" />
<Compile Include="Configuration\Options\DelayedOption.cs" />
<Compile Include="Configuration\Options\DisabledOption.cs" />
## -221,6 +222,9 ##
<ItemGroup>
<Folder Include="Properties\" />
</ItemGroup>
+ <ItemGroup>
+ <None Include="Internals\README.md" />
+ </ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
Other similar extension points exist, see Microsoft.Common.targets.
Index: src/Topshelf/Hosts/InstallHost.cs
===================================================================
--- src/Topshelf/Hosts/InstallHost.cs (original)
+++ src/Topshelf/Hosts/InstallHost.cs (add SetArguments)
## -139,6 +139,11 ##
_dependencies = dependencies;
}
+ public string Arguments
+ {
+ get { return _settings.Arguments; }
+ }
+
public string Name
{
get { return _settings.Name; }
Index: src/Topshelf/Configuration/HostConfigurators/CommandLineParserOptions.cs
===================================================================
--- src/Topshelf/Configuration/HostConfigurators/CommandLineParserOptions.cs (original)
+++ src/Topshelf/Configuration/HostConfigurators/CommandLineParserOptions.cs (add SetArguments)
## -61,7 +61,9 ##
.Or(from disp in x.Definition("displayname")
select (Option)new DisplayNameOption(disp.Value))
.Or(from instance in x.Definition("instance")
- select (Option)new InstanceOption(instance.Value)));
+ select (Option)new InstanceOption(instance.Value))
+ .Or(from instance in x.Definition("arguments")
+ select (Option)new ArgumentsOption(instance.Value)));
}
internal static void AddUnknownOptions(ICommandLineElementParser<Option> x)
Index: src/Topshelf/Configuration/HostConfigurators/HostConfiguratorImpl.cs
===================================================================
--- src/Topshelf/Configuration/HostConfigurators/HostConfiguratorImpl.cs (original)
+++ src/Topshelf/Configuration/HostConfigurators/HostConfiguratorImpl.cs (add SetArguments)
## -64,7 +64,7 ##
yield return this.Failure("Name", "must be specified and not empty");
else
{
- var disallowed = new[] {' ', '\t', '\r', '\n', '\\', '/'};
+ var disallowed = new[] { ' ', '\t', '\r', '\n', '\\', '/' };
if (_settings.Name.IndexOfAny(disallowed) >= 0)
yield return this.Failure("Name", "must not contain whitespace, '/', or '\\' characters");
}
## -86,6 +86,11 ##
yield return this.Success("ServiceName", _settings.ServiceName);
}
+ public void SetArguments(string arguments)
+ {
+ _settings.Arguments = arguments;
+ }
+
public void SetDisplayName(string name)
{
_settings.DisplayName = name;
Index: src/Topshelf/Configuration/HostConfigurators/HostConfigurator.cs
===================================================================
--- src/Topshelf/Configuration/HostConfigurators/HostConfigurator.cs (original)
+++ src/Topshelf/Configuration/HostConfigurators/HostConfigurator.cs (add SetArguments)
## -17,6 +17,12 ##
public interface HostConfigurator
{
/// <summary>
+ /// Specifies additional command line arguments that should be added when the service is registered
+ /// </summary>
+ /// <param name="name"> </param>
+ void SetArguments(string arguments);
+
+ /// <summary>
/// Specifies the name of the service as it should be displayed in the service control manager
/// </summary>
/// <param name="name"> </param>
Index: src/Topshelf/Configuration/Options/ArgumentsOption.cs
===================================================================
--- src/Topshelf/Configuration/Options/ArgumentsOption.cs (nonexistent)
+++ src/Topshelf/Configuration/Options/ArgumentsOption.cs (add SetArguments)
## -0,0 +1,31 ##
+// Copyright 2007-2012 Chris Patterson, Dru Sellers, Travis Smith, et. al.
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not use
+// this file except in compliance with the License. You may obtain a copy of the
+// License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software distributed
+// under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
+// CONDITIONS OF ANY KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations under the License.
+namespace Topshelf.Options
+{
+ using HostConfigurators;
+
+ public class ArgumentsOption : Option
+ {
+ string _arguments;
+
+ public ArgumentsOption(string arguments)
+ {
+ _arguments = arguments;
+ }
+
+ public void ApplyTo(HostConfigurator configurator)
+ {
+ configurator.SetArguments(_arguments);
+ }
+ }
+}
\ No newline at end of file
Index: src/Topshelf/HelpText.txt
===================================================================
--- src/Topshelf/HelpText.txt (original)
+++ src/Topshelf/HelpText.txt (add SetArguments)
## -28,8 +28,10 ##
installing
-description The service description the service should use when
installing
- -displayname The display name the the service should use when
+ -displayname The display name the service should use when
installing
+ -arguments The command line arguments the service should
+ also get when installing
start Starts the service if it is not already running
## -57,3 +59,10 ##
Installs the service, appending the instance name to the service name
so that the service can be installed multiple times. You may need to
tweak the log4net.config to make this play nicely with the log files.
+
+ service install -arguments "-configFile \"C:\my folder\service.config\""
+ Installs the service, appending the command line argument to the
+ service so that the service will always retrieve this information
+ when started.
+ If double quotes are needed within the arguments they must be
+ escaped by a backslash \" (see example).
You have really easy options
static void Main(String[] args)
{
ServiceBase[] ServicesToRun;
ServicesToRun = new ServiceBase[] { new Service1(args) };
ServiceBase.Run(ServicesToRun);
}
then
public Service1(String[] args)
{
if (args.Length==0) // terminate with appropriate error
else
folderPath = arg[0]; // as an example
}
then in your service setup you can/must include a path.
Or, simply read it from a settings file that will be where the exe is - which is another and normal way of doing this.
Related
I have a Windows services which is written in C# (.NET 3.5) and this service's role is to receive DLLs with a specific interface and execute their methods.
Some clients are complaining that sometimes the service fails to start and after I've looked at our logs I've noticed that the part when trying to load the assemblies dynamically is taking a lot of time (about ~30-40 seconds), so the service gets a timeout after 60 seconds when trying to start.
The major problem here is that this issue does not always occur. We can rarely recreate this slowness in QA Environment so when I'm trying to refactor the code of the dynamic loading I can't really know if I'm solving this issue.
Currently the code is loading 2 DLLs and for each assembly we get all referenced assemblies in order to dynamic load the assembly tree recursively.
I can also see when I'm going recursively on the loading I try to load .NET assemblies like System.Data, System.Core etc, which I obliviously fail because they aren't in my directory. This seems like a major time consuming. Any better suggestions?
/// <summary>
/// Loads all the assemblies and classes needed to run methods dinamically.
/// </summary>
public class BLService
{
public static Dictionary<string, Assembly> AssemblyHistory { get; private set; }
static BLService()
{
// assembly
Assembly assemblyToLoad = LoadAssembly(System.Configuration.ConfigurationManager.AppSettings[settingKey]);
// load assembly dependencies
foreach (var dependencyAssemblyName in assemblyToLoad.GetReferencedAssemblies())
DynamicallyLoadAssembly(dependencyAssemblyName);
}
private static Assembly DynamicallyLoadAssembly(AssemblyName dependencyAssemblyName)
{
LCTracer.Instance.WriteToLog(string.Format("Loading assembly | Trying to dynamically load assembly {0}", dependencyAssemblyName.Name), LCTracer.ProfileAction.Start);
Assembly currentAssembly = LoadAssembly(dependencyAssemblyName.Name);
if (currentAssembly != null)
{
LCTracer.Instance.WriteToLog(string.Format("Loading assembly | Iterating on Assembly {0} references", dependencyAssemblyName.Name));
foreach (var assembly in currentAssembly.GetReferencedAssemblies())
{
DynamicallyLoadAssembly(assembly);
}
LCTracer.Instance.WriteToLog(string.Format("Loading assembly | Finished iterating on Assembly {0} references", dependencyAssemblyName.Name));
}
LCTracer.Instance.WriteToLog(string.Format("Loading assembly"), LCTracer.ProfileAction.End);
return currentAssembly;
}
/// <summary>
/// Loads an assembly
/// </summary>
/// <param name="assemblyName"></param>
/// <returns></returns>
private static Assembly LoadAssembly(string assemblyName)
{
string assembliesDir = System.Configuration.ConfigurationManager.AppSettings["AssemblyPath"];
string assemblyPath = Path.Combine(assembliesDir, assemblyName + ".dll");
// We only load files from inside the designated directory.
if (!File.Exists(assemblyPath))
{
LCTracer.Instance.WriteToLog("Loading assembly | " + assemblyName + " does not exist in path");
return null;
}
byte[] assemblyByteArray = File.ReadAllBytes(assemblyPath);
Assembly asm = null;
try
{
asm = Assembly.Load(assemblyByteArray);
// Load only if already loaded.
if (!AssemblyHistory.ContainsKey(asm.GetName().Name))
{
AssemblyHistory.Add(asm.GetName().Name, asm);
LCTracer.Instance.WriteToLog("Loading assembly | Success: Adding Assembly " + assemblyName + " to history.");
}
}
catch (Exception ex)
{
LCTracer.Instance.WriteToLog("Loading assembly | Error: Adding Assembly " + assemblyName + " failed: message - " + ex.Message + " ,stack trace - " + ex.StackTrace + " ,inner exception - " + ex.InnerException, null, LCTracer.LogDebugLevel.Low);
}
return (asm != null) ? AssemblyHistory[asm.GetName().Name] : AssemblyHistory[assemblyName];
}
}
Edit: Added some code for context. You can think of this service as plugin runner, which gets a plugin and executes it.
Any help will be appreciated.
Try installing Fusion log viewer (comes with Windows SDK) on the complaining clients' servers, and set it to log all load attempts (also successfull).
Then you can see where it si looking for the assemblies - you might find some slow network path there or other problems.
MSDN on Fusion Log Viewer
usefull blog post on using Fusion Log Viewer
I found the solution!
One of the assembly that I try to load was NHibernate.XmlSerializers.dll which is created at runtime if not found and on slow machine this creation was taking a lot of time.
Here is an article of how to pregenerated this dll to improve the performance.
The main command is:
sgen.exe NHibernate.dll /type:NHibernate.Cfg.MappingSchema.HbmMapping /compiler:/keyfile:NHibernate.snk
I am using a setup project to publish my projects. I want the version of each project to be the same as the setup version.
I want to change my setup version property in Visual Studio and after building, for all project versions to be updated from this property, is this possible?
Projects have Assembly & File version numbers: (not setup versions I edited your question accordingly)
Answer 1:
If you want to make the Setup projects version number set the Assembly & File version numbers you need to do it with a script/exe that gets triggered by the build.
This article on How To Update Assembly Version Number Automatically shows half the solution...
From the research I did it is not possible to use the SetupVersion in a PreBuildEvent. There isn't a $SetupVersion command for it: http://msdn.microsoft.com/en-us/library/42x5kfw4(v=vs.80).aspx
Having to change the PreBuildEvent each build as shown in this comment in the Code Project article using the -set: command is not ideal.
The solution we need is a PreBuildEvent to call the AssemblyInfoUtil.exe and have it read the "ProductVersion" from the vdproj project file. And then update the Assembly version number(s).
I have modified the code from the article to show you how to read the product version from the Setup.vdproj and this is how it can be called from a PreBuildEvent:
AssemblyInfoUtil.exe -setup:"C:\Program Files\MyProject1\Setup1\Setup1.vdproj" -ass:"C:\Program Files\MyProject1\AssemblyInfo.cs"
This is the modified code:
using System;
using System.IO;
using System.Text;
namespace AssemblyInfoUtil
{
class AssemblyInfoUtil
{
private static int incParamNum = 0;
private static string fileName = "";
private static string setupfileName = "";
private static string versionStr = null;
private static bool isVB = false;
[STAThread]
static void Main(string[] args)
{
for (int i = 0; i < args.Length; i++) {
if (args[i].StartsWith("-setup:")) {
string s = args[i].Substring("-setup:".Length);
setupfileName = int.Parse(s);
}
else if (args[i].StartsWith("-ass:")) {
fileName = args[i].Substring("-ass:".Length);
}
}
//Jeremy Thompson showing how to detect "ProductVersion" = "8:1.0.0" in vdproj
string setupproj = System.IO.File.ReadAllText(setupfileName);
int startPosOfProductVersion = setupproj.IndexOf("\"ProductVersion\" = \"") +20;
int endPosOfProductVersion = setupproj.IndexOf(Environment.NewLine, startPosOfProductVersion) - startPosOfProductVersion;
string versionStr = setupproj.Substring(startPosOfProductVersion, endPosOfProductVersion);
versionStr = versionStr.Replace("\"", string.Empty).Replace("8:",string.Empty);
if (Path.GetExtension(fileName).ToLower() == ".vb")
isVB = true;
if (fileName == "") {
System.Console.WriteLine("Usage: AssemblyInfoUtil
<path to :Setup.vdproj file> and <path to AssemblyInfo.cs or AssemblyInfo.vb file> [options]");
System.Console.WriteLine("Options: ");
System.Console.WriteLine(" -setup:Setup.vdproj file path");
System.Console.WriteLine(" -ass:Assembly file path");
return;
}
if (!File.Exists(fileName)) {
System.Console.WriteLine
("Error: Can not find file \"" + fileName + "\"");
return;
}
System.Console.Write("Processing \"" + fileName + "\"...");
StreamReader reader = new StreamReader(fileName);
StreamWriter writer = new StreamWriter(fileName + ".out");
String line;
while ((line = reader.ReadLine()) != null) {
line = ProcessLine(line);
writer.WriteLine(line);
}
reader.Close();
writer.Close();
File.Delete(fileName);
File.Move(fileName + ".out", fileName);
System.Console.WriteLine("Done!");
}
private static string ProcessLine(string line) {
if (isVB) {
line = ProcessLinePart(line, "<Assembly: AssemblyVersion(\"");
line = ProcessLinePart(line, "<Assembly: AssemblyFileVersion(\"");
}
else {
line = ProcessLinePart(line, "[assembly: AssemblyVersion(\"");
line = ProcessLinePart(line, "[assembly: AssemblyFileVersion(\"");
}
return line;
}
private static string ProcessLinePart(string line, string part) {
int spos = line.IndexOf(part);
if (spos >= 0) {
spos += part.Length;
int epos = line.IndexOf('"', spos);
string oldVersion = line.Substring(spos, epos - spos);
string newVersion = "";
bool performChange = false;
if (incParamNum > 0) {
string[] nums = oldVersion.Split('.');
if (nums.Length >= incParamNum && nums[incParamNum - 1] != "*") {
Int64 val = Int64.Parse(nums[incParamNum - 1]);
val++;
nums[incParamNum - 1] = val.ToString();
newVersion = nums[0];
for (int i = 1; i < nums.Length; i++) {
newVersion += "." + nums[i];
}
performChange = true;
}
}
else if (versionStr != null) {
newVersion = versionStr;
performChange = true;
}
if (performChange) {
StringBuilder str = new StringBuilder(line);
str.Remove(spos, epos - spos);
str.Insert(spos, newVersion);
line = str.ToString();
}
}
return line;
}
}
}
Answer 2:
To my way of thinking a better way is to use a Shared Assembly Info class rather than individual AssemblyInfo class files.
To implement this, create a file in the solution folder named SharedAssemblyInfo.cs and then add a link in each project to SharedAssemblyInfo.cs. You can also move the linked SharedAssemblyInfo.cs into the Properties folder so that it sits side-by-side with the AssemblyInfo.cs that is specific to each project in the solution, as shown below.
Here is a sample SharedAssemblyInfo.cs file:
using System;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
// General Information about an assembly is controlled through the following
// set of attributes. Change these attribute values to modify the information
// associated with an assembly.
[assembly: AssemblyCompany("Saint Bart Technologies")]
[assembly: AssemblyProduct("Demo")]
[assembly: AssemblyCopyright("Copyright ? Saint Bart 2013")]
[assembly: AssemblyTrademark("")]
// Make it easy to distinguish Debug and Release (i.e. Retail) builds;
// for example, through the file properties window.
#if DEBUG
[assembly: AssemblyConfiguration("Debug")]
[assembly: AssemblyDescription("Flavor=Debug")] // a.k.a. "Comments"
#else
[assembly: AssemblyConfiguration("Retail")]
[assembly: AssemblyDescription("Flavor=Retail")] // a.k.a. "Comments"
#endif
[assembly: CLSCompliant(true)]
// Setting ComVisible to false makes the types in this assembly not visible
// to COM components. If you need to access a type in this assembly from
// COM, set the ComVisible attribute to true on that type.
[assembly: ComVisible(false)]
// Note that the assembly version does not get incremented for every build
// to avoid problems with assembly binding (or requiring a policy or
// <bindingRedirect> in the config file).
//
// The AssemblyFileVersionAttribute is incremented with every build in order
// to distinguish one build from another. AssemblyFileVersion is specified
// in AssemblyVersionInfo.cs so that it can be easily incremented by the
// automated build process.
[assembly: AssemblyVersion("1.0.0.0")]
// By default, the "Product version" shown in the file properties window is
// the same as the value specified for AssemblyFileVersionAttribute.
// Set AssemblyInformationalVersionAttribute to be the same as
// AssemblyVersionAttribute so that the "Product version" in the file
// properties window matches the version displayed in the GAC shell extension.
[assembly: AssemblyInformationalVersion("1.0.0.0")] // a.k.a. "Product version"
Here is a sample AssemblyInfo.cs file:
// Note: Shared assembly information is specified in SharedAssemblyInfo.cs
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
// General Information about an assembly is controlled through the following
// set of attributes. Change these attribute values to modify the information
// associated with an assembly.
[assembly: AssemblyTitle("WindowsFormsApplication2")]
// The following GUID is for the ID of the typelib if this project is exposed to COM
[assembly: Guid("ffded14d-6c95-440b-a45d-e1f502476539")]
So each time you want to change all projects Assembly info you can do it in one spot. I assume you would want to set the MSI Setup Version the same as the Assembly version number, one manual step.
Answer 3:
Consider switching to use MSBuild it has all these kinds of benefits but I'm not sure if you have the time to pick it up right now.
Answer 4:
Assemblies can auto-increment their build numbers using the following asterisk syntax within AssemblyInfo.cs:
[assembly: AssemblyVersion("1.0.0.*")]
This is a good method because the point of tracking a build number is
to be able to recognize different builds. Having a pre-build changing
build numbers defeats this purpose as the build has not yet occurred.
Answer 5:
The other CodeProject answer here assumes you want to update the ProductVersion, ProductCode, PackageCode in the Setup MSI Project file. I didn't interpret your question that way and according to this thread there are problems:
pre-build event to change setup project's ProductVersion doesn't take effect until after the build
Answer 6 (new):
There is a few TFS Build plugins to set "Assembly Info": https://marketplace.visualstudio.com/items?itemName=bleddynrichards.Assembly-Info-Task
https://marketplace.visualstudio.com/items?itemName=bool.update-assembly-info
https://marketplace.visualstudio.com/items?itemName=ggarbuglia.setassemblyversion-task
I don't know if this solves your problem perfectly but you could implement a common class with all the configmanagment informations like:
public class VersionInfo{
public const string cProductVersion = "1.0.0"
//other version info
}
After you can update all your AssemblyInfo.cs with the new class:
[assembly: AssemblyVersion(VersionInfo.cProductVersion)]
I hope this helps.
My hook post-commit template calls my postcommitlog.exe to create a MyFile_LOG.txt and write to it the 3 arguments. The repository and the transaction is passed from the postcommit template.
The problem is that, after committing a file in TurtoiseSVN, there is no log created. It looks like either I do not have the permission to create a file on the repository or there is an error in my code.
My code works locally, when I Debug it and pass random arguments to it, a log file is created on my local machine. But its not working on the SVN hook.
THE TEMPLATE
\\myserver\e$\Repositories\CONRAD\hooks\postcommitlog.exe %1 %2
MY PROGRAM
using System;
using System.IO;
namespace postcommitlog
{
internal class Program
{
private static void Main(string[] args)
{
string repositories = args[0];
string transaction = args[1];
const string LOG_PATH = #"\\myserver\e$\Repositories\CONRAD\hooks\MyFile_LOG.txt";
FileInfo fileInfo = new FileInfo(LOG_PATH);
File.AppendAllText(LOG_PATH, "Repositories " + args[0]
+ "\t Transaction " + args[1] + "\t Date " + DateTime.Now.ToString("MMM ddd d HH:mm yyyy") + Environment.NewLine);
}
}
}
The mistake was that I was supposed to create a BAT file from the template, not use the template. The template does nothing, it is what it is : a template. The BAT file is run by SVN then runs the script.
More info here:
http://svnbook.red-bean.com/en/1.5/svn.reposadmin.create.html#svn.reposadmin.create.hooks
Closed. This question does not meet Stack Overflow guidelines. It is not currently accepting answers.
We don’t allow questions seeking recommendations for books, tools, software libraries, and more. You can edit the question so it can be answered with facts and citations.
Closed 5 years ago.
Improve this question
Is there a command-line parsing library for C# with good support for "sub-commands" in the style of git, svn etc.? For example, the "git" command has several sub-commands:
git add
git status
git diff
...
There are both global options that must precede the sub-command name, and options specific to the sub-command that must follow its name. For example, these do different things:
git -p add
git add -p
Different sub-commands might each have entirely different sets of options and arguments.
I've been using NDesk.Options, but up until now I haven't needed to implement sub-commands. I think it's flexible enough to build sub-commands on top of, but it's not entirely obvious how best to do this in a concise and elegant fashion. Is it possible to do this in NDesk.Options or is there a more suitable C# command-line parsing library?
I'm partial to my own option parsing library, which I'm blogging about currently. I do plan to cover sub-commands, but it'll be a while before I get to it (it'll be one of the last posts).
Nito.KitchenSink.OptionParsing doesn't support sub-commands directly, but you can use the library to parse only parts of the command line, and handle the sub-commands yourself. The "global" and "sub-command-specific" option sets add an interesting twist, but it can be done like this:
using System;
using System.Linq;
using Nito.KitchenSink.OptionParsing;
class Program
{
private sealed class GlobalOptions : OptionArgumentsBase
{
// Use a better name than "POption". This is just an example.
[Option('p', OptionArgument.None)]
public bool POption { get; set; }
// Override Validate to allow AdditionalArguments.
public override void Validate()
{
}
}
private sealed class AddOptions : OptionArgumentsBase
{
[Option('p', OptionArgument.None)]
public bool POption { get; set; }
}
static int Main()
{
try
{
// Parse the entire command line into a GlobalOptions object.
var options = OptionParser.Parse<GlobalOptions>();
// The first entry in AdditionalArguments is our sub-command.
if (options.AdditionalArguments.Count == 0)
throw new OptionParsingException("No sub-command specified.");
object subcommandOptions = null;
string subcommand = options.AdditionalArguments[0];
switch (subcommand)
{
case "add":
{
// Parse the remaining arguments as command-specific options.
subcommandOptions = OptionParser.Parse<AddOptions>(options.AdditionalArguments.Skip(1));
break;
}
case "status": // TODO: Parse command-specific options for this, too.
break;
case "diff": // TODO: Parse command-specific options for this, too.
break;
default:
throw new OptionParsingException("Unknown sub-command: " + subcommand);
}
// At this point, we have our global options, subcommand, and subcommand options.
Console.WriteLine("Global -p option: " + options.POption);
Console.WriteLine("Subcommand: " + subcommand);
var addOptions = subcommandOptions as AddOptions;
if (addOptions != null)
Console.WriteLine("Add-specific -p option: " + addOptions.POption);
return 0;
}
catch (OptionParsingException ex)
{
Console.Error.WriteLine(ex.Message);
// TODO: write out usage information.
return 2;
}
catch (Exception ex)
{
Console.Error.WriteLine(ex);
return 1;
}
}
}
The sample program above produces the following output:
>CommandLineParsingTest.exe
No sub-command specified.
>CommandLineParsingTest.exe -p
No sub-command specified.
>CommandLineParsingTest.exe test
Unknown sub-command: test
>CommandLineParsingTest.exe add
Global -p option: False
Subcommand: add
Add-specific -p option: False
>CommandLineParsingTest.exe -p add
Global -p option: True
Subcommand: add
Add-specific -p option: False
>CommandLineParsingTest.exe add -p
Global -p option: False
Subcommand: add
Add-specific -p option: True
>CommandLineParsingTest.exe -p add -p
Global -p option: True
Subcommand: add
Add-specific -p option: True
>CommandLineParsingTest.exe status
Global -p option: False
Subcommand: status
>CommandLineParsingTest.exe -p status
Global -p option: True
Subcommand: status
You can try the Open Source project "Command Line" :
http://commandline.codeplex.com/
I have my own custom tool for Visual Studio 2008 SP1. It consists of 5 assemblies: 3 assemblies with code that are used heavily in my other projects, 1 assembly-wrapper above VS2008 SDK and an assembly with the tool.
If I'd debug my tool from visual studio, using "Run external program" option with command line "C:\Program Files (x86)\Microsoft Visual Studio 9.0\Common7\IDE\devenv.exe" and arguments "/ranu /rootsuffix Exp" all works perfectly.
After that I'm trying to deploy it to my working VS copy, not to experimental hive, doing: gacutil /i Asm1.dll for all my assemblies and doing RegAsm Asm1.dll only for assembly with custom tool. Neither of utils prints any error, all work as planned, even registry keys appear. But my tool doesn't work (error occurred "Cannot find custom tool 'TransportGeneratorTool' on this system") even after PC restart. What did I do wrong?
Wrapper looks like that:
[ComVisible(true)]
public abstract class CustomToolBase : IVsSingleFileGenerator, IObjectWithSite
{
#region IVsSingleFileGenerator Members
int IVsSingleFileGenerator.DefaultExtension(out string pbstrDefaultExtension)
{
pbstrDefaultExtension = ".cs";
return 0;
}
int IVsSingleFileGenerator.Generate(string wszInputFilePath, string bstrInputFileContents, string wszDefaultNamespace, IntPtr[] rgbOutputFileContents, out uint pcbOutput, IVsGeneratorProgress pGenerateProgress)
{
GenerationEventArgs gea = new GenerationEventArgs(
bstrInputFileContents,
wszInputFilePath,
wszDefaultNamespace,
new ServiceProvider(Site as Microsoft.VisualStudio.OLE.Interop.IServiceProvider)
.GetService(typeof(ProjectItem)) as ProjectItem,
new GenerationProgressFacade(pGenerateProgress)
);
if (OnGenerateCode != null)
{
OnGenerateCode(this, gea);
}
byte[] bytes = gea.GetOutputCodeBytes();
int outputLength = bytes.Length;
rgbOutputFileContents[0] = Marshal.AllocCoTaskMem(outputLength);
Marshal.Copy(bytes, 0, rgbOutputFileContents[0], outputLength);
pcbOutput = (uint)outputLength;
return VSConstants.S_OK;
}
#endregion
#region IObjectWithSite Members
void IObjectWithSite.GetSite(ref Guid riid, out IntPtr ppvSite)
{
IntPtr pUnk = Marshal.GetIUnknownForObject(Site);
IntPtr intPointer = IntPtr.Zero;
Marshal.QueryInterface(pUnk, ref riid, out intPointer);
ppvSite = intPointer;
}
void IObjectWithSite.SetSite(object pUnkSite)
{
Site = pUnkSite;
}
#endregion
#region Public Members
public object Site { get; private set; }
public event EventHandler<GenerationEventArgs> OnGenerateCode;
[ComRegisterFunction]
public static void Register(Type type)
{
using (var parent = Registry.LocalMachine.OpenSubKey(#"Software\Microsoft\VisualStudio\9.0", true))
foreach (CustomToolRegistrationAttribute ourData in type.GetCustomAttributes(typeof(CustomToolRegistrationAttribute), false))
ourData.Register(x => parent.CreateSubKey(x), (x, name, value) => x.SetValue(name, value));
}
[ComUnregisterFunction]
public static void Unregister(Type type)
{
using (var parent = Registry.LocalMachine.OpenSubKey(#"Software\Microsoft\VisualStudio\9.0", true))
foreach (CustomToolRegistrationAttribute ourData in type.GetCustomAttributes(typeof(CustomToolRegistrationAttribute), false))
ourData.Unregister(x => parent.DeleteSubKey(x, false));
}
#endregion
}
My tool code:
[ComVisible(true)]
[Guid("55A6C192-D29F-4e22-84DA-DBAF314ED5C3")]
[CustomToolRegistration(ToolName, typeof(TransportGeneratorTool))]
[ProvideObject(typeof(TransportGeneratorTool))]
public class TransportGeneratorTool : CustomToolBase
{
private const string ToolName = "TransportGeneratorTool";
public TransportGeneratorTool()
{
OnGenerateCode += GenerateCode;
}
private static void GenerateCode(object s, GenerationEventArgs e)
{
try
{
var serializer = new XmlSerializer(typeof (Parser.System));
using (var reader = new StringReader(e.InputText))
using (var writer = new StringWriter(e.OutputCode))
{
Generator.System = (Parser.System) serializer.Deserialize(reader);
Generator.System.Namespace = e.Namespace;
Generator.GenerateSource(writer);
}
}
catch (Exception ex)
{
e.Progress.GenerateError(ex.ToString());
}
}
}
Resulting registry keys:
Windows Registry Editor Version 5.00
[HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\VisualStudio\9.0\Generators]
[HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\VisualStudio\9.0\Generators\{FAE04EC1-301F-11D3-BF4B-00C04F79EFBC}]
[HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\VisualStudio\9.0\Generators\{FAE04EC1-301F-11D3-BF4B-00C04F79EFBC}\TransportGeneratorTool]
#="TransportGeneratorTool"
"CLSID"="{55a6c192-d29f-4e22-84da-dbaf314ed5c3}"
"GeneratesDesignTimeSource"=dword:00000001
"GeneratesSharedDesignTimeSource"=dword:00000001
Here is the code of my custom attribute (it is in wrapper assembly):
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = true)]
public class CustomToolRegistrationAttribute : RegistrationAttribute
{
public CustomToolRegistrationAttribute(string name, Type customToolType)
{
Name = name;
CustomToolType = customToolType;
}
/// <summary>
/// The type that implements the custom tool. This starts
/// as MyCustomTool by default in the template.
/// </summary>
public Type CustomToolType { get; set; }
public string Name { get; set; }
#region RegistrationAttribute abstract member implementations
public override void Register(RegistrationContext context)
{
Register(x => context.CreateKey(x), (x, key, value) => x.SetValue(key, value));
}
public void Register<T>(Func<string, T> keyCreator, Action<T, string, object> valueCreator)
{
var keyName = CreateKeyName(Name);
var key = keyCreator(keyName);
valueCreator(key, string.Empty, Name);
valueCreator(key, "CLSID", CustomToolType.GUID.ToString("B"));
valueCreator(key, "GeneratesDesignTimeSource", 1);
valueCreator(key, "GeneratesSharedDesignTimeSource", 1);
var disposable = key as IDisposable;
if (disposable != null)
disposable.Dispose();
}
private static string CreateKeyName(string name)
{
return string.Format(#"Generators\{0}\{1}", vsContextGuids.vsContextGuidVCSProject, name);
}
public override void Unregister(RegistrationContext context)
{
Unregister(context.RemoveKey);
}
public void Unregister(Action<string> keyRemover)
{
keyRemover(CreateKeyName(Name));
}
#endregion
}
My solution is to make a setup project. I get the registry settings from the pkgdef file by adding the following to the csproj file of the package:
<Target Name="GeneratePackageRegistryFiles">
<Exec Command=""$(VSSDK90Install)VisualStudioIntegration\Tools\Bin\RegPkg.exe" /root:Software\Microsoft\VisualStudio\9.0 /codebase "$(TargetPath)" /regfile:"$(OutDir)$(TargetName).reg"" />
</Target>
<PropertyGroup>
<BuildDependsOn>$(BuildDependsOn);GeneratePackageRegistryFiles;</BuildDependsOn>
</PropertyGroup>
When building look in the output directory you should find a .reg file which you can import in the setup project.
Obviously you can run the regpkg.exe from the command-line if modifying the project is not an option.
This is what I ended up with last time when I struggled to get my custom tool registered.
I hope this instruction is detailed enough and covers everything so you won't spend much time fighting it. The following MSDN article was used as a starting point. http://msdn.microsoft.com/en-US/library/bb166527(v=vs.80).aspx Unfortunately you cannot use it alone. What you really need to do is:
Make sure the assembly is signed. Why? Because otherwise you won't be able to put it into GAC at step 6 below.
To sign your assembly follow these steps:
1.1. Go to the Properties screen of the project.
1.2. Once there go to the Signing tab.
1.3. Once there check the Sign the assembly checkbox.
Make sure you know the version number of your assembly. You will need this number to specify the ASSEMBLY_VERSION parameter later.
In order to get this number open the AssemblyInfo.cs file in the Properties folder of your project and look for the line starting with: [assembly: AssemblyVersion(
Make sure you know the GUID of the generator class. You will need it to specify the GENERATOR_GUID parameter later.
In order to get this GUID open the file with the generator class and look for the Guid class-attribute that decorates this class, something like: [Guid("17799E85-421B-4684-B59E-650E34ECC718")]
Build the project
Get the public token key of the assembly. In order to do that you will have to run the following command:
sn.exe -T ASSEMBLY_FILE
You will need this information later when for PUBLIC_TOKEN_KEY.
The sn.exe file can be found in C:\Program Files\Microsoft SDKs\Windows\v8.0A\bin\sn.exe
Pay attention to the version number of the framework (v8.0A) in the filepath above. It needs to be consistent with the version of the framework used to compile the project.
Put the assembly to the GAC using the following command:
gacutil.exe /i ASSEMBLY_FILE /f
Getting registered in GAC requires administrative permissions.
The gacutil.exe file can be found in C:\Program Files\Microsoft SDKs\Windows\v8.0A\bin\NETFX 4.0 Tools\gacutil.exe
Pay attention to the version number of the framework (v8.0A) in the filepath above. It needs to be consistent with the version of the framework used to compile the project.
Make the following changes to the .REG (see below) file. PLEASE NOTE: that both GENERATOR_GUID and PROJECT_TYPE_GUID need to be supplied WITH curly braces: {XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX}
7.1. Fix version number of Visual Studio is used (for example: 10.0 or 9.0): VS_VERSION
7.2. Fix the GUID of the generator: GENERATOR_GUID
7.3. Fix the namespace of the assembly: NAMESPACE_NAME
7.4. Fix the generator class name: GENERATOR_TYPE_NAME
7.5. In order to register the generator the Visual Studio needs to know to which project types this generator can be applied to. So you need to get GUID's of proper project types (C#, VB.NET, etc.).
To figure out the GUID's of the project types you need to open a visual studio project file (*.csproj) in a text editor and look for GUID's in the ProjectTypeGuids XML element.
For each of these GUIDs repeat the block of last 3 entries in the .REG file replacing the PROJECT_TYPE_GUID with the a GUID just found.
7.6. Fix the extension of the file associated with the custom tool: FILE_EXTENSTION
Run the .REG file. You may need to have administrative permissions for doing this.
.REG file:
Windows Registry Editor Version 5.00
[HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\VisualStudio\VS_VERSION\CLSID\GENERATOR_GUID]
#="COM+ class: NAMESPACE_NAME.GENERATOR_TYPE_NAME"
"InprocServer32"="C:\\WINDOWS\\system32\\mscoree.dll"
"ThreadingModel"="Both"
"Class"="NAMESPACE_NAME.GENERATOR_TYPE_NAME"
"Assembly"="NAMESPACE_NAME, Version=ASSEMBLY_VERSION, Culture=Neutral, PublicKeyToken=PUBLIC_TOKEN_KEY"
[HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\VisualStudio\VS_VERSION\Generators]
[HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\VisualStudio\VS_VERSION\Generators\PROJECT_TYPE_GUID]
[HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\VisualStudio\VS_VERSION\Generators\PROJECT_TYPE_GUID\\.FILE_EXTENSTION]
#="GENERATOR_TYPE_NAME"
[HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\VisualStudio\VS_VERSION\Generators\PROJECT_TYPE_GUID\GENERATOR_TYPE_NAME]
#="Code generator for whatever you like"
"CLSID"="GENERATOR_GUID"
"GeneratesDesignTimeSource"=dword:00000001
PS.
Sorry for not being able to make placehoders in the REG file distinct, unfortunately the text editor that StackOverflow uses cannot distinguish its markup elements from the content.