654 lines
24 KiB
C#
654 lines
24 KiB
C#
// ----------------------------------------------------------------------------
|
|
//
|
|
// Copyright (c) Microsoft Corporation. All rights reserved.
|
|
//
|
|
// ----------------------------------------------------------------------------
|
|
|
|
using System;
|
|
using ManifestReader;
|
|
using System.Collections;
|
|
using System.Diagnostics;
|
|
using System.IO;
|
|
namespace AppReader
|
|
{
|
|
class ReaderMain
|
|
{
|
|
static void Usage()
|
|
{
|
|
Console.WriteLine("AppReader.exe <manifestfile | (-d | -s) manifestdirectory | -r appdirectory>");
|
|
return;
|
|
}
|
|
|
|
static double computeSpread(int[] directoryCounts)
|
|
{
|
|
double count = 0;
|
|
for (int i = 0; i < directoryCounts.Length; i++) {
|
|
int curLevelCount = directoryCounts[i];
|
|
count += Math.Pow(100, -i)*(curLevelCount - 1);
|
|
}
|
|
return count;
|
|
}
|
|
|
|
|
|
static int getAppNumber(string name, Hashtable appNameTable, ref int nameCount)
|
|
{
|
|
if (!appNameTable.ContainsKey(name))
|
|
{
|
|
appNameTable[name] = nameCount++;
|
|
}
|
|
return (int)appNameTable[name];
|
|
}
|
|
|
|
static void parseFile(string appName, string file)
|
|
{
|
|
ManifestParser mp;
|
|
// get the suffix
|
|
string[] components = file.Split(new char[] {'.'}, 50);
|
|
string suffix = components[components.Length - 1];
|
|
if (suffix.Equals("wxs"))
|
|
{
|
|
mp = new WiXParser();
|
|
}
|
|
else if (suffix.ToLower().Equals("man") ||
|
|
suffix.ToLower().Equals("manifest"))
|
|
{
|
|
mp = new CMIParser();
|
|
}
|
|
else if (suffix.Equals("inf")) {
|
|
mp = new INFParser();
|
|
}
|
|
else if (suffix.Equals("inft")) {
|
|
mp = new RegINFParser();
|
|
}
|
|
else {
|
|
//Console.WriteLine("Unknown manifest suffix " + suffix);
|
|
return;
|
|
}
|
|
|
|
// otherwise, try to parse it
|
|
try {
|
|
mp.Parse(appName, file, new Hashtable());
|
|
}
|
|
catch (System.Xml.XmlException) {
|
|
Console.WriteLine("Could not parse " + file);
|
|
}
|
|
// catch(System.Exception e)
|
|
// {
|
|
// Console.WriteLine("Failed to parse " + file + " : " + e.Message);
|
|
// }
|
|
return;
|
|
}
|
|
|
|
static Hashtable parseFile(string appName, string file, Hashtable wxsvals, Hashtable cmivals)
|
|
{
|
|
Hashtable whash = new Hashtable();;
|
|
ManifestParser mp;
|
|
Hashtable inHash;
|
|
// get the suffix
|
|
string[] components = file.Split(new char[] {'.'}, 50);
|
|
string suffix = components[components.Length - 1];
|
|
if (suffix.Equals("wxs"))
|
|
{
|
|
inHash = wxsvals;
|
|
mp = new WiXParser();
|
|
}
|
|
else if (suffix.ToLower().Equals("man") ||
|
|
suffix.ToLower().Equals("manifest"))
|
|
{
|
|
inHash = cmivals;
|
|
mp = new CMIParser();
|
|
}
|
|
else if (suffix.Equals("inf")) {
|
|
inHash = null;
|
|
mp = new INFParser();
|
|
}
|
|
else if (suffix.Equals("inft")) {
|
|
inHash = null;
|
|
mp = new RegINFParser();
|
|
}
|
|
else {
|
|
Console.WriteLine("Unknown manifest suffix " + suffix);
|
|
return whash;
|
|
}
|
|
|
|
// otherwise, try to parse it
|
|
try
|
|
{
|
|
whash = mp.Parse(appName, file, inHash);
|
|
}
|
|
catch (System.Xml.XmlException) {
|
|
Console.WriteLine("Could not parse " + file);
|
|
}
|
|
return whash;
|
|
}
|
|
|
|
static bool sameApp(string first, string second)
|
|
{
|
|
// we'll compute a heuristic measure of similarity by checking
|
|
// the number of elements they have in common in
|
|
// their strings. Any difference of 2 or less and
|
|
// we will say that they are the same
|
|
string[] firstArray = first.Split(new char[] {'.', '_'}, 40);
|
|
string[] secondArray = second.Split(new char[] {'.', '_'}, 40);
|
|
Hashtable firstHash = new Hashtable();
|
|
for (int i = 0; i < firstArray.Length; i++)
|
|
{
|
|
if (!firstHash.ContainsKey(firstArray[i]))
|
|
{
|
|
firstHash[firstArray[i]] = 0;
|
|
}
|
|
firstHash[firstArray[i]] = (int)firstHash[firstArray[i]] + 1;
|
|
}
|
|
|
|
for (int i = 0; i < secondArray.Length; i++)
|
|
{
|
|
if (firstHash.ContainsKey(secondArray[i]))
|
|
{
|
|
firstHash[secondArray[i]] = (int)firstHash[secondArray[i]] - 1;
|
|
}
|
|
}
|
|
|
|
ICollection vcol = firstHash.Values;
|
|
IEnumerator ienum = vcol.GetEnumerator();
|
|
int sum = 0;
|
|
while (ienum.MoveNext())
|
|
{
|
|
sum += (int)ienum.Current;
|
|
}
|
|
return System.Math.Abs(sum) <= 2;
|
|
}
|
|
|
|
[STAThread]
|
|
static void Main(string[] args)
|
|
{
|
|
if (args.Length < 1)
|
|
{
|
|
Usage();
|
|
return;
|
|
}
|
|
#region Setup Variables
|
|
// grab a new app tree
|
|
//AppTree tempat = AppTree.CreateAppTree(args[0]);
|
|
|
|
//ArrayList tempar = tempat.Flatten();
|
|
// ... that we can spit out for clustering
|
|
//for(int j = 0; j < tempar.Count; j++)
|
|
//{
|
|
// Console.Write(((AppNode)tempar[j]).Name + "\t");
|
|
//}
|
|
//Console.WriteLine();
|
|
|
|
// get the mapping from WiX elements to app tree paths
|
|
//Hashtable wxvals = WiXParser.GetManifestHashtable(args[1]);
|
|
|
|
// get the mapping from CMI elements to app tree paths
|
|
//Hashtable cmivals = CMIParser.GetManifestHashtable(args[2]);
|
|
|
|
#endregion
|
|
|
|
bool doConflictsAndDependencies = true;
|
|
if (args[0].Equals("-s")) {
|
|
doConflictsAndDependencies = false;
|
|
}
|
|
|
|
#region Parse Directory
|
|
if (args[0].Equals("-r")) {
|
|
// the next argument must be a directory that
|
|
// we can parse to get a list of directories that map to apps
|
|
if (args.Length != 2) {
|
|
Usage();
|
|
return;
|
|
}
|
|
string dir = args[1];
|
|
DirectoryInfo di = new DirectoryInfo(dir);
|
|
if (!di.Exists) {
|
|
Console.WriteLine("Directory " + dir + " does not exist");
|
|
return;
|
|
}
|
|
DirectoryInfo[] dis = di.GetDirectories();
|
|
for (int i = 0; i < dis.Length; i++) {
|
|
DirectoryInfo tempDi = dis[i];
|
|
|
|
// if this directory itself contains directories,
|
|
// step into them to get each app
|
|
DirectoryInfo[] subDirs = tempDi.GetDirectories();
|
|
if (subDirs.Length > 0)
|
|
{
|
|
for (int j = 0; j < subDirs.Length; j++)
|
|
{
|
|
DirectoryInfo subDir = subDirs[j];
|
|
string appName = tempDi.Name + " " + subDir.Name;
|
|
FileInfo[] fis = subDir.GetFiles();
|
|
for (int k = 0; k < fis.Length; k++)
|
|
{
|
|
parseFile(appName, subDir.FullName + "\\" + fis[k].Name);
|
|
}
|
|
|
|
}
|
|
}
|
|
else {
|
|
string appName = tempDi.Name;
|
|
FileInfo[] fis = tempDi.GetFiles();
|
|
for (int j = 0; j < fis.Length; j++)
|
|
{
|
|
parseFile(appName, tempDi.FullName + "\\" + fis[j].Name);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else if (args[0].Equals("-d") || args[0].Equals("-s")) {
|
|
// the next argument must be a directory that
|
|
// contains a set of .wxs files for us to parse
|
|
if (args.Length != 2)
|
|
{
|
|
Usage();
|
|
return;
|
|
}
|
|
Console.WriteLine("About to parse " + args[1]);
|
|
int count = 0;
|
|
string dir = args[1];
|
|
DirectoryInfo di = new DirectoryInfo(dir);
|
|
if (!di.Exists)
|
|
{
|
|
Console.WriteLine("Directory " + dir + " does not exist");
|
|
return;
|
|
}
|
|
FileInfo[] fis = di.GetFiles();
|
|
|
|
// Console.WriteLine("Got here with size " + fis.Length);
|
|
|
|
// for each manifest file
|
|
for (int i = 0; i < fis.Length; i++)
|
|
{
|
|
Console.WriteLine(++count);
|
|
// grab a new app tree
|
|
//AppTree at = AppTree.CreateAppTree(args[0]);
|
|
|
|
//Hashtable whash = parseFile(fis[i].DirectoryName + "\\" + fis[i].Name, wxvals, cmivals);
|
|
parseFile(fis[i].DirectoryName + "\\" + fis[i].Name, fis[i].DirectoryName + "\\" + fis[i].Name);
|
|
// then set up the app tree
|
|
//at.IncrementWithHashtable(whash);
|
|
|
|
// and flatten it to a point in R^n
|
|
//ArrayList ar = at.Flatten();
|
|
|
|
//Console.Write(fis[i].Name + "\t");
|
|
|
|
// ... that we can spit out for clustering
|
|
//for(int j = 0; j < ar.Count; j++)
|
|
//{
|
|
// Console.Write(((AppNode)ar[j]).Weight() + "\t");
|
|
//}
|
|
//Console.WriteLine();
|
|
}
|
|
}
|
|
#endregion
|
|
#region Parse Single File
|
|
else {
|
|
if (args.Length != 1)
|
|
{
|
|
Usage();
|
|
return;
|
|
}
|
|
// grab a new app tree
|
|
//AppTree at = AppTree.CreateAppTree(args[0]);
|
|
|
|
Console.WriteLine("about to parse file");
|
|
//Hashtable whash = parseFile(args[3], wxvals, cmivals);
|
|
parseFile(args[0], args[0]);
|
|
//at.IncrementWithHashtable(whash);
|
|
//at.Dump();
|
|
}
|
|
#endregion
|
|
|
|
|
|
#region Enumerate Registry Dependencies
|
|
Console.WriteLine("---Registry Conflicts---");
|
|
// this Hashtable will contain all the names found in the conflict table
|
|
// and the other apps with which they conflict on registry key/value pairs.
|
|
Hashtable appConflicts = new Hashtable();
|
|
|
|
// this table will be used to reduce the size of the generated file
|
|
Hashtable appNameTable = new Hashtable();
|
|
int nameCount = 0;
|
|
if (doConflictsAndDependencies) {
|
|
|
|
// now walk the Hashtable and dump any conflicts
|
|
Hashtable conflictTable = ManifestParser.registryConflictTable;
|
|
IDictionaryEnumerator iDictEnum = conflictTable.GetEnumerator();
|
|
while (iDictEnum.MoveNext()) {
|
|
if (((ArrayList)iDictEnum.Value).Count > 1) {
|
|
// then we might have a conflict
|
|
ArrayList confs = ((ArrayList)iDictEnum.Value);
|
|
string val = (string)((Pair)confs[1]).First;
|
|
string appname = (string)((Pair)confs[1]).Second;
|
|
bool founddifference = false;
|
|
bool foundtwoapps = false;
|
|
for (int i = 0; i < confs.Count; i++) {
|
|
Pair outp = (Pair)confs[i];
|
|
string newval = (string)((Pair)confs[i]).First;
|
|
string newappname = (string)((Pair)confs[i]).Second;
|
|
if ((val == null && newval == null) ||
|
|
(val != null && newval != null && val.ToLower().Equals(newval.ToLower())))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// heuristic (may give some false negatives, but then we're just underestimating a bit)
|
|
|
|
// if one of these starts with a '[' in the first two characters,
|
|
// then it's probably just a substitution that we didn't notice.
|
|
// In that case, claim that it is not a conflict
|
|
if ((val != null && val.Length > 1 && val[0] == '[') ||
|
|
(val != null && val.Length > 2 && val[1] == '[') ||
|
|
(newval != null && newval.Length > 1 && newval[0] == '[') ||
|
|
(newval != null && newval.Length > 2 && newval[1] == '['))
|
|
{
|
|
continue;
|
|
}
|
|
founddifference = true;
|
|
if (newappname.Equals(appname)) continue;
|
|
foundtwoapps = true;
|
|
}
|
|
|
|
if (!founddifference || !foundtwoapps) continue;
|
|
|
|
Console.WriteLine("C: " + ((string)iDictEnum.Key) + " : " + ((ArrayList)iDictEnum.Value).Count);
|
|
|
|
Hashtable confHash = new Hashtable();
|
|
for (int i = 0; i < confs.Count; i++) {
|
|
Pair outp = (Pair)confs[i];
|
|
string confAppName = (string)outp.Second;
|
|
if (!appConflicts.ContainsKey(confAppName)) {
|
|
appConflicts[confAppName] = new Hashtable();
|
|
}
|
|
// write the conflicting app into the hash with its value
|
|
confHash[confAppName] = (string)outp.First;
|
|
Console.WriteLine("\t " +
|
|
getAppNumber((string)outp.Second, appNameTable, ref nameCount)
|
|
+ " writes " + (string)outp.First);
|
|
}
|
|
|
|
IDictionaryEnumerator iconf1 = confHash.GetEnumerator();
|
|
while (iconf1.MoveNext()) {
|
|
string confAppName = (string)iconf1.Key;
|
|
string confVal1 = (string)iconf1.Value;
|
|
Hashtable htable = (Hashtable)appConflicts[confAppName];
|
|
IDictionaryEnumerator iconf2 = confHash.GetEnumerator();
|
|
while (iconf2.MoveNext()) {
|
|
string curAppName = (string)iconf2.Key;
|
|
string confVal2 = (string)iconf2.Value;
|
|
|
|
// don't add this conflict if they are the same app
|
|
if (curAppName.Equals(confAppName)) continue;
|
|
|
|
// don't add this conflict if they have the same value on this key
|
|
if (confVal2 == null && confVal1 == null) {
|
|
continue;
|
|
}
|
|
else if (confVal2 != null && confVal1 != null && confVal2.ToLower().Equals(confVal1.ToLower())) {
|
|
continue;
|
|
}
|
|
|
|
if (!htable.ContainsKey(curAppName)) {
|
|
htable[curAppName] = 0;
|
|
}
|
|
htable[curAppName] = (int)htable[curAppName] + 1;
|
|
}
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
IDictionaryEnumerator iDE = appConflicts.GetEnumerator();
|
|
while (iDE.MoveNext()) {
|
|
string firstName = (string)iDE.Key;
|
|
Hashtable confvals = (Hashtable)iDE.Value;
|
|
IDictionaryEnumerator iDEInner = confvals.GetEnumerator();
|
|
while (iDEInner.MoveNext()) {
|
|
string secondName = (string)iDEInner.Key;
|
|
int strength = (int)iDEInner.Value;
|
|
|
|
Console.WriteLine(getAppNumber(firstName, appNameTable, ref nameCount)
|
|
+ "\t" + getAppNumber(secondName, appNameTable, ref nameCount)
|
|
+ "\t" + strength);
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Enumerate File Dependencies
|
|
|
|
Console.WriteLine("---File Dependencies---");
|
|
// now let's display all the file dependencies.
|
|
// We'll walk through the ManifestParser.provideFileTable
|
|
// and ManifestParser.requireFileTable
|
|
IDictionaryEnumerator ireq = ManifestParser.requiresFileTable.GetEnumerator();
|
|
while (ireq.MoveNext()) {
|
|
string requiredFile = (string)ireq.Key;
|
|
ArrayList reqArray = (ArrayList)ireq.Value;
|
|
for (int i = 0; i < reqArray.Count; i++) {
|
|
string requiringAppName = (string)reqArray[i];
|
|
if (!ManifestParser.provideFileTable.ContainsKey(requiredFile)) {
|
|
// do nothing about it for now
|
|
//throw new Exception("File " + requiredFile + " is required but not provided!");
|
|
}
|
|
else {
|
|
// otherwise, we'll walk through all the apps that provide
|
|
// this file and print a dependency between them
|
|
ArrayList provArray = (ArrayList)ManifestParser.provideFileTable[requiredFile];
|
|
for (int j = 0; j < provArray.Count; j++) {
|
|
string providingAppName = (string)provArray[j];
|
|
Console.WriteLine(getAppNumber(requiringAppName, appNameTable, ref nameCount) + "\t" +
|
|
getAppNumber(providingAppName, appNameTable, ref nameCount));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
#endregion
|
|
}
|
|
|
|
|
|
#region Evaluate File Spread
|
|
|
|
Console.WriteLine("\n---File Spread---\n");
|
|
|
|
// we use the appFileTable for each app to compute this
|
|
IDictionaryEnumerator appFileEnum = ManifestParser.appFileTable.GetEnumerator();
|
|
while (appFileEnum.MoveNext()) {
|
|
string appName = (string)appFileEnum.Key;
|
|
Hashtable appFiles = (Hashtable)appFileEnum.Value;
|
|
// walk through the list and figure out how many unique prefixes there are of each length
|
|
IDictionaryEnumerator lengthEnum = appFiles.GetEnumerator();
|
|
int max = 0;
|
|
while (lengthEnum.MoveNext()) {
|
|
string path = (string)lengthEnum.Key;
|
|
string[] pathElts = path.Split(new char[] {'\\'}, 40);
|
|
// since the paths all start with \ and
|
|
// we don't want to count the empty first pathElt
|
|
int length = pathElts.Length - 1;
|
|
if (length > max) {
|
|
max = length;
|
|
}
|
|
}
|
|
|
|
int[] lengths = new int[max];
|
|
|
|
for (int i = 0; i < lengths.Length; i++) {
|
|
lengths[i] = 0;
|
|
}
|
|
|
|
lengthEnum.Reset();
|
|
|
|
while (lengthEnum.MoveNext()) {
|
|
string path = (string)lengthEnum.Key;
|
|
string[] pathElts = path.Split(new char[] {'\\'}, 40);
|
|
// since the paths all start with \ and
|
|
// we don't want to count the empty first pathElt
|
|
int length = pathElts.Length - 1;
|
|
// this -1 is because the lengths array is 0-based
|
|
lengths[length - 1]++;
|
|
}
|
|
|
|
for (int i = 0; i < lengths.Length; i++)
|
|
{
|
|
if (lengths[i] == 0)
|
|
{
|
|
lengths[i] = 1;
|
|
}
|
|
}
|
|
|
|
double spread = computeSpread(lengths);
|
|
// look up the real name
|
|
if (ManifestParser.nameTable.ContainsKey(appName)) {
|
|
appName = (string)ManifestParser.nameTable[appName];
|
|
}
|
|
|
|
string strSpread = "";
|
|
for (int i = 0; i < lengths.Length; i++) {
|
|
if (i != 0) {
|
|
strSpread += " ";
|
|
}
|
|
strSpread += lengths[i];
|
|
}
|
|
Console.WriteLine(appName + " : " + spread + " : " + strSpread);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Evaluate Registry Spread
|
|
|
|
Console.WriteLine("\n---Registry Spread---\n");
|
|
|
|
// we use the appFileTable for each app to compute this
|
|
IDictionaryEnumerator appRegistryEnum = ManifestParser.appRegistryTable.GetEnumerator();
|
|
while (appRegistryEnum.MoveNext()) {
|
|
string appName = (string)appRegistryEnum.Key;
|
|
Hashtable appRegs = (Hashtable)appRegistryEnum.Value;
|
|
// walk through the list and figure out how many unique prefixes there are of each length
|
|
IDictionaryEnumerator lengthEnum = appRegs.GetEnumerator();
|
|
int max = 0;
|
|
while (lengthEnum.MoveNext()) {
|
|
string path = (string)lengthEnum.Key;
|
|
string[] pathElts = path.Split(new char[] {'\\'}, 40);
|
|
// since the paths all start with \ and
|
|
// we don't want to count the empty first pathElt
|
|
int length = pathElts.Length - 1;
|
|
if (length > max) {
|
|
max = length;
|
|
}
|
|
}
|
|
|
|
int[] lengths = new int[max];
|
|
|
|
for (int i = 0; i < lengths.Length; i++) {
|
|
lengths[i] = 0;
|
|
}
|
|
|
|
lengthEnum.Reset();
|
|
|
|
while (lengthEnum.MoveNext()) {
|
|
string path = (string)lengthEnum.Key;
|
|
string[] pathElts = path.Split(new char[] {'\\'}, 40);
|
|
// since the paths all start with \ and
|
|
// we don't want to count the empty first pathElt
|
|
int length = pathElts.Length - 1;
|
|
// this -1 is because the lengths array is 0-based
|
|
lengths[length - 1]++;
|
|
}
|
|
for (int i = 0; i < lengths.Length; i++)
|
|
{
|
|
if (lengths[i] == 0)
|
|
{
|
|
lengths[i] = 1;
|
|
}
|
|
}
|
|
|
|
double spread = computeSpread(lengths);
|
|
// look up the real name
|
|
if (ManifestParser.nameTable.ContainsKey(appName)) {
|
|
appName = (string)ManifestParser.nameTable[appName];
|
|
}
|
|
|
|
string strSpread = "";
|
|
for (int i = 0; i < lengths.Length; i++) {
|
|
if (i != 0) {
|
|
strSpread += " ";
|
|
}
|
|
strSpread += lengths[i];
|
|
}
|
|
Console.WriteLine(appName + " : " + spread + " : " + strSpread);
|
|
|
|
}
|
|
|
|
#endregion
|
|
|
|
|
|
#region Print Custom Actions
|
|
|
|
Console.WriteLine("---Custom Actions---");
|
|
|
|
// walk through the ManifestParser.customActionsTable
|
|
IDictionaryEnumerator customEnum = ManifestParser.customActionsTable.GetEnumerator();
|
|
while (customEnum.MoveNext())
|
|
{
|
|
string appName = (string)customEnum.Key;
|
|
int customCount = (int)customEnum.Value;
|
|
Console.WriteLine(appName + " : " + customCount);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Print File Extensions and Counts
|
|
|
|
Console.WriteLine("---File Extensions---");
|
|
|
|
// walk through the ManifestParser.fileTypeTable
|
|
IDictionaryEnumerator fileEnum = ManifestParser.fileTypeTable.GetEnumerator();
|
|
while (fileEnum.MoveNext())
|
|
{
|
|
int uniqueFileTypes = 0;
|
|
string appName = (string)fileEnum.Key;
|
|
Hashtable exts = (Hashtable)fileEnum.Value;
|
|
IDictionaryEnumerator extEnum = exts.GetEnumerator();
|
|
while (extEnum.MoveNext())
|
|
{
|
|
uniqueFileTypes++;
|
|
string extension = (string)extEnum.Key;
|
|
int extCount = (int)extEnum.Value;
|
|
Console.WriteLine(appName + " : " + extension + " : " + extCount);
|
|
}
|
|
Console.WriteLine(appName + " : " + uniqueFileTypes);
|
|
}
|
|
|
|
#endregion
|
|
|
|
|
|
#region Print Name Table
|
|
Console.WriteLine("\n---Name Table---\n");
|
|
|
|
// now write out the name table
|
|
IDictionaryEnumerator iDENT = appNameTable.GetEnumerator();
|
|
while (iDENT.MoveNext())
|
|
{
|
|
string appName = "";
|
|
// look up the real name
|
|
if (ManifestParser.nameTable.ContainsKey((string)iDENT.Key)) {
|
|
appName = (string)ManifestParser.nameTable[(string)iDENT.Key];
|
|
}
|
|
|
|
if (appName.Equals("")) {
|
|
Console.WriteLine((string)iDENT.Key + " " + (int)iDENT.Value);
|
|
}
|
|
else {
|
|
Console.WriteLine((string)iDENT.Key + " " + appName + " " + (int)iDENT.Value);
|
|
}
|
|
}
|
|
#endregion
|
|
return;
|
|
}
|
|
|
|
|
|
}
|
|
}
|