Initial release v1.01 — InstaSoft Office Tool
Office deployment wizard for InstaSoft customers: - Install Office 2019/2021/2024 (Standard, Professional Plus, Home & Business) - Auto-download ODT from Microsoft, generate config XML, run setup - Remove existing Office installations (C2R + MSI) - License troubleshooting via ospp.vbs (dstatus, unpkey) - Fluent Design UI (WPF .NET Framework 4.8, Win7+ compatible) - Hungarian interface, multi-language Office installation - Product key input with auto-activation support Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
119
Services/LicenseManager.cs
Normal file
119
Services/LicenseManager.cs
Normal file
@@ -0,0 +1,119 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace InstaSoftOfficeTool.Services
|
||||
{
|
||||
public class LicenseManager
|
||||
{
|
||||
public string OsppPath { get; private set; }
|
||||
|
||||
public bool FindOspp()
|
||||
{
|
||||
var searchPaths = new[]
|
||||
{
|
||||
// Click-to-Run (leggyakoribb — Office 365, 2019, 2021, 2024)
|
||||
@"C:\Program Files\Microsoft Office\root\Office16\OSPP.VBS",
|
||||
@"C:\Program Files (x86)\Microsoft Office\root\Office16\OSPP.VBS",
|
||||
// Hagyományos MSI
|
||||
@"C:\Program Files\Microsoft Office\Office16\ospp.vbs",
|
||||
@"C:\Program Files (x86)\Microsoft Office\Office16\ospp.vbs",
|
||||
@"C:\Program Files\Microsoft Office\Office15\ospp.vbs",
|
||||
@"C:\Program Files (x86)\Microsoft Office\Office15\ospp.vbs",
|
||||
@"C:\Program Files\Microsoft Office\Office14\ospp.vbs",
|
||||
@"C:\Program Files (x86)\Microsoft Office\Office14\ospp.vbs",
|
||||
};
|
||||
|
||||
foreach (var path in searchPaths)
|
||||
{
|
||||
if (File.Exists(path))
|
||||
{
|
||||
OsppPath = path;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var c2rPath = Microsoft.Win32.Registry.LocalMachine.OpenSubKey(
|
||||
@"SOFTWARE\Microsoft\Office\ClickToRun\Configuration")
|
||||
?.GetValue("InstallationPath") as string;
|
||||
|
||||
if (!string.IsNullOrEmpty(c2rPath))
|
||||
{
|
||||
// C2R: root\Office16 vagy sima Office16
|
||||
var candidates = new[]
|
||||
{
|
||||
Path.Combine(c2rPath, "root", "Office16", "OSPP.VBS"),
|
||||
Path.Combine(c2rPath, "Office16", "OSPP.VBS"),
|
||||
};
|
||||
foreach (var candidate in candidates)
|
||||
{
|
||||
if (File.Exists(candidate))
|
||||
{
|
||||
OsppPath = candidate;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public async Task<string> GetStatusAsync()
|
||||
{
|
||||
if (string.IsNullOrEmpty(OsppPath))
|
||||
return "Az ospp.vbs nem tal\u00e1lhat\u00f3.";
|
||||
|
||||
var runner = new ProcessRunner();
|
||||
return await runner.RunAndCaptureAsync("cscript",
|
||||
"//Nologo \"" + OsppPath + "\" /dstatus");
|
||||
}
|
||||
|
||||
public List<string> ParseLicenseKeys(string dstatusOutput)
|
||||
{
|
||||
var keys = new List<string>();
|
||||
var regex = new Regex(@"Last 5 characters of installed product key:\s*(\S+)",
|
||||
RegexOptions.IgnoreCase);
|
||||
|
||||
foreach (Match match in regex.Matches(dstatusOutput))
|
||||
{
|
||||
keys.Add(match.Groups[1].Value);
|
||||
}
|
||||
|
||||
return keys;
|
||||
}
|
||||
|
||||
public async Task<string> RemoveKeyAsync(string last5Chars)
|
||||
{
|
||||
if (string.IsNullOrEmpty(OsppPath))
|
||||
return "Az ospp.vbs nem tal\u00e1lhat\u00f3.";
|
||||
|
||||
var runner = new ProcessRunner();
|
||||
return await runner.RunAndCaptureAsync("cscript",
|
||||
"//Nologo \"" + OsppPath + "\" /unpkey:" + last5Chars);
|
||||
}
|
||||
|
||||
public async Task<string> RemoveAllKeysAsync()
|
||||
{
|
||||
var status = await GetStatusAsync();
|
||||
var keys = ParseLicenseKeys(status);
|
||||
|
||||
if (keys.Count == 0)
|
||||
return "Nem tal\u00e1lhat\u00f3 telep\u00edtett term\u00e9kkulcs.";
|
||||
|
||||
var results = new List<string>();
|
||||
foreach (var key in keys)
|
||||
{
|
||||
var result = await RemoveKeyAsync(key);
|
||||
results.Add(key + ": " + result.Trim());
|
||||
}
|
||||
|
||||
return string.Join("\n", results);
|
||||
}
|
||||
}
|
||||
}
|
||||
96
Services/OdtDownloader.cs
Normal file
96
Services/OdtDownloader.cs
Normal file
@@ -0,0 +1,96 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Net;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace InstaSoftOfficeTool.Services
|
||||
{
|
||||
public class OdtDownloader
|
||||
{
|
||||
private const string OdtDownloadUrl = "https://go.microsoft.com/fwlink/p/?LinkID=626065";
|
||||
|
||||
public event Action<string> StatusChanged;
|
||||
public event Action<int> ProgressChanged;
|
||||
|
||||
public string OdtFolder { get; private set; }
|
||||
public string SetupExePath { get; private set; }
|
||||
|
||||
public OdtDownloader()
|
||||
{
|
||||
OdtFolder = Path.Combine(Path.GetTempPath(), "InstaSoftODT");
|
||||
}
|
||||
|
||||
public async Task<bool> DownloadAndExtractAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
Directory.CreateDirectory(OdtFolder);
|
||||
|
||||
var odtExePath = Path.Combine(OdtFolder, "officedeploymenttool.exe");
|
||||
SetupExePath = Path.Combine(OdtFolder, "setup.exe");
|
||||
|
||||
if (File.Exists(SetupExePath))
|
||||
{
|
||||
StatusChanged?.Invoke("Az ODT setup.exe m\u00e1r el\u00e9rhet\u0151, let\u00f6lt\u00e9s kihagyva.");
|
||||
return true;
|
||||
}
|
||||
|
||||
StatusChanged?.Invoke("Office Deployment Tool let\u00f6lt\u00e9se...");
|
||||
|
||||
using (var client = new WebClient())
|
||||
{
|
||||
client.DownloadProgressChanged += (s, e) =>
|
||||
{
|
||||
ProgressChanged?.Invoke(e.ProgressPercentage);
|
||||
};
|
||||
|
||||
await client.DownloadFileTaskAsync(new Uri(OdtDownloadUrl), odtExePath);
|
||||
}
|
||||
|
||||
StatusChanged?.Invoke("ODT kicsomagol\u00e1sa...");
|
||||
|
||||
var runner = new ProcessRunner();
|
||||
var exitCode = await runner.RunAsync(odtExePath,
|
||||
"/extract:\"" + OdtFolder + "\" /quiet");
|
||||
|
||||
if (exitCode != 0)
|
||||
{
|
||||
StatusChanged?.Invoke("Hiba: Az ODT kicsomagol\u00e1sa sikertelen (k\u00f3d: " + exitCode + ")");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!File.Exists(SetupExePath))
|
||||
{
|
||||
StatusChanged?.Invoke("Hiba: A setup.exe nem tal\u00e1lhat\u00f3 a kicsomagol\u00e1s ut\u00e1n.");
|
||||
return false;
|
||||
}
|
||||
|
||||
StatusChanged?.Invoke("ODT sikeresen let\u00f6ltve \u00e9s kicsomagolva.");
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
StatusChanged?.Invoke("Hiba a let\u00f6lt\u00e9s sor\u00e1n: " + ex.Message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<int> RunSetupAsync(string configXmlPath, Action<string> outputCallback)
|
||||
{
|
||||
var runner = new ProcessRunner();
|
||||
runner.OutputReceived += line => outputCallback?.Invoke(line);
|
||||
|
||||
StatusChanged?.Invoke("Office telep\u00edt\u00e9s ind\u00edt\u00e1sa...");
|
||||
return await runner.RunAsync(SetupExePath, "/configure \"" + configXmlPath + "\"");
|
||||
}
|
||||
|
||||
public async Task<int> RunRemoveAsync(string configXmlPath, Action<string> outputCallback)
|
||||
{
|
||||
var runner = new ProcessRunner();
|
||||
runner.OutputReceived += line => outputCallback?.Invoke(line);
|
||||
|
||||
StatusChanged?.Invoke("Office elt\u00e1vol\u00edt\u00e1s ind\u00edt\u00e1sa...");
|
||||
return await runner.RunAsync(SetupExePath, "/configure \"" + configXmlPath + "\"");
|
||||
}
|
||||
}
|
||||
}
|
||||
55
Services/OdtXmlGenerator.cs
Normal file
55
Services/OdtXmlGenerator.cs
Normal file
@@ -0,0 +1,55 @@
|
||||
using System.Xml.Linq;
|
||||
using InstaSoftOfficeTool.Models;
|
||||
|
||||
namespace InstaSoftOfficeTool.Services
|
||||
{
|
||||
public static class OdtXmlGenerator
|
||||
{
|
||||
public static string Generate(InstallConfig config)
|
||||
{
|
||||
var product = new XElement("Product",
|
||||
new XAttribute("ID", config.Edition.ProductId));
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(config.ProductKey))
|
||||
{
|
||||
product.Add(new XAttribute("PIDKEY", config.ProductKey.Replace("-", "").Trim()));
|
||||
}
|
||||
|
||||
product.Add(new XElement("Language", new XAttribute("ID", config.Language)));
|
||||
|
||||
foreach (var app in config.ExcludedApps)
|
||||
{
|
||||
product.Add(new XElement("ExcludeApp", new XAttribute("ID", app)));
|
||||
}
|
||||
|
||||
var add = new XElement("Add",
|
||||
new XAttribute("OfficeClientEdition", config.Architecture),
|
||||
new XAttribute("Channel", config.Edition.Channel),
|
||||
product);
|
||||
|
||||
var configuration = new XElement("Configuration",
|
||||
add,
|
||||
new XElement("Display",
|
||||
new XAttribute("Level", "Full"),
|
||||
new XAttribute("AcceptEULA", "TRUE")),
|
||||
new XElement("Property",
|
||||
new XAttribute("Name", "FORCEAPPSHUTDOWN"),
|
||||
new XAttribute("Value", "TRUE")));
|
||||
|
||||
var doc = new XDocument(new XDeclaration("1.0", "utf-8", null), configuration);
|
||||
return doc.Declaration + "\n" + doc.Root;
|
||||
}
|
||||
|
||||
public static string GenerateRemoveAll()
|
||||
{
|
||||
var configuration = new XElement("Configuration",
|
||||
new XElement("Remove", new XAttribute("All", "TRUE")),
|
||||
new XElement("Display",
|
||||
new XAttribute("Level", "Full"),
|
||||
new XAttribute("AcceptEULA", "TRUE")));
|
||||
|
||||
var doc = new XDocument(new XDeclaration("1.0", "utf-8", null), configuration);
|
||||
return doc.Declaration + "\n" + doc.Root;
|
||||
}
|
||||
}
|
||||
}
|
||||
104
Services/OfficeDetector.cs
Normal file
104
Services/OfficeDetector.cs
Normal file
@@ -0,0 +1,104 @@
|
||||
using System.Collections.Generic;
|
||||
using InstaSoftOfficeTool.Models;
|
||||
using Microsoft.Win32;
|
||||
|
||||
namespace InstaSoftOfficeTool.Services
|
||||
{
|
||||
public static class OfficeDetector
|
||||
{
|
||||
public static List<InstalledOffice> Detect()
|
||||
{
|
||||
var results = new List<InstalledOffice>();
|
||||
|
||||
// Check Click-to-Run
|
||||
DetectClickToRun(results);
|
||||
|
||||
// Check MSI-based installs
|
||||
DetectMsi(results);
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
private static void DetectClickToRun(List<InstalledOffice> results)
|
||||
{
|
||||
try
|
||||
{
|
||||
using (var key = Registry.LocalMachine.OpenSubKey(
|
||||
@"SOFTWARE\Microsoft\Office\ClickToRun\Configuration"))
|
||||
{
|
||||
if (key == null) return;
|
||||
|
||||
var productIds = key.GetValue("ProductReleaseIds") as string;
|
||||
var versionToReport = key.GetValue("VersionToReport") as string;
|
||||
|
||||
if (!string.IsNullOrEmpty(productIds))
|
||||
{
|
||||
results.Add(new InstalledOffice
|
||||
{
|
||||
DisplayName = "Microsoft Office Click-to-Run (" + productIds + ")",
|
||||
Version = versionToReport ?? "",
|
||||
IsClickToRun = true,
|
||||
IsSelected = true
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
private static void DetectMsi(List<InstalledOffice> results)
|
||||
{
|
||||
var uninstallPaths = new[]
|
||||
{
|
||||
@"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall",
|
||||
@"SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall"
|
||||
};
|
||||
|
||||
foreach (var path in uninstallPaths)
|
||||
{
|
||||
try
|
||||
{
|
||||
using (var key = Registry.LocalMachine.OpenSubKey(path))
|
||||
{
|
||||
if (key == null) continue;
|
||||
|
||||
foreach (var subKeyName in key.GetSubKeyNames())
|
||||
{
|
||||
using (var subKey = key.OpenSubKey(subKeyName))
|
||||
{
|
||||
if (subKey == null) continue;
|
||||
|
||||
var displayName = subKey.GetValue("DisplayName") as string;
|
||||
var publisher = subKey.GetValue("Publisher") as string;
|
||||
|
||||
if (displayName != null &&
|
||||
publisher != null &&
|
||||
publisher.Contains("Microsoft") &&
|
||||
(displayName.Contains("Microsoft Office") ||
|
||||
displayName.Contains("Microsoft 365")))
|
||||
{
|
||||
var version = subKey.GetValue("DisplayVersion") as string ?? "";
|
||||
|
||||
// Skip Click-to-Run updater entries
|
||||
if (displayName.Contains("Click-to-Run") &&
|
||||
!displayName.Contains("Microsoft Office"))
|
||||
continue;
|
||||
|
||||
results.Add(new InstalledOffice
|
||||
{
|
||||
DisplayName = displayName,
|
||||
Version = version,
|
||||
ProductCode = subKeyName,
|
||||
IsClickToRun = false,
|
||||
IsSelected = true
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
63
Services/ProcessRunner.cs
Normal file
63
Services/ProcessRunner.cs
Normal file
@@ -0,0 +1,63 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace InstaSoftOfficeTool.Services
|
||||
{
|
||||
public class ProcessRunner
|
||||
{
|
||||
public event Action<string> OutputReceived;
|
||||
|
||||
public async Task<int> RunAsync(string fileName, string arguments, bool redirectOutput = true)
|
||||
{
|
||||
var psi = new ProcessStartInfo
|
||||
{
|
||||
FileName = fileName,
|
||||
Arguments = arguments,
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true,
|
||||
RedirectStandardOutput = redirectOutput,
|
||||
RedirectStandardError = redirectOutput,
|
||||
StandardOutputEncoding = redirectOutput ? Encoding.UTF8 : null,
|
||||
StandardErrorEncoding = redirectOutput ? Encoding.UTF8 : null
|
||||
};
|
||||
|
||||
using (var process = new Process { StartInfo = psi })
|
||||
{
|
||||
if (redirectOutput)
|
||||
{
|
||||
process.OutputDataReceived += (s, e) =>
|
||||
{
|
||||
if (e.Data != null)
|
||||
OutputReceived?.Invoke(e.Data);
|
||||
};
|
||||
process.ErrorDataReceived += (s, e) =>
|
||||
{
|
||||
if (e.Data != null)
|
||||
OutputReceived?.Invoke("[HIBA] " + e.Data);
|
||||
};
|
||||
}
|
||||
|
||||
process.Start();
|
||||
|
||||
if (redirectOutput)
|
||||
{
|
||||
process.BeginOutputReadLine();
|
||||
process.BeginErrorReadLine();
|
||||
}
|
||||
|
||||
await Task.Run(() => process.WaitForExit());
|
||||
return process.ExitCode;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<string> RunAndCaptureAsync(string fileName, string arguments)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
OutputReceived += line => sb.AppendLine(line);
|
||||
await RunAsync(fileName, arguments);
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user