Uninstalling a ClickOnce application silently - batch-file

We have an production application that is deployed using Visual Studio's built-in ClickOnce deployment tool. I am writing a batch file to uninstall the application:
rundll32.exe dfshim.dll,ShArpMaintain AppName.application, Culture=neutral,
PublicKeyToken=XXXXXX, processorArchitecture=x86
The batch file works and the application's uninstall is called. However, I'm looking to do this silently. I have tried /Q /q /S /s /Silent but with no joy.
How can I do this?
I do not want to hide the batch file window. Only the ClickOnce window.

Since there seemed to be no good solution for this, I implemented a new ClickOnce uninstaller. It can be called via command-line, from .NET, or integrated into a WiX setup project as custom action.
https://github.com/6wunderkinder/Wunder.ClickOnceUninstaller
We used this for our Wunderlist 2.1 release, where we switched from ClickOnce to a Windows Installer package. It's integrated into the installation process and completely transparent to the user.

I can confirm that WMIC doesn't work for ClickOnce applications. They are just not listed in there...
I thought of putting this here as I've been working on finding how to resolve this for a long time now and couldn't find a complete solution.
I'm rather new in this whole programming thing but I think this could give ideas of how to proceed.
It basically verifies if the application is currently running, and if so, it kills it. Then it checks the registry to find the uninstall string, put it in a batch file and wait for the process to end. Then it does Sendkeys to automatically agree to uninstall. That's it.
namespace MyNameSpace
{
public class uninstallclickonce
{
[System.Runtime.InteropServices.DllImport("user32.dll")]
private static extern IntPtr FindWindow(string lpClassName, string lpWindowName);
[System.Runtime.InteropServices.DllImport("user32.dll")]
private static extern bool SetForegroundWindow(IntPtr hWnd);
private Process process;
private ProcessStartInfo startInfo;
public void isAppRunning()
{
// Run the below command in CMD to find the name of the process
// in the text file.
//
// WMIC /OUTPUT:C:\ProcessList.txt PROCESS get Caption,Commandline,Processid
//
// Change the name of the process to kill
string processNameToKill = "Auto-Crop";
Process [] runningProcesses = Process.GetProcesses();
foreach (Process myProcess in runningProcesses)
{
// Check if given process name is running
if (myProcess.ProcessName == processNameToKill)
{
killAppRunning(myProcess);
}
}
}
private void killAppRunning(Process myProcess)
{
// Ask the user if he wants to kill the process
// now or cancel the installation altogether
DialogResult killMsgBox =
MessageBox.Show(
"Crop-Me for OCA must not be running in order to get the new version\nIf you are ready to close the app, click OK.\nClick Cancel to abort the installation.",
"Crop-Me Still Running",
MessageBoxButtons.OKCancel,
MessageBoxIcon.Question);
switch(killMsgBox)
{
case DialogResult.OK:
//Kill the process
myProcess.Kill();
findRegistryClickOnce();
break;
case DialogResult.Cancel:
//Cancel whole installation
break;
}
}
private void findRegistryClickOnce()
{
string uninstallRegString = null; // Will be ClickOnce Uninstall String
string valueToFind = "Crop Me for OCA"; // Name of the application we want
// to uninstall (found in registry)
string keyNameToFind = "DisplayName"; // Name of the Value in registry
string uninstallValueName = "UninstallString"; // Name of the uninstall string
//Registry location where we find all installed ClickOnce applications
string regProgsLocation =
"Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall";
using (RegistryKey baseLocRegKey = Registry.CurrentUser.OpenSubKey(regProgsLocation))
{
//Console.WriteLine("There are {0} subkeys in here", baseLocRegKey.SubKeyCount.ToString());
foreach (string subkeyfirstlevel in baseLocRegKey.GetSubKeyNames())
{
//Can be used to see what you find in registry
// Console.WriteLine("{0,-8}: {1}", subkeyfirstlevel, baseLocRegKey.GetValueNames());
try
{
string subtest = baseLocRegKey.ToString() + "\\" + subkeyfirstlevel.ToString();
using (RegistryKey cropMeLocRegKey =
Registry.CurrentUser.OpenSubKey(regProgsLocation + "\\" + subkeyfirstlevel))
{
//Can be used to see what you find in registry
// Console.WriteLine("Subkey DisplayName: " + cropMeLocRegKey.GetValueNames());
//For each
foreach (string subkeysecondlevel in cropMeLocRegKey.GetValueNames())
{
// If the Value Name equals the name application to uninstall
if (cropMeLocRegKey.GetValue(keyNameToFind).ToString() == valueToFind)
{
uninstallRegString = cropMeLocRegKey.GetValue(uninstallValueName).ToString();
//Exit Foreach
break;
}
}
}
}
catch (System.Security.SecurityException)
{
MessageBox.Show("security exception?");
}
}
}
if (uninstallRegString != null)
{
batFileCreateStartProcess(uninstallRegString);
}
}
// Creates batch file to run the uninstall from
private void batFileCreateStartProcess(string uninstallRegstring)
{
//Batch file name, which will be created in Window's temps foler
string tempPathfile = Path.GetTempPath() + "cropmeuninstall.bat";
if (!File.Exists(#tempPathfile))
{
using (FileStream createfile = File.Create(#tempPathfile))
{
createfile.Close();
}
}
using (StreamWriter writefile = new StreamWriter(#tempPathfile))
{
//Writes our uninstall value found earlier in batch file
writefile.WriteLine(#"Start " + uninstallRegstring);
}
process = new Process();
startInfo = new ProcessStartInfo();
startInfo.FileName = tempPathfile;
process.StartInfo = startInfo;
process.Start();
process.WaitForExit();
File.Delete(tempPathfile); //Deletes the file
removeClickOnceAuto();
}
// Automation of clicks in the uninstall to remove the
// need of any user interactions
private void removeClickOnceAuto()
{
IntPtr myWindowHandle = IntPtr.Zero;
for (int i = 0; i < 60 && myWindowHandle == IntPtr.Zero; i++)
{
Thread.Sleep(1500);
myWindowHandle = FindWindow(null, "Crop Me for OCA Maintenance");
}
if (myWindowHandle != IntPtr.Zero)
{
SetForegroundWindow(myWindowHandle);
SendKeys.Send("+{TAB}"); // Shift + TAB
SendKeys.Send("{ENTER}");
SendKeys.Flush();
}
}
}
}

You could try to use Hidden Start.

You can not suppress the uninstall dialog for a ClickOnce application. You can write a small .NET application to uninstall the ClickOnce application and programmatically hit the button on the dialog, so no action is required by the user. That's about the best you can do.

Don't over complicate it & keep it simple - this works on both Windows XP & 7:
Go to Add/Remove Programs and make note of the exact name of the program.
Open Notepad and paste the text below:
wmic product where name="PROGRAM NAME" uninstall
but make sure to type the exact name of the program in between the quotation marks and chose Save As /All Files and name the file Uninstall.bat and then test it out to make sure it works.

Related

Run a command line command from WPF application

After seeing a youtube video I tested this code:
string cmd1 = #"xcopy c:\A\*.* c:\b";
Process ps = new Process();
ps.StartInfo.FileName = "cmd.exe";
ps.StartInfo.CreateNoWindow = true;
ps.StartInfo.RedirectStandardInput = true;
ps.StartInfo.RedirectStandardOutput = true;
ps.StartInfo.UseShellExecute = false;
ps.Start();
ps.StandardInput.WriteLine(cmd1);
ps.StandardInput.Flush();
ps.StandardInput.Close();
ps.WaitForExit();
Console.WriteLine(ps.StandardOutput.ReadToEnd());
The command is executed but i see no output.
What do i need to do to make visible the cmd window and the command output?
Thanks
If you want to execute your command in the cmd.exe window, you can do it like this.
var process = new Process();
var startInfo = new ProcessStartInfo
{
FileName = "cmd.exe",
Arguments = #"/K xcopy c:\A\*.* c:\b"
};
process.StartInfo = startInfo;
process.Start();
Note that /K keeps the command line window open, replace it with /C to automatically close it after copying.
If you want to start xcopy without showing the console window and collecting the output to show it where you want, use this.
var process = new Process();
var startInfo = new ProcessStartInfo
{
WindowStyle = ProcessWindowStyle.Hidden,
FileName = "xcopy",
Arguments = #"c:\A\*.* c:\b",
RedirectStandardOutput = true
};
process.StartInfo = startInfo;
process.Start();
var output = process.StandardOutput.ReadToEnd();
process.WaitForExit();
// Print the output to Standard Out
Console.WriteLine(output);
// Print the ouput to e.g. Visual Studio debug window
Debug.WriteLine(output);
Note that this only works as expected, if your B folder exists folder does not already contain any of your files. Otherwise, the window will stay open, because you are asked, whether the directory should be created and the files should be overwritten. To not overcomplicate this by writing input, you could use the following arguments for xcopy.
Arguments = #"c:\A\*.* c:\b\ /Y /I"
The /Y switch will overwrite files without asking and /I will create a non-existing directory. It is mandatory for this to work to have a trailing backslash on your destination directory path (c:\b\ instead of c:\b).

Proper way to upgrade WPF program settings on program update (Desktop Bridge/Project Centennial)

When it was a clickonce program it worked, but then I made an appxpackage and exported it as a centennial app for windows store and the upgrade does not work any more.
Right now I have in App.xaml.cs
protected override void OnStartup(StartupEventArgs e) {
if (myprog.Properties.Settings.Default.UpgradeRequired)
{
myprog.Properties.Settings.Default.Upgrade();
myprog.Properties.Settings.Default.UpgradeRequired = false;
myprog.Properties.Settings.Default.Save();
}
With UpgradeRequired as a bool in user settings. Is that the right place?
I am getting settings reset on each version update. Now I have several of these directories
C:\Users\me\AppData\Local\progname\prog.exe_Url_randomChars
each with several different version of program settings. So after the upgrade another one of those is created, instead a subfolder with x.x.x.x of the current version.
As before, on each version release I increase version in Assembly Information the Assembly Version, File Version, and now I have the same numbers in AppxManifest.xml. I keep the last number group of the version to 0 as advised, and just increase the 3rd number group.
Is there something I am missing?
UWP and Desktop Bridge apps need to store their settings in ApplicationData.LocalSettings:
https://learn.microsoft.com/en-us/windows/uwp/design/app-settings/store-and-retrieve-app-data#local-app-data
You could load the previous user.config file into current settings. This is just a workaround, and can be used to transition to ApplicationData.LocalSettings.
public static void Init()
{
if (myprog.Properties.Settings.Default.UpgradeRequired)
{
LoadPreviousSettings(myprog.Properties.Settings.Default);
myprog.Properties.Settings.Default.UpgradeRequired = false;
myprog.Properties.Settings.Default.Save();
}
}
private static void LoadPreviousSettings(params ApplicationSettingsBase[] applicationSettings)
{
const string companyName = "YOUR_COMPANY_NAME_HERE";
var userConfigXml = GetUserConfigXml(companyName);
Configuration config = ConfigurationManager.OpenExeConfiguration(
ConfigurationUserLevel.PerUserRoamingAndLocal);
foreach (ApplicationSettingsBase setting in applicationSettings)
{
try
{
// loads settings from user.config into configuration
LoadSettingSection(setting, config, userConfigXml);
config.Save(ConfigurationSaveMode.Modified);
ConfigurationManager.RefreshSection("userSettings");
}
catch (FileNotFoundException)
{
// Could not import settings.
// Perhaps user has no previous version installed
}
setting.Reload();
}
}
private static void LoadSettingSection(ApplicationSettingsBase setting, Configuration config, XDocument userConfigXml)
{
string appSettingsXmlName = setting.ToString();
var settings = userConfigXml.XPathSelectElements("//" + appSettingsXmlName);
config.GetSectionGroup("userSettings")
.Sections[appSettingsXmlName]
.SectionInformation
.SetRawXml(settings.Single().ToString());
}
private static XDocument GetUserConfigXml(string companyName)
{
var localPath = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData) + $#"\{companyName}";
// previous package folder
var previousLocal = GetDirectoryByWriteTime(localPath, 1);
// previous version, e.g. 1.2.0
var prevousVersion = GetDirectoryByWriteTime(previousLocal, 0);
// application settings for previous version
return XDocument.Load(prevousVersion + #"\user.config");
}
private static string GetDirectoryByWriteTime(string path, int order)
{
var direcotires = new DirectoryInfo(path).EnumerateDirectories()
.OrderBy(d => d.LastWriteTime)
.Reverse()
.ToList();
if (direcotires.Count > order)
{
var previous = direcotires[order];
return previous.FullName;
}
throw new FileNotFoundException("Previous config file not found.");
}
There is a working answer here.
Basically you need to create a duplicate version using UWP's ApplicationData.Settings and then loading it at the beginning of the app. It is very straightforward when your settings are strings, bools, etc. But not so if you have unique settings.
To elaborate more from the answer in the link, when you have settings consisting of custom types/classes, when creating UWP's duplicate version, you can use Newtonsoft.Json to serialise the custom setting:
try
{
ApplicationData.Current.LocalSettings.Values[value.Name] = value.PropertyValue;
}
catch
{
string serialised = JsonConvert.SerializeObject(value.PropertyValue);
ApplicationData.Current.LocalSettings.Values[value.Name] = serialised;
}
Then when loading your custom setting:
if (s.Name == "MyCustomSetting")
{
var deserialised = JsonConvert.DeserializeObject<MyCustomClass>((string)setting.Value);
s.PropertyValue = deserialised;
}

how to start console application with windows application and read (monitoring) command line - line by line real time in c#

I have a console application which generating rows line by line:
Data1
Data2
Data3
...
and when its over command line will be cleared, it reapeats infinitelly (the datas can change)
I have to watch the console application's command line with windows aplication real time and work for the lines data (for example save it to list ox line by line)! It is possible?
You basically need to subscribe to the output streams of that console application, to be able to get each line printed on the console.
What you need to do is to create the Windows Forms application (WPF would also work) and start the console application from there.
If you don't want to show your current console application as a visible window, remember to set CreateNoWindow to true.
Here's how to start the console application:
var processStartInfo = new ProcessStartInfo(fileName, arguments);
processStartInfo.UseShellExecute = false;
processStartInfo.ErrorDialog = false;
processStartInfo.RedirectStandardOutput = true; // We handle the output
processStartInfo.CreateNoWindow = true; // If you want to hide the console application so it only works in the background.
// Create the actual process
currentProcess = new Process();
currentProcess.EnableRaisingEvents = true;
currentProcess.StartInfo = processStartInfo;
// Start the process
bool processDidStart = currentProcess.Start();
We need a BackgroundWorker to read the output from the console application in the background.
outputReader = TextReader.Synchronized(currentProcess.StandardOutput);
outputWorker.RunWorkerAsync();
Now you're able to get all the output from the console application in real time and use it to create a list or whatever you want to.
void outputWorker_DoWork(object sender, DoWorkEventArgs e)
{
// Work until cancelled
while (outputWorker.CancellationPending == false)
{
int count = 0;
char[] buffer = new char[1024];
do
{
StringBuilder builder = new StringBuilder();
// Read the data from the buffer and append to the StringBuilder
count = outputReader.Read(buffer, 0, 1024);
builder.Append(buffer, 0, count);
outputWorker.ReportProgress(0, new OutputEvent() { Output = builder.ToString() });
} while (count > 0);
}
}
The processed data is available through the ProgressChanged event of the BackgroundWorker.
void outputWorker_ProgressChanged(object sender, ProgressChangedEventArgs e)
{
if (e.UserState is OutputEvent)
{
var outputEvent = e.UserState as OutputEvent;
/* HERE YOU CAN USE THE OUTPUT LIKE THIS:
* outputEvent.Output
*
* FOR EXAMPLE:
* yourList.Add(outputEvent.Output);
*/
}
}
The above code is taken and modified to your purposes from the following Codeproject.com article in case it ceases to exist in the future: Embedding a Console in a C# Application.

Attach a PDF File to a Print Dialog

I'm trying to attach a PDF File to a Print Dialog, but I haven't find out the way to do it.
I'm using a WPF app, and I have some code related with printing and looks like this:
private void Imprimir()
{
try
{
FixedDocument document = null;
PageContent pageContent = null;
FixedPage fixedPage = null;
PrintDialog printDlg = new PrintDialog();
if (printDlg.ShowDialog() != true)
return;
document.DocumentPaginator.PageSize = new System.Windows.Size(1400, 1450);
fixedPage.Width = document.DocumentPaginator.PageSize.Width;
fixedPage.Height = document.DocumentPaginator.PageSize.Height;
fixedPage.Margin = new Thickness(96, 96, 0, 0);
fixedPage.Children.Add(this);
((System.Windows.Markup.IAddChild)pageContent).AddChild(fixedPage);
document.Pages.Add(pageContent);
printDlg.PrintDocument(document.DocumentPaginator, "Impresion Cierre");
fixedPage.Children.Clear();
}
catch (Exception ex)
{
System.Windows.MessageBox.Show(ex.Message);
}
}
But, by this way I'm just printing a UI Element added to Fixed Page.
I've looked for other codes, but I find nothing.
So, I donĀ“t know if is possible to add a PDF File stored locally to the Print Dialog ?
Thanks for the help...
Well you cannot do this using PrintDialog. There are several options, depending on your goals:
var printQueue = LocalPrintServer.GetDefaultPrintQueue();
using (var input = File.OpenRead("path_to_your.pdf")) {
using (var job = printQueue.AddJob()) {
using (var output = job.JobStream) {
input.CopyTo(output);
}
}
}
Will silently send print job for your file to local print queue. Print job is configurable.
Alternatively you can use adobe reader to handle that for you (or another pdf reader installed on user's machine) but starting process with path to your pdf as FileName and Verb="print".
One more option is to use third party tools (like ghostscript) which can help you with that.

Whats wrong with my backgroundwork method

I am trying to get a background worker process working in a wpf application. it creates 2 files then crashes.
BackgroundWorker worker = new BackgroundWorker();
worker.DoWork += delegate(object s, DoWorkEventArgs args)
{
CreateFile(i.ToString());
};
worker.RunWorkerAsync();
private void CreateFile(string fileName)
{
string path = string.Format(#"{0}\{1}.txt", directory, fileName);
using (StreamWriter sw = new StreamWriter(path))
{
sw.WriteLine(fileName);
}
}
I get this error "
The requested operation cannot be performed on a file with a user-mapped section open." what am I doing wrong?
Any help would be great
Another process has the file open, e.g., an antivirus program or WordPad. You can use Process Monitor to see which process it is.

Resources