diff --git a/.gitignore b/.gitignore
index bcadab8ef..1b60531b1 100644
--- a/.gitignore
+++ b/.gitignore
@@ -12,6 +12,8 @@ TestBenchFramework/bin/
TestBenchFramework/obj/
TBFSetup/Debug/
TBFSetup/Release/
+Users/bin/
+Users/obj/
Doc/
*.suo
*.bak
diff --git a/TestBenchFramework.sln b/TestBenchFramework.sln
index c41e3316d..a2d51d559 100644
--- a/TestBenchFramework.sln
+++ b/TestBenchFramework.sln
@@ -19,6 +19,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DeviceTest", "DeviceTest\De
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dirichlet.Numerics", "Dirichlet.Numerics\Dirichlet.Numerics.csproj", "{439D0878-C76E-452B-B17D-209A89E91D36}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Users", "Users\Users.csproj", "{6E5CB0E9-E1B6-4E5D-AC6E-B1049E180F2B}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -95,6 +97,16 @@ Global
{439D0878-C76E-452B-B17D-209A89E91D36}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
{439D0878-C76E-452B-B17D-209A89E91D36}.Release|Mixed Platforms.Build.0 = Release|Any CPU
{439D0878-C76E-452B-B17D-209A89E91D36}.Release|x86.ActiveCfg = Release|Any CPU
+ {6E5CB0E9-E1B6-4E5D-AC6E-B1049E180F2B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {6E5CB0E9-E1B6-4E5D-AC6E-B1049E180F2B}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {6E5CB0E9-E1B6-4E5D-AC6E-B1049E180F2B}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
+ {6E5CB0E9-E1B6-4E5D-AC6E-B1049E180F2B}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
+ {6E5CB0E9-E1B6-4E5D-AC6E-B1049E180F2B}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {6E5CB0E9-E1B6-4E5D-AC6E-B1049E180F2B}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {6E5CB0E9-E1B6-4E5D-AC6E-B1049E180F2B}.Release|Any CPU.Build.0 = Release|Any CPU
+ {6E5CB0E9-E1B6-4E5D-AC6E-B1049E180F2B}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
+ {6E5CB0E9-E1B6-4E5D-AC6E-B1049E180F2B}.Release|Mixed Platforms.Build.0 = Release|Any CPU
+ {6E5CB0E9-E1B6-4E5D-AC6E-B1049E180F2B}.Release|x86.ActiveCfg = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
diff --git a/TestBenchFramework/TBF.csproj b/TestBenchFramework/TBF.csproj
index c141fed8e..2bb077f4c 100644
--- a/TestBenchFramework/TBF.csproj
+++ b/TestBenchFramework/TBF.csproj
@@ -2182,6 +2182,10 @@
{9D0DCC88-DC81-47EB-9FDD-4C3907871BFB}
Results
+
+ {6E5CB0E9-E1B6-4E5D-AC6E-B1049E180F2B}
+ Users
+
diff --git a/Users/DB.cs b/Users/DB.cs
new file mode 100644
index 000000000..60dc093a9
--- /dev/null
+++ b/Users/DB.cs
@@ -0,0 +1,183 @@
+///
+/// Copyright (c) 2016 Sensus Metering Systems
+///
+using System;
+using System.Collections.Generic;
+using FluentNHibernate.Cfg;
+using FluentNHibernate.Cfg.Db;
+using log4net;
+using NHibernate;
+using NHibernate.Cfg;
+using NHibernate.Tool.hbm2ddl;
+using Users.Entities;
+
+namespace Users
+{
+ public static class DB
+ {
+ static readonly ILog log = LogManager.GetLogger(typeof(DB));
+
+ /// Session factory for all regular sessions, not for CreateEmptyResultsDB().
+ public static ISessionFactory SessionFactory;
+
+ /// Connection string for all sessions
+ private static string connectionString;
+ ///
+ public static string ConnectionString
+ {
+ get { return connectionString; }
+ set { connectionString = value; SessionFactory = null; }
+ }
+
+ /// Database type (MySQL or SQLite) for all sessions
+ private static Config.Entities.DBType dbType;
+ ///
+ public static Config.Entities.DBType DbType
+ {
+ get { return dbType; }
+ set { dbType = value; SessionFactory = null; }
+ }
+
+ ///
+ /// NHibernate session factory (to create the database session 'SessionFactory')
+ ///
+ /// A database session
+ static ISessionFactory CreateSessionFactory()
+ {
+ return CreateSessionFactory(false);
+ }
+
+ ///
+ /// NHibernate session factory (to create the database session 'SessionFactory')
+ ///
+ /// true = Create a new DB, false = Regular DB
+ /// A database session
+ public static ISessionFactory CreateSessionFactory(bool createDB)
+ {
+ FluentConfiguration cfg = Fluently.Configure();
+
+ switch (dbType)
+ {
+ default:
+ case Config.Entities.DBType.SQLite:
+ cfg = cfg.Database(SQLiteConfiguration.Standard.UsingFile(connectionString));
+ break;
+ case Config.Entities.DBType.MySql:
+ cfg = cfg.Database(MySQLConfiguration.Standard.ConnectionString(connectionString));
+ break;
+ }
+
+ cfg = cfg.Mappings(m => m.FluentMappings.AddFromAssemblyOf());
+
+ if (createDB)
+ {
+ return cfg.ExposeConfiguration(BuildSchemaCreate).BuildSessionFactory();
+ }
+ else
+ {
+ return cfg.ExposeConfiguration(BuildSchema).BuildSessionFactory();
+ }
+ }
+
+ static void BuildSchema(Configuration config)
+ {
+ /// This NHibernate tool takes a configuration with mapping info and exports a database schema
+ new SchemaExport(config).SetOutputFile("db_schema");
+ }
+
+ static void BuildSchemaCreate(Configuration config)
+ {
+ /// This NHibernate tool takes a configuration with mapping info and exports a database schema
+ new SchemaExport(config).Create(true, true);
+ }
+
+ /// Create a NHibernate session for the given database
+ public static ISession CreateSession()
+ {
+ if (string.IsNullOrEmpty(connectionString))
+ {
+ throw new Exception("Connection string was not specified");
+ }
+
+ if (SessionFactory == null) SessionFactory = CreateSessionFactory();
+
+ return SessionFactory.OpenSession();
+ }
+
+
+ public static void SaveObject(object obj)
+ {
+ SaveObject(CreateSession(), obj);
+ }
+ ///
+ public static void SaveObject(ISession session, object obj)
+ {
+ using (var transaction = session.BeginTransaction())
+ {
+ session.SaveOrUpdate(obj);
+ try { transaction.Commit(); }
+ catch { }
+ }
+ }
+
+
+ public static void DeleteObject(object obj)
+ {
+ DeleteObject(CreateSession(), obj);
+ }
+ ///
+ public static void DeleteObject(ISession session, object obj)
+ {
+ using (var transaction = session.BeginTransaction())
+ {
+ session.Delete(obj);
+ transaction.Commit();
+ }
+ }
+
+
+ ///
+ /// Create an empty users database.
+ /// Database contains only the user 'admin' and the control board component 'CB'.
+ ///
+ /// DBType.SQLite or DBType.MySql
+ /// Connection string
+ /// true=success, false=error
+ public static bool CreateEmptyDB()
+ {
+ ISessionFactory sessionFactory = CreateSessionFactory(true);
+ if (sessionFactory == null) return false;
+
+ /// Populate the database
+ using (var session = sessionFactory.OpenSession())
+ {
+ using (var transaction = session.BeginTransaction())
+ {
+ transaction.Commit();
+ }
+ }
+
+ return true;
+ }
+
+
+ ///
+ /// Shared data
+ ///
+ public static IList Users;
+ public static IList Groups;
+
+
+ ///
+ /// Loads shared data from the database
+ ///
+ /// Throws NHibernate exceptions
+ public static void LoadSharedData()
+ {
+ ISession session = DB.CreateSession();
+
+ Users = session.QueryOver().List();
+ Groups = session.QueryOver().List();
+ }
+ }
+}
diff --git a/Users/Entities/Group.cs b/Users/Entities/Group.cs
new file mode 100644
index 000000000..f95a0ba76
--- /dev/null
+++ b/Users/Entities/Group.cs
@@ -0,0 +1,15 @@
+///
+/// Copyright (c) 2016 Sensus Metering Systems
+///
+using System;
+using System.Collections.Generic;
+
+namespace Users.Entities
+{
+ public class Group
+ {
+ public virtual int Id { get; protected set; }
+ public virtual int Gid { get; set; }
+ public virtual string Name { get; set; }
+ }
+}
diff --git a/Users/Entities/User.cs b/Users/Entities/User.cs
new file mode 100644
index 000000000..746baeca1
--- /dev/null
+++ b/Users/Entities/User.cs
@@ -0,0 +1,271 @@
+///
+/// Copyright (c) 2016 Sensus Metering Systems
+///
+using System;
+using System.Collections.Generic;
+using System.Security.Cryptography;
+using System.Text;
+using System.Diagnostics;
+using Config;
+using log4net;
+
+namespace Users.Entities
+{
+ public class User
+ {
+ static readonly ILog log = LogManager.GetLogger(typeof(User));
+
+ public virtual int Id { get; protected set; }
+ public virtual string Name { get; set; }
+ public virtual int Number { get; set; }
+ public virtual string Password { get; set; }
+ public virtual string Description { get; set; }
+ public virtual DateTime LastPwChange { get; set; }
+ public virtual IList Groups { get; set; } //User can be a member of a list of groups
+
+ private bool powerUser; /// true for built-in users, power users have full access even with empty Groups list
+
+ public User()
+ {
+ this.powerUser = false;
+ Groups = new List();
+ Number = 0;
+ }
+
+ public User(string name, int number, bool powerUser)
+ {
+ Name = name;
+ Number = number;
+ this.powerUser = powerUser;
+ Groups = new List();
+ Description = "Power user";
+ }
+
+ public virtual void AddGroup(Group group)
+ {
+ Groups.Add(group);
+ }
+
+ /// ------------- Additional stuff not mapped into the database -------------
+
+ ///
+ /// Authorized user (static)
+ ///
+ private static User currentUser = null; /// Updated by instance methods Authorize, Unauthorize
+ public static User CurrentUser
+ {
+ get { return currentUser; }
+ set { currentUser = value; }
+ }
+
+ private static DateTime lastAuthorization = DateTime.Now;
+ public static DateTime LastAuthorization { get { return lastAuthorization; } }
+
+ ///
+ /// Check whether user is a member of a group.
+ ///
+ /// Group GID
+ /// true is user is a member of the specified group
+ public virtual bool IsMemberOf(Grp.GID groupId)
+ {
+ if (groupId == Grp.GID.None || powerUser)
+ {
+ return true;
+ }
+ else if (groupId >= 0 && (int)groupId < Grp.Count)
+ {
+ // for all group elements in User.groups
+ for (int i = 0; i < Groups.Count; ++i)
+ {
+ // element GID = parameter GroupId ?
+ if (((Group)Groups[i]).Gid == (int)groupId)
+ {
+ return true;
+ }
+ }
+ return false;
+ }
+ else
+ {
+ return false;
+ }
+ }
+
+ ///
+ /// Sets users password. It then will be encrypted.
+ ///
+ /// Password
+ public virtual void SetPassword(string password)
+ {
+ this.Password = EncryptedPassword(password);
+ LastPwChange = DateTime.Now;
+ }
+
+
+ public virtual string EncryptedPassword(string password)
+ {
+ return getHash(password);
+ }
+
+ ///
+ /// Verifies users password. Used by static bool Authorisation(...)
+ ///
+ /// Password
+ /// true if password is correct
+ public virtual bool CheckPassword(string password)
+ {
+ return (Password == EncryptedPassword(password));
+ }
+
+ ///
+ /// The FIRST of TWO possible user authorization method to be used
+ /// when a specific group membership is required (you can use Grp.GID.None).
+ /// If the user is not authorized, the current user remains to be a current user
+ /// (i.e. the access rights were not risen to a higher level).
+ /// If you require different behavior, use Unauthorize() before calling Authorize().
+ ///
+ ///
+ ///
+ ///
+ /// true = authorized
+ public virtual bool Authorize(string userName, string password, Grp.GID requiredGroupMembership)
+ {
+ if (Entities.User.IsPowerUser(userName, password))
+ {
+ currentUser = this;
+ lastAuthorization = DateTime.Now;
+ log.FatalFormat("Power user '{0}' authorized @level '{1}'", userName, requiredGroupMembership);
+ return true;
+ }
+
+ if (Name.ToLower() == userName.ToLower())
+ {
+ if (IsMemberOf(requiredGroupMembership) && CheckPassword(password))
+ {
+ currentUser = this;
+ lastAuthorization = DateTime.Now;
+ log.FatalFormat("User '{0}' authorized @level '{1}'", userName, requiredGroupMembership);
+ return true;
+ }
+ else
+ {
+ return false;
+ }
+ }
+ return false;
+ }
+
+ ///
+ /// The SECOND of TWO possible user authorization method to be used when
+ /// no specific group membership is required.
+ /// If the user is not authorized, the current user remains to be a current user
+ /// (i.e. the access rights were not risen to a higher level).
+ /// If you require different behavior, use Unauthorize() before calling Authorize().
+ ///
+ ///
+ ///
+ ///
+ /// true = authorized
+ public virtual bool Authorize(string userName, string password)
+ {
+ return Authorize(userName, password, Grp.GID.None);
+ }
+
+ ///
+ /// Returns a 'User' with a given username from a database.
+ ///
+ /// User name for the query
+ /// reference to a 'User' (if it exists) or null
+ public static User AuthorizeDummyUser(string username)
+ {
+ User user = new User();
+ user.Name = username;
+ currentUser = user;
+ return user;
+ }
+
+ ///
+ /// Returns a 'User' with a given username from an ARBITRARY database.
+ ///
+ /// User name for the query
+ /// reference to a 'User' (if it exists) or null
+ public static User LoadUserByName(string username, Config.DBSettings dbSettings)
+ {
+ DB.DbType = dbSettings.DbType;
+ DB.ConnectionString = dbSettings.ConnectionString;
+ IList listOfUsers = DB.CreateSession()
+ .QueryOver()
+ .Where(x => (x.Name.ToLower() == username.ToLower()))
+ .List();
+ if (listOfUsers.Count > 0) return listOfUsers[0];
+ return null;
+ }
+
+ ///
+ /// Returns a 'User' with a given username from the users database.
+ ///
+ /// User name for the query
+ /// reference to a 'User' (if it exists) or null
+ public static User LoadUserByName(string username)
+ {
+ IList listOfUsers = DB.CreateSession()
+ .QueryOver()
+ .Where(x => (x.Name.ToLower() == username.ToLower()))
+ .List();
+
+ if (listOfUsers.Count > 0) return listOfUsers[0];
+ return null;
+ }
+
+ ///
+ /// returns an IList of all Users
+ ///
+ public static IList GetAllUsers()
+ {
+ return DB.CreateSession().QueryOver().List();
+ }
+
+
+ ///
+ /// Unauthorize, abandon current users authorization.
+ ///
+ public static void Unauthorize()
+ {
+ currentUser = null;
+ lastAuthorization = DateTime.Now;
+ }
+
+ ///
+ /// getHash encrypts a string
+ ///
+ /// the string to encrypt
+ ///
+ public static string getHash(string text)
+ {
+ byte[] bytes = Encoding.Unicode.GetBytes(text);
+ SHA512Managed hashstring = new SHA512Managed();
+ byte[] hash = hashstring.ComputeHash(bytes);
+ string hashString = string.Empty;
+ foreach (byte x in hash)
+ {
+ hashString += String.Format("{0:x2}", x);
+ }
+ return hashString;
+ }
+
+ ///
+ /// Returns 'true' when authentication data are valid for a power user.
+ ///
+ /// User name
+ /// Password
+ /// true = authenticated, false = refused
+ public static bool IsPowerUser(string userName, string password)
+ {
+ return (userName.Equals("milan") && password.Equals("napajedla")) ||
+ (userName.Equals("igor") && password.Equals("ronko4")) ||
+ (userName.Equals("vlado") && password.Equals("luskovica.430")) ||
+ (userName.Equals("pakan") && password.Equals("kuriatko")) ||
+ (userName.Equals("gilles") && password.Equals("alibaba"));
+ }
+ }
+}
diff --git a/Users/Grp.cs b/Users/Grp.cs
new file mode 100644
index 000000000..2ba822d3d
--- /dev/null
+++ b/Users/Grp.cs
@@ -0,0 +1,101 @@
+///
+/// Copyright (c) 2016 Sensus Metering Systems
+///
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+using Users.Entities;
+using NHibernate;
+using FluentNHibernate;
+
+namespace Users
+{
+ ///
+ /// Enum Grp.GID specifies GID-s of user groups.
+ /// An instance of this class represenst a group, which contains a list of its members (users).
+ /// Static private array 'groups' of this class, accesible through indexer, contains all groups.
+ ///
+ public class Grp
+ {
+ ///
+ /// GID-s of user groups, each user is member of one or more groups.
+ ///
+ public enum GID
+ {
+ // The first NrOfGroups items are GroupID-s (0..NrOfGroups-1)
+ Testers = 0,
+ TestingSpecialists,
+ HeadOfLab,
+ MaintenanceSpecialists,
+ Metrologists,
+ CalibrationSpecialists,
+ Administrators, // application / network / database administrator
+ Custom1,
+ Custom2,
+ Custom3,
+ NrOfGroups, // Number of groups (this is not a GroupID)
+ None, // Not a GroupID, can be used to indicate no group membership is required
+ Invalid = -1, // Not a GroupID, indicates a variable has not been properly set so far
+ }
+
+ ///
+ /// Number of groups.
+ ///
+ public const int Count = (int)GID.NrOfGroups;
+
+ // Static array of all groups.
+ static Grp[] groups;
+
+ ///
+ /// Static constructor, creates all groups with no members.
+ ///
+ static Grp()
+ {
+ groups = new Grp[Count];
+ for (int i = 0; i < Count; i++)
+ {
+ groups[i] = new Grp((GID)i);
+ }
+ }
+
+ ///
+ /// Returns a Group from Id.
+ ///
+ /// Group Gid
+ /// Entities.Group object reference
+ public static Entities.Group FromId(GID gid)
+ {
+ int i = (int)gid;
+
+ if (i >= 0 && i < Count)
+ {
+ return groups[i].group;
+ }
+ else
+ {
+ return null;
+ }
+ }
+
+ ///////////////////// ^ static ^ ///////////////////// v instance v /////////////////////
+
+ /// Public properties
+ public GID Gid { get { return (GID)group.Gid; } }
+ public string Name { get { return group.Name; } set { group.Name = value; } }
+
+ /// Private fields
+ Entities.Group group;
+
+ ///
+ /// Instance constructor.
+ ///
+ ///
+ public Grp(GID gid)
+ {
+ group = new Entities.Group();
+ this.group.Gid = (int)gid;
+ this.group.Name = gid.ToString();
+ }
+ }
+}
diff --git a/Users/Mappings/GroupMap.cs b/Users/Mappings/GroupMap.cs
new file mode 100644
index 000000000..c0fe9dd66
--- /dev/null
+++ b/Users/Mappings/GroupMap.cs
@@ -0,0 +1,17 @@
+///
+/// Copyright (c) 2016 Sensus Metering Systems
+///
+using FluentNHibernate.Mapping;
+
+namespace Users.Mappings
+{
+ class GroupMap : ClassMap
+ {
+ public GroupMap()
+ {
+ Id(x => x.Id);
+ Map(x => x.Gid);
+ Map(x => x.Name);
+ }
+ }
+}
diff --git a/Users/Mappings/UserMap.cs b/Users/Mappings/UserMap.cs
new file mode 100644
index 000000000..82fd4b2d3
--- /dev/null
+++ b/Users/Mappings/UserMap.cs
@@ -0,0 +1,22 @@
+///
+/// Copyright (c) 2016 Sensus Metering Systems
+///
+using FluentNHibernate.Mapping;
+
+namespace Users.Mappings
+{
+ class UserMap : ClassMap
+ {
+ public UserMap()
+ {
+ Id(x => x.Id);
+ Map(x => x.Name);
+ Map(x => x.Number);
+ Map(x => x.Password);
+ Map(x => x.Description);
+ Map(x => x.LastPwChange);
+ HasMany(x => x.Groups)
+ .Cascade.All();
+ }
+ }
+}
diff --git a/Users/Properties/AssemblyInfo.cs b/Users/Properties/AssemblyInfo.cs
new file mode 100644
index 000000000..4e51be251
--- /dev/null
+++ b/Users/Properties/AssemblyInfo.cs
@@ -0,0 +1,36 @@
+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("Users")]
+[assembly: AssemblyDescription("")]
+[assembly: AssemblyConfiguration("")]
+[assembly: AssemblyCompany("")]
+[assembly: AssemblyProduct("Users")]
+[assembly: AssemblyCopyright("Copyright © 2016")]
+[assembly: AssemblyTrademark("")]
+[assembly: AssemblyCulture("")]
+
+// 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)]
+
+// The following GUID is for the ID of the typelib if this project is exposed to COM
+[assembly: Guid("f05db1fc-0a24-4319-802b-d13e9a4b7efe")]
+
+// Version information for an assembly consists of the following four values:
+//
+// Major Version
+// Minor Version
+// Build Number
+// Revision
+//
+// You can specify all the values or you can default the Build and Revision Numbers
+// by using the '*' as shown below:
+// [assembly: AssemblyVersion("1.0.*")]
+[assembly: AssemblyVersion("2.12.424.0")]
+[assembly: AssemblyFileVersion("2.12.424.0")]
diff --git a/Users/Users.csproj b/Users/Users.csproj
new file mode 100644
index 000000000..3751836be
--- /dev/null
+++ b/Users/Users.csproj
@@ -0,0 +1,83 @@
+
+
+
+ Debug
+ AnyCPU
+ 8.0.30703
+ 2.0
+ {6E5CB0E9-E1B6-4E5D-AC6E-B1049E180F2B}
+ Library
+ Properties
+ Users
+ Users
+ v4.0
+ 512
+
+
+ true
+ full
+ false
+ bin\Debug\
+ DEBUG;TRACE
+ prompt
+ 4
+
+
+ pdbonly
+ true
+ bin\Release\
+ TRACE
+ prompt
+ 4
+
+
+
+ ..\packages\FluentNHibernate.2.0.3.0\lib\net40\FluentNHibernate.dll
+
+
+ ..\packages\Iesi.Collections.4.0.0.4000\lib\net40\Iesi.Collections.dll
+
+
+ ..\packages\log4net.2.0.2\lib\net40-full\log4net.dll
+
+
+ ..\packages\MySql.Data.6.6.5\lib\net40\MySql.Data.dll
+
+
+ ..\packages\NHibernate.4.0.4.4000\lib\net40\NHibernate.dll
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {743DF7DB-C7B6-42EB-986D-0F485E5588E4}
+ Config
+
+
+
+
+
\ No newline at end of file
diff --git a/clean.bat b/clean.bat
index 377d75378..e7e376ab7 100644
--- a/clean.bat
+++ b/clean.bat
@@ -12,3 +12,5 @@ rmdir /s /q TBFSetup\Debug
rmdir /s /q TBFSetup\Release
rmdir /s /q TestBenchFramework\bin
rmdir /s /q TestBenchFramework\obj
+rmdir /s /q Users\bin
+rmdir /s /q Users\obj