namespace VAL
{
using System;
using System.Diagnostics;
using VAL.Model;
using VAL.Model.Entities;
using VAL.Core;
using VAL.Contracts;
using System.Threading.Tasks;
/// <summary>
/// The Launcher class starts new processes.
/// </summary>
internal sealed class Launcher
{
/// <summary>
/// Access to the log4Net logging object
/// </summary>
private static readonly log4net.ILog log = log4net.LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);
private const int MaximumWaitForIdleTime = 5000;
private const string WhiteSpace = " ";
/// <summary>
/// Set to true the first time we find the Access Executable, so we
/// don't have to attempt File.Exists each time we launch a
/// database
/// </summary>
private static bool accessExists;
#region Access Switches Class
/// <summary>
/// This class contains all of the command line switches that are used by Microsoft
/// Access when launching databases
/// </summary>
private class AccessSwitches
{
/// <summary>
/// Opens the specified Access database for exclusive access. To open the database for shared access in a multiuser environment, omit this option. Applies to Access databases only. "
/// </summary>
public const string Exclusive = "/excl";
/// <summary>
/// Opens the specified Access database or Access project for read-only access.
/// </summary>
public const string ReadOnly = "/ro";
/// <summary>
/// User name Starts Access by using the specified user name. Applies to Access databases only.
/// </summary>
public const string UserName = "/user";
/// <summary>
/// Password Starts Access by using the specified password. Applies to Access databases only.
/// </summary>
public const string Password = "/pwd";
/// <summary>
/// User profile Starts Access by using the options in the specified user profile instead of the standard Windows Registry settings created when you installed Microsoft Access. This replaces the /ini option used in versions of Microsoft Access prior to Access 97 to specify an initialization file.
/// </summary>
public const string Profile = "/profile";
/// <summary>
/// Target database or target Access project Compacts and repairs the Access database, or compacts the Access project that was specified before the /compact option, and then closes Access. If you omit a target file name following the /compact option, the file is compacted to the original name and folder. To compact to a different name, specify a target file. If you don't include a path in target database or target Access project, the target file is created in your My Documents folder by default.
/// In an Access project, this option compacts the Access project (.adp) file but not the Microsoft SQL Server database.
/// </summary>
public const string Compact = "/compact";
/// <summary>
/// Repairs the Access database that was specified before the /repair option, and then closes Microsoft Access. In Microsoft Access 2000 or later, compact and repair functionality is combined under /compact. The /repair option is supported for backward compatibility.
/// </summary>
public const string Repair = "/repair";
/// <summary>
/// Starts Access and runs the specified macro. Another way to run a macro when you open a database is to use an AutoExec macro.
/// </summary>
public const string Macro = "/x";
/// <summary>
/// Specifies that what follows on the command line is the value that will be returned by the Command function. This option must be the last option on the command line. You can use a semicolon (;) as an alternative to /cmd.
/// Use this option to specify a command-line argument that can be used in Visual Basic code.
/// </summary>
public const string Command = "/cmd";
/// <summary>
/// Starts Access by using the specified workgroup information file. Applies to Access databases only.
/// </summary>
public const string Workgroup = "/wrkgrp ";
}
#endregion
/// <summary>
/// No construct
/// </summary>
private Launcher() { }
/// <summary>
/// Try and find access
/// </summary>
/// <param name="directory"></param>
/// <returns></returns>
private static string FindAccess(string directory)
{
string microsoftAccess = string.Empty;
try
{
foreach (string d in System.IO.Directory.GetDirectories(directory))
{
string[] f = System.IO.Directory.GetFiles(d, "msaccess.exe");
if (f.Length > 0)
microsoftAccess = f[0];
if (microsoftAccess.Length > 0)
break;
FindAccess(d);
}
}
catch
{
}
return microsoftAccess;
}
/// <summary>
/// Launches a new process
/// </summary>
/// <param name="file">The <see cref="File"/> to launch</param>
public static void LaunchFile(File file)
{
ProcessStartInfo psi = new ProcessStartInfo();
FileTypeEnum filetype = (FileTypeEnum)file.FileTypeID;
bool requiresDistribution = (file.DistributeToUserProfile ||
!string.IsNullOrEmpty(file.DistributeTo));
string filePath = file.Path;
// See if we are distributing a local copy
if (requiresDistribution)
{
filePath = System.IO.Path.GetDirectoryName(DistributeFile(file)) +
System.IO.Path.DirectorySeparatorChar.ToString();
}
switch (filetype)
{
// If this is a database, we need to launch it via the
// Access executable file. Therefore, the arguments are file name, workgroup etc
case FileTypeEnum.Database:
#region Find Access Executable
// Get the location from config, test that it exists
if (!accessExists)
{
bool requiresSearch = true;
if (!string.IsNullOrEmpty(UserOptions.Current.AccessExecutableLocation))
{
requiresSearch = !System.IO.File.Exists(UserOptions.Current.AccessExecutableLocation);
}
// If we don't have a setting yet or the filename currently stored in the user settings
// file doesn't exist (user may have signed on to another computer since last launch of VAL)
// then we need to try and find Access
if (requiresSearch)
{
UserOptions.Current.AccessExecutableLocation = FindAccess(Session.Configuration.MicrosoftOfficeLocation);
// If we still haven't got a setting after searching, we can't launch Access databases
if (string.IsNullOrEmpty(UserOptions.Current.AccessExecutableLocation))
{
throw new System.IO.FileNotFoundException("Microsoft Access cannot be located in any sub-directory of '"
+ Session.Configuration.MicrosoftOfficeLocation + "', you cannot launch Access databases");
}
}
accessExists = true;
}
#endregion
// Set up the access exe as the main Win32 executable target
psi.FileName = UserOptions.Current.AccessExecutableLocation;
// Now add the database to open it with
psi.Arguments += @"""" + String.Concat(filePath, file.Name) + @"""";
// See if we're using a workgroup
if (!string.IsNullOrEmpty(file.Workgroup))
{
// Add in the workgroup info - if we're set to distribute workgroups then we
// should launch via the local copy
if (Session.Configuration.DistributeWorkgroups && requiresDistribution)
{
psi.Arguments += (AccessSwitches.Workgroup +
String.Concat(filePath + System.IO.Path.GetFileName(file.Workgroup)));
}
else
{
psi.Arguments += (AccessSwitches.Workgroup + file.Workgroup);
}
}
// See if the user wants to autosign on
if (!string.IsNullOrEmpty(file.Workgroup) && UserOptions.Current.AutoSignOn)
{
psi.Arguments += WhiteSpace + (AccessSwitches.UserName + WhiteSpace + UserOptions.Current.UserName);
psi.Arguments += WhiteSpace + (AccessSwitches.Password + WhiteSpace + UserOptions.Current.GetDecryptedPassword());
}
break;
default:
// Otherwise, it must be an executable file type that we can
// just start up with no probs!
psi.FileName = String.Concat(filePath, file.Name);
if (!string.IsNullOrEmpty(file.Command))
{
psi.Arguments += WhiteSpace + file.Command;
}
break;
}
psi.UseShellExecute = true;
psi.WindowStyle = ProcessWindowStyle.Normal;
psi.ErrorDialog = true;
if (!System.IO.File.Exists(psi.FileName))
{
throw new System.IO.FileNotFoundException("The file '" + psi.FileName + "' could not be found");
}
Process proc = Process.Start(psi);
try
{
proc.WaitForInputIdle(MaximumWaitForIdleTime);
}
catch (InvalidOperationException)
{
// Not much we can do about this...probably no UI on the application?
}
#region Reporting Activity
if (Session.Configuration.IsReportingEnabled)
{
var task = Task.Factory.StartNew(() =>
{
// Log the file launched by the user
var activity = new UserActivity
{
FileID = file.Id,
UserID = Session.CurrentUser.Id,
LaunchDatetime = System.DateTime.Now,
LaunchedFrom = Environment.MachineName
};
try
{
using (var service = new VAL.ClientServices.UserServiceClient())
{
service.RecordUserActivity(activity);
}
}
catch (Exception ex)
{
// Just log this - it's only a problem with reporting the
// data so we'll just silently log + continue
log.Error(ex.Message);
}
});
}
#endregion
}
/// <summary>
/// Method that helps with distributing files from their main location to another location that is 'local' to the user.
/// </summary>
/// <remarks>
/// In many cases, there will be a network version of an Access database that
/// each user will have their own front-end copy of. This class checks the local
/// copy and distributes the network version if it is newer
/// </remarks>
/// <param name="file">The instance of the file to distribute</param>
private static string DistributeFile(File file)
{
string targetDirectory = string.Empty;
string applicationName =
System.IO.Path.GetFileNameWithoutExtension(file.Name).Replace(WhiteSpace, string.Empty);
if (file.DistributeToUserProfile)
{
targetDirectory = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments);
}
else
{
targetDirectory = file.DistributeTo;
}
// Regardless of 'target' directory, wrap up every distributed file into their own directories
// simply to avoid cluttering root directories too much
targetDirectory += @"\VAL\" + applicationName + System.IO.Path.DirectorySeparatorChar.ToString();
string networkDatabase = String.Concat(file.Path, file.Name);
string localDatabase = String.Concat(targetDirectory, file.Name);
string localWorkgroup = String.Empty;
// See if we also need to copy a workgroup
if (!string.IsNullOrEmpty(file.Workgroup) && Session.Configuration.DistributeWorkgroups)
{
localWorkgroup = String.Concat(targetDirectory + System.IO.Path.GetFileName(file.Workgroup));
}
DateTime networkVersion = System.IO.File.GetLastWriteTime(networkDatabase);
// Set the date to a version that networkVersion will always be greater
// than to force an inital copy. I doubt they had Access in 1900...maybe
// a really annoying, buggy version
DateTime localVersion = new DateTime(1900, 1, 1);
// Check that the path we are distributing to actually exists
if (!System.IO.Directory.Exists(System.IO.Path.GetDirectoryName(localDatabase)))
{
System.IO.Directory.CreateDirectory(System.IO.Path.GetDirectoryName(localDatabase));
}
// Get the two date times for comparison
if (System.IO.File.Exists(localDatabase))
{
localVersion = System.IO.File.GetLastWriteTime(localDatabase);
}
// See if we need to copy a later version of the db
if (networkVersion > localVersion)
{
System.IO.File.Copy(networkDatabase, localDatabase, true);
}
// See if we need to copy a workgroup as well
if (!string.IsNullOrEmpty(localWorkgroup))
{
if (!System.IO.File.Exists(localWorkgroup) ||
System.IO.File.GetLastWriteTime(file.Workgroup) > System.IO.File.GetLastWriteTime(localWorkgroup))
{
System.IO.File.Copy(file.Workgroup, localWorkgroup, true);
}
}
return localDatabase;
}
}
}