本文來告訴大家一個黑科技,通過 .suo 文件讀取 VisualStudio 的啟動項目。在 sln 項目里面,都會生成對應的 suo 文件,這個文件是 OLE 格式的文件,文件的格式沒有公開,本文的方法適合用在 VisualStudio 2019 上,對於其他版本的 VisualStudio 也許會不適合
感謝 Simon Cropp 大佬提供的方法
默認在 sln 解決方案文件的相同文件夾里面,將會存放 .vs\{解決方案名}\v{VS版本}\.suo
文件,如解決方案文件名為 HairhechallchujurKairbilairlem.sln
在 VisualStudio 2019 下將會存放 .vs\HairhechallchujurKairbilairlem\v16\.suo
文件
這個 .suo
文件是包含了 VisualStudio 解決方案的一些配置,如啟動項目。關多關於此文件,請參閱 Solution User Options (.Suo) File 文檔
預計這個 suo 格式文件基本不會更改,在 1995 年的時候就開始使用這個格式
讀取 .suo 需要使用到 Open MCDF 庫。這是一個完全由 C# 實現的讀取 OLE 格式文檔的庫,我在做 OFFICE 組件也用到這個庫
在 suo 文件里面,通過 SolutionConfiguration 內容存放當前的啟動項,這里面的內容是使用 UTF-16 編碼的字符串,讀取的方法如下
using (var fileStream = new FileStream(suoFilePath, FileMode.Open))
{
using CompoundFile compoundFile = new CompoundFile(fileStream, CFSUpdateMode.ReadOnly, CFSConfiguration.SectorRecycle | CFSConfiguration.EraseFreeSectors);
var cfStream = compoundFile.RootStorage.GetStream("SolutionConfiguration");
var byteList = cfStream.GetData();
var encoding = Encoding.GetEncodings()
.Single(x => string.Equals(x.Name, "utf-16", StringComparison.OrdinalIgnoreCase));
var text = encoding.GetEncoding().GetString(byteList);
}
這里的 text 的內容大概如下
"\u0011\0MultiStartupProj\0=\u0003\0\0;4\0{45171CDC-EDAC-4D0B-BDF8-63DE2D4F947B}.dwStartupOpt\0=\u0003\0\0;\u000f\0StartupProject\0=\b&\0{45171CDC-EDAC-4D0B-BDF8-63DE2D4F947B};A\0{45171CDC-EDAC-4D0B-BDF8-63DE2D4F947B}.Release|Any CPU.fBatchBld\0=\u0003\0\0;?\0{45171CDC-EDAC-4D0B-BDF8-63DE2D4F947B}.Debug|Any CPU.fBatchBld\0=\u0003\0\0;4\0{AE3577E5-5D4E-44F8-B181-88A31B92584A}.dwStartupOpt\0=\u0003\0\0;A\0{AE3577E5-5D4E-44F8-B181-88A31B92584A}.Release|Any CPU.fBatchBld\0=\u0003\0\0;?\0{AE3577E5-5D4E-44F8-B181-88A31B92584A}.Debug|Any CPU.fBatchBld\0=\u0003\0\0;4\0{A2FE74E1-B743-11D0-AE1A-00A0C90FFFC3}.dwStartupOpt\0=\u0003\0\0;\n\0ActiveCfg\0=\b\r\0Debug|Any CPU;"
通過讀取 StartupProject 后續的內容即可找到當前的啟動項目的 GUID 值,以下是我寫的正則
var text = encoding.GetEncoding().GetString(byteList);
const char nul = '\u0000';
const char dc1 = '\u0011';
const char etx = '\u0003';
const char soh = '\u0001';
var startupProjectRegex = new Regex(@$"StartupProject{nul}={'\b'}&{nul}(.{'{'}{38}{'}'});A");
var startupProjectMatch = startupProjectRegex.Match(text);
if (startupProjectMatch.Success)
{
var guid = Guid.Parse(startupProjectMatch.Groups[1].Value);
}
上面代碼拿到的 guid 就是啟動項目的 guid 內容
咱可以采用 Simon Cropp 大佬的開源項目 https://github.com/SimonCropp/SetStartupProjects 來輔助讀取當前 sln 里面包含的 csproj 的 GUID 和路徑
代碼如下
var projectList = SetStartupProjects.SolutionProjectExtractor.GetAllProjectFiles(solutionFile.FullName).ToList();
通過 guid 獲取當前的 csproj 項目文件路徑方法如下
var guid = Guid.Parse(startupProjectMatch.Groups[1].Value);
var project = projectList.FirstOrDefault(temp => new Guid(temp.Guid) == guid);
我封裝了方法,傳入的是 sln 文件,返回啟動項目的路徑
private static FileInfo GetStartupProject(FileInfo solutionFile)
{
var solutionFilePath = solutionFile.FullName;
var solutionDirectory = solutionFile.DirectoryName;
var solutionName = Path.GetFileNameWithoutExtension(solutionFilePath);
var suoDirectoryPath = Path.Combine(solutionDirectory, ".vs", solutionName, "v16");
Directory.CreateDirectory(suoDirectoryPath);
var suoFilePath = Path.Combine(suoDirectoryPath, ".suo");
var projectList = SetStartupProjects.SolutionProjectExtractor.GetAllProjectFiles(solutionFile.FullName).ToList();
using (var fileStream = new FileStream(suoFilePath, FileMode.Open))
{
using CompoundFile compoundFile = new CompoundFile(fileStream, CFSUpdateMode.ReadOnly, CFSConfiguration.SectorRecycle | CFSConfiguration.EraseFreeSectors);
var cfStream = compoundFile.RootStorage.GetStream("SolutionConfiguration");
var byteList = cfStream.GetData();
var encoding = Encoding.GetEncodings()
.Single(x => string.Equals(x.Name, "utf-16", StringComparison.OrdinalIgnoreCase));
var text = encoding.GetEncoding().GetString(byteList);
const char nul = '\u0000';
const char dc1 = '\u0011';
const char etx = '\u0003';
const char soh = '\u0001';
var startupProjectRegex = new Regex(@$"StartupProject{nul}={'\b'}&{nul}(.{'{'}{38}{'}'});A");
var startupProjectMatch = startupProjectRegex.Match(text);
if (startupProjectMatch.Success)
{
var guid = Guid.Parse(startupProjectMatch.Groups[1].Value);
var project = projectList.FirstOrDefault(temp => new Guid(temp.Guid) == guid);
return new FileInfo(project.FullPath);
}
}
return null;
}
需要先在項目安裝 SetStartupProjects 庫,才能使用這個方法
本文所有代碼放在 github 和 gitee 歡迎小伙伴訪問
除了讀取啟動項目,還可以讀取斷點等內容,讀取 suo 里面的所有內容的方法如下
compoundFile.RootStorage.VisitEntries(item =>
{
if (item.IsStream)
{
Console.WriteLine(item.Name);
var stream = item as CFStream;
byteList = stream.GetData();
text = encoding.GetEncoding().GetString(byteList);
}
}, true);
當然了,獲取到的內容不一定使用 UTF-16 編碼格式,還需要自己嘗試,里面的數據只是二進制而是,上面代碼的轉換字符串只是用來調試
更多請看
SimonCropp/SetStartupProjects: Setting Visual Studio startup projects by hacking the suo
Solution User Options (.Suo) File
更多編譯相關請看手把手教你寫 Roslyn 修改編譯