diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 526fcaa74..4e36b136b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,12 +1,19 @@ name: ci +# Build validation. GitHub Actions minutes are limited, so this runs automatically +# only for PRs into `main` (the develop -> main gate). For PRs into `develop`, +# day-to-day validation is done with scripts/local-ci.sh; a run here happens only +# when an admin adds the `run-ci` label (re-add it to trigger each run) or +# dispatches the workflow manually. See the `if:` on the build job below. +# +# This workflow does NOT publish releases — that is handled by release.yml +# (manual dispatch + a monthly check for new commits on main). on: - push: - branches: [main, develop] - paths-ignore: ['**.md', 'docs/**', 'LICENSE*'] pull_request: branches: [main, develop] + types: [opened, synchronize, reopened, labeled] paths-ignore: ['**.md', 'docs/**', 'LICENSE*'] + workflow_dispatch: concurrency: group: ci-${{ github.ref }} @@ -14,6 +21,12 @@ concurrency: jobs: build: + # Auto for PRs into main and manual dispatch; for develop PRs only when an + # admin adds the `run-ci` label. + if: >- + github.event_name == 'workflow_dispatch' || + github.base_ref == 'main' || + (github.event.action == 'labeled' && github.event.label.name == 'run-ci') runs-on: windows-2025 timeout-minutes: 10 env: @@ -88,101 +101,3 @@ jobs: ConfuserEx-GUI.zip ConfuserEx.zip Confuser.MSBuild.Tasks/bin/Release/*.nupkg - - # Dev build: on push to develop branch - dev-release: - needs: build - if: github.event_name == 'push' && github.ref == 'refs/heads/develop' - runs-on: windows-2025 - timeout-minutes: 5 - permissions: - contents: write - steps: - - uses: actions/checkout@v5 - with: - fetch-depth: 0 - - - name: Install nbgv - run: dotnet tool install -g nbgv - - - name: Compute version - id: version - shell: pwsh - run: | - $ver = nbgv get-version -v NuGetPackageVersion - echo "VERSION=$ver" >> $env:GITHUB_OUTPUT - - - name: Download artifacts - uses: actions/download-artifact@v5 - with: - name: confuserex-packages - - - name: Create or update dev release - uses: softprops/action-gh-release@v2 - with: - tag_name: dev-latest - name: "Dev build v${{ steps.version.outputs.VERSION }}" - prerelease: true - make_latest: false - body: | - **Development build** — for testing only, not production use. - - Version: `${{ steps.version.outputs.VERSION }}` - Branch: `develop` - Commit: ${{ github.sha }} - - Download the binaries below to test recent fixes and features before they are included in a stable release. - files: | - ConfuserEx-CLI.zip - ConfuserEx-GUI.zip - ConfuserEx.zip - *.nupkg - - # Release: only on PR merge to main - release: - needs: build - if: github.event_name == 'push' && github.ref == 'refs/heads/main' - runs-on: windows-2025 - timeout-minutes: 5 - permissions: - contents: write - steps: - - uses: actions/checkout@v5 - with: - fetch-depth: 0 - - - name: Install nbgv - run: dotnet tool install -g nbgv - - - name: Compute version - id: version - shell: pwsh - run: | - $ver = nbgv get-version -v NuGetPackageVersion - echo "VERSION=$ver" >> $env:GITHUB_OUTPUT - - - name: Configure git identity - run: | - git config user.name "github-actions[bot]" - git config user.email "github-actions[bot]@users.noreply.github.com" - - - name: Create release tag - run: | - git tag -a "v${{ steps.version.outputs.VERSION }}" -m "Release ${{ steps.version.outputs.VERSION }}" - git push origin "v${{ steps.version.outputs.VERSION }}" - - - name: Download artifacts - uses: actions/download-artifact@v5 - with: - name: confuserex-packages - - - name: Create GitHub Release - uses: softprops/action-gh-release@v2 - with: - tag_name: v${{ steps.version.outputs.VERSION }} - name: v${{ steps.version.outputs.VERSION }} - files: | - ConfuserEx-CLI.zip - ConfuserEx-GUI.zip - ConfuserEx.zip - *.nupkg diff --git a/.github/workflows/format.yml b/.github/workflows/format.yml deleted file mode 100644 index 112a45e48..000000000 --- a/.github/workflows/format.yml +++ /dev/null @@ -1,35 +0,0 @@ -name: format - -on: - pull_request: - branches: [master, pre-release] - paths: ['**.cs', '**.vb', '.editorconfig'] - -jobs: - check-format: - runs-on: windows-2025 - timeout-minutes: 5 - permissions: - contents: read - steps: - - uses: actions/checkout@v5 - - - name: Check formatting - run: dotnet format Confuser2.sln --verify-no-changes --verbosity diagnostic - continue-on-error: true - id: format-check - - - name: Format diff - if: steps.format-check.outcome == 'failure' - shell: pwsh - run: | - dotnet format Confuser2.sln - git diff --stat - $diff = git diff --name-only - if ($diff) { - Write-Host "::warning::The following files need formatting:" -ForegroundColor Yellow - $diff | ForEach-Object { Write-Host " $_" } - Write-Host "" - Write-Host "Run 'dotnet format Confuser2.sln' locally and commit the changes." - exit 1 - } diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 60a2b6cc4..8204d7727 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -1,11 +1,17 @@ name: lint +# GitHub Actions minutes are limited. Linting is done locally with +# scripts/local-ci.sh (dotnet format whitespace/style/analyzers). This workflow +# runs automatically only for PRs into `main`; for PRs into `develop` it runs only +# when an admin adds the `run-ci` label (re-add to trigger each run) or dispatches +# it manually. The previous any-branch `push` trigger was removed to stop a lint +# run firing on every push. See the `if:` on the lint job below. on: - push: - paths-ignore: ['**.md', 'docs/**', 'LICENSE*'] pull_request: branches: [main, develop] + types: [opened, synchronize, reopened, labeled] paths-ignore: ['**.md', 'docs/**', 'LICENSE*'] + workflow_dispatch: concurrency: group: lint-${{ github.ref }} @@ -13,6 +19,12 @@ concurrency: jobs: lint: + # Auto for PRs into main and manual dispatch; for develop PRs only when an + # admin adds the `run-ci` label. + if: >- + github.event_name == 'workflow_dispatch' || + github.base_ref == 'main' || + (github.event.action == 'labeled' && github.event.label.name == 'run-ci') runs-on: windows-2025 timeout-minutes: 5 permissions: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 000000000..abdef97f8 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,161 @@ +name: release + +# GitHub Actions minutes are limited, so releases are NOT cut automatically on +# every push to main. Instead: +# - Run this workflow manually (Actions tab -> release -> Run workflow) to build +# and publish a release from main on demand. +# - On the 1st of each month it checks main for commits since the last release +# tag and only spends the (expensive) Windows build when there are new changes. +# +# The check job runs on a cheap Ubuntu runner, so a monthly run with nothing new +# costs only a few seconds and publishes nothing. +on: + workflow_dispatch: + inputs: + force: + description: "Release even if there are no new commits since the last release tag" + type: boolean + default: false + schedule: + - cron: "0 6 1 * *" # 06:00 UTC on the 1st of every month + +concurrency: + group: release + cancel-in-progress: false + +permissions: + contents: write + +jobs: + check: + name: Check main for new commits + runs-on: ubuntu-latest + outputs: + should_release: ${{ steps.decide.outputs.should_release }} + steps: + - uses: actions/checkout@v5 + with: + ref: main + fetch-depth: 0 + fetch-tags: true + + - name: Decide whether to release + id: decide + shell: bash + run: | + last_tag=$(git tag --list 'v*' --sort=-v:refname | head -n1) + if [ -z "$last_tag" ]; then + echo "No release tag found — treating as first release." + echo "should_release=true" >> "$GITHUB_OUTPUT" + exit 0 + fi + + behind=$(git rev-list "$last_tag"..HEAD --count) + echo "Last release: $last_tag — $behind new commit(s) on main since then." + + if [ "${{ github.event_name }}" = "workflow_dispatch" ] && [ "${{ inputs.force }}" = "true" ]; then + echo "Manual dispatch with force=true — releasing." + echo "should_release=true" >> "$GITHUB_OUTPUT" + elif [ "$behind" -gt 0 ]; then + echo "New commits present — releasing." + echo "should_release=true" >> "$GITHUB_OUTPUT" + else + echo "No new commits since $last_tag — skipping. (Use force=true to release anyway.)" + echo "should_release=false" >> "$GITHUB_OUTPUT" + fi + + release: + name: Build and publish release + needs: check + if: needs.check.outputs.should_release == 'true' + runs-on: windows-2025 + timeout-minutes: 15 + permissions: + contents: write + env: + NUGET_PACKAGES: ${{ github.workspace }}/.nuget/packages + steps: + - uses: actions/checkout@v5 + with: + ref: main + fetch-depth: 0 + + - name: Setup MSBuild + uses: microsoft/setup-msbuild@v2 + + - name: Cache NuGet packages + uses: actions/cache@v4 + with: + path: ${{ github.workspace }}/.nuget/packages + key: ${{ runner.os }}-nuget-${{ hashFiles('**/*.csproj', '**/*.vcxproj') }} + restore-keys: | + ${{ runner.os }}-nuget- + + - name: Install nbgv + run: dotnet tool install -g nbgv + + - name: Compute version + id: version + shell: pwsh + run: | + $ver = nbgv get-version -v NuGetPackageVersion + echo "VERSION=$ver" >> $env:GITHUB_OUTPUT + Write-Host "Version: $ver" + + - name: Restore + run: msbuild Confuser2.sln -t:Restore -verbosity:minimal + + - name: Build + run: msbuild Confuser2.sln -p:Configuration=Release -verbosity:minimal + + - name: Package CLI + shell: pwsh + run: | + $src = 'Confuser.CLI/bin/Release/net10.0' + Get-ChildItem $src -Exclude '*.pdb','*.xml' | Compress-Archive -DestinationPath 'ConfuserEx-CLI.zip' + Write-Host "Created ConfuserEx-CLI.zip" + + - name: Package GUI + shell: pwsh + run: | + $src = 'ConfuserEx/bin/Release/net10.0-windows' + Get-ChildItem $src -Exclude '*.pdb','*.xml' | Compress-Archive -DestinationPath 'ConfuserEx-GUI.zip' + Write-Host "Created ConfuserEx-GUI.zip" + + - name: Package combined + shell: pwsh + run: | + $tmp = 'combined' + New-Item -ItemType Directory -Path $tmp -Force | Out-Null + Copy-Item 'Confuser.CLI/bin/Release/net10.0/*' $tmp -Exclude '*.pdb','*.xml' -Recurse + Copy-Item 'ConfuserEx/bin/Release/net10.0-windows/*' $tmp -Exclude '*.pdb','*.xml' -Recurse -Force + Get-ChildItem $tmp | Compress-Archive -DestinationPath 'ConfuserEx.zip' + Remove-Item $tmp -Recurse -Force + Write-Host "Created ConfuserEx.zip" + + - name: Configure git identity + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + + - name: Create release tag + shell: bash + run: | + tag="v${{ steps.version.outputs.VERSION }}" + if git rev-parse "$tag" >/dev/null 2>&1; then + echo "Tag $tag already exists — skipping tag creation." + else + git tag -a "$tag" -m "Release ${{ steps.version.outputs.VERSION }}" + git push origin "$tag" + fi + + - name: Create GitHub Release + uses: softprops/action-gh-release@v2 + with: + tag_name: v${{ steps.version.outputs.VERSION }} + name: v${{ steps.version.outputs.VERSION }} + files: | + ConfuserEx-CLI.zip + ConfuserEx-GUI.zip + ConfuserEx.zip + Confuser.MSBuild.Tasks/bin/Release/*.nupkg diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 728b57c48..62cd3c286 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,9 +1,16 @@ name: test +# GitHub Actions minutes are limited. This full build+test+coverage run happens +# automatically only for PRs into `main` (the release gate). For PRs into +# `develop`, run tests locally with scripts/local-ci.sh; a run here happens only +# when an admin adds the `run-ci` label (re-add to trigger each run) or dispatches +# the workflow manually. See the `if:` on the test job below. on: pull_request: branches: [main, develop] + types: [opened, synchronize, reopened, labeled] paths-ignore: ['**.md', 'docs/**', 'LICENSE*'] + workflow_dispatch: concurrency: group: test-${{ github.ref }} @@ -11,6 +18,12 @@ concurrency: jobs: test: + # Auto for PRs into main and manual dispatch; for develop PRs only when an + # admin adds the `run-ci` label. + if: >- + github.event_name == 'workflow_dispatch' || + github.base_ref == 'main' || + (github.event.action == 'labeled' && github.event.label.name == 'run-ci') runs-on: windows-2025 timeout-minutes: 15 permissions: @@ -61,8 +74,12 @@ jobs: $name = $proj.BaseName Write-Host "`nTesting $name..." -ForegroundColor Cyan + # Do NOT pass --collect:"XPlat Code Coverage" here. Coverage is driven by each + # project's RunSettingsFilePath (Tests/Directory.Build.targets), which enables it + # only for .NET (Core) test projects. A command-line --collect overrides that gate + # and forces Coverlet to instrument the signed Confuser.* assemblies on net4x, + # breaking their strong name so .NET Framework refuses to load them. dotnet test $proj.FullName -c Release --no-build --verbosity minimal ` - --collect:"XPlat Code Coverage" ` --logger "trx;LogFileName=$name.trx" ` --results-directory "$resultsDir/$name" diff --git a/.gitignore b/.gitignore index 9ee6a61e2..beadc33d8 100644 --- a/.gitignore +++ b/.gitignore @@ -39,4 +39,11 @@ packages/ gh-pages/ .idea/ -**/*out* \ No newline at end of file +**/*out* + +# Local CI artifacts +test-results/ +coverage/ + +# Release packages (produced by the CI/local-ci package step) +*.zip \ No newline at end of file diff --git a/Confuser.CLI/Confuser.CLI.csproj b/Confuser.CLI/Confuser.CLI.csproj index d087b4871..df0632989 100644 --- a/Confuser.CLI/Confuser.CLI.csproj +++ b/Confuser.CLI/Confuser.CLI.csproj @@ -16,6 +16,9 @@ + + + diff --git a/Confuser.CLI/Program.cs b/Confuser.CLI/Program.cs index aff59c47c..9d3323c02 100644 --- a/Confuser.CLI/Program.cs +++ b/Confuser.CLI/Program.cs @@ -5,8 +5,12 @@ using System.Linq; using System.Xml; using Confuser.Core; +using Confuser.Core.Diagnostics; using Confuser.Core.Project; +using Microsoft.Extensions.Logging; using NDesk.Options; +using Serilog; +using Serilog.Events; namespace Confuser.CLI { internal class Program { @@ -21,6 +25,10 @@ static int Main(string[] args) { try { bool noPause = false; bool debug = false; + bool quiet = false; + bool dumpRequested = false; + string dumpPath = null; + int verbosity = 0; string outDir = null; string snKeyPath = null; string snKeyPass = null; @@ -48,6 +56,15 @@ static int Main(string[] args) { }, { "snkeypass=", "specifies strong name key password.", value => { snKeyPass = value; } + }, { + "v|verbose", "increase verbosity (repeat for more: -v, -vv, -vvv).", + value => { verbosity++; } + }, { + "q|quiet", "only show warnings and errors.", + value => { quiet = (value != null); } + }, { + "dump:", "write a diagnostic report (optionally to the given file).", + value => { dumpRequested = true; if (!string.IsNullOrEmpty(value)) dumpPath = value; } } }; @@ -130,7 +147,7 @@ static int Main(string[] args) { parameters.Project = proj; } - int retVal = RunProject(parameters); + int retVal = RunProject(parameters, quiet, verbosity, dumpRequested, dumpPath); if (NeedPause() && !noPause) { Console.WriteLine("Press any key to continue..."); @@ -192,14 +209,61 @@ static void LoadTemplateProject(string templatePath, ConfuserProject proj, List< templateModules.Add(templateModule); } - static int RunProject(ConfuserParameters parameters) { - var logger = new ConsoleLogger(); - parameters.Logger = logger; + static int RunProject(ConfuserParameters parameters, bool quiet, int verbosity, bool dumpRequested, string dumpPath) { + var levelSwitch = quiet + ? LogEventLevel.Warning + : verbosity >= 3 ? LogEventLevel.Verbose + : verbosity >= 2 ? LogEventLevel.Verbose + : verbosity >= 1 ? LogEventLevel.Debug + : LogEventLevel.Information; + + Log.Logger = new LoggerConfiguration() + .MinimumLevel.Is(levelSwitch) + .WriteTo.Console( + outputTemplate: "[{Level:u4}] {Message:lj}{NewLine}{Exception}") + .CreateLogger(); + + using var loggerFactory = LoggerFactory.Create(builder => + builder.AddSerilog(dispose: false)); + var melLogger = loggerFactory.CreateLogger("ConfuserEx"); + + var progressReporter = new ConsoleProgressReporter(); + + // When a diagnostic report is requested, wrap both the logger and the progress reporter + // with a collector so the report captures the full transcript, timing and outcome even + // when the run fails. + DiagnosticCollector collector = null; + if (dumpRequested) { + collector = new DiagnosticCollector(melLogger, progressReporter) { Project = parameters.Project }; + parameters.Logger = collector; + parameters.ProgressReporter = collector; + } + else { + parameters.Logger = melLogger; + parameters.ProgressReporter = progressReporter; + } - Console.Title = "ConfuserEx - Running..."; + if (OperatingSystem.IsWindows()) + Console.Title = "ConfuserEx - Running..."; ConfuserEngine.Run(parameters).GetAwaiter().GetResult(); - return logger.ReturnValue; + Log.CloseAndFlush(); + + if (collector != null) + WriteDiagnosticReport(collector, dumpPath); + + return progressReporter.ReturnValue; + } + + static void WriteDiagnosticReport(DiagnosticCollector collector, string dumpPath) { + string path = string.IsNullOrEmpty(dumpPath) ? "confuser-diagnostic-report.md" : dumpPath; + try { + File.WriteAllText(path, collector.GenerateReport()); + WriteLineWithColor(ConsoleColor.Cyan, "Diagnostic report written to: " + Path.GetFullPath(path)); + } + catch (Exception ex) { + WriteLineWithColor(ConsoleColor.Red, "Failed to write diagnostic report: " + ex.Message); + } } static bool NeedPause() { @@ -217,6 +281,9 @@ static void PrintUsage() { WriteLine(" -debug : specifies debug symbol generation."); WriteLine(" -snkey : specifies strong name key file path."); WriteLine(" -snkeypass : specifies strong name key password."); + WriteLine(" -v|verbose : increase verbosity (-v debug, -vv trace)."); + WriteLine(" -q|quiet : only show warnings and errors."); + WriteLine(" -dump : write a diagnostic report (-dump= for a custom path)."); } static void WriteLineWithColor(ConsoleColor color, string txt) { @@ -234,57 +301,15 @@ static void WriteLine() { Console.WriteLine(); } - class ConsoleLogger : ILogger { + class ConsoleProgressReporter : IProgressReporter { readonly DateTime begin; - public ConsoleLogger() { + public ConsoleProgressReporter() { begin = DateTime.Now; } public int ReturnValue { get; private set; } - public void Debug(string msg) { - WriteLineWithColor(ConsoleColor.Gray, "[DEBUG] " + msg); - } - - public void DebugFormat(string format, params object[] args) { - WriteLineWithColor(ConsoleColor.Gray, "[DEBUG] " + string.Format(format, args)); - } - - public void Info(string msg) { - WriteLineWithColor(ConsoleColor.White, " [INFO] " + msg); - } - - public void InfoFormat(string format, params object[] args) { - WriteLineWithColor(ConsoleColor.White, " [INFO] " + string.Format(format, args)); - } - - public void Warn(string msg) { - WriteLineWithColor(ConsoleColor.Yellow, " [WARN] " + msg); - } - - public void WarnFormat(string format, params object[] args) { - WriteLineWithColor(ConsoleColor.Yellow, " [WARN] " + string.Format(format, args)); - } - - public void WarnException(string msg, Exception ex) { - WriteLineWithColor(ConsoleColor.Yellow, " [WARN] " + msg); - WriteLineWithColor(ConsoleColor.Yellow, "Exception: " + ex); - } - - public void Error(string msg) { - WriteLineWithColor(ConsoleColor.Red, "[ERROR] " + msg); - } - - public void ErrorFormat(string format, params object[] args) { - WriteLineWithColor(ConsoleColor.Red, "[ERROR] " + string.Format(format, args)); - } - - public void ErrorException(string msg, Exception ex) { - WriteLineWithColor(ConsoleColor.Red, "[ERROR] " + msg); - WriteLineWithColor(ConsoleColor.Red, "Exception: " + ex); - } - public void Progress(int progress, int overall) { } public void EndProgress() { } diff --git a/Confuser.Core/Confuser.Core.csproj b/Confuser.Core/Confuser.Core.csproj index 0e13ac77c..2969a4d41 100644 --- a/Confuser.Core/Confuser.Core.csproj +++ b/Confuser.Core/Confuser.Core.csproj @@ -15,8 +15,9 @@ - + + diff --git a/Confuser.Core/ConfuserContext.cs b/Confuser.Core/ConfuserContext.cs index a8c7a1c76..493643745 100644 --- a/Confuser.Core/ConfuserContext.cs +++ b/Confuser.Core/ConfuserContext.cs @@ -4,6 +4,7 @@ using Confuser.Core.Project; using dnlib.DotNet; using dnlib.DotNet.Writer; +using Microsoft.Extensions.Logging; namespace Confuser.Core { /// @@ -18,7 +19,13 @@ public class ConfuserContext { /// Gets the logger used for logging events. /// /// The logger. - public ILogger Logger { get; internal set; } + public Microsoft.Extensions.Logging.ILogger Logger { get; internal set; } + + /// + /// Gets the progress reporter used for reporting protection progress. + /// + /// The progress reporter. + public IProgressReporter ProgressReporter { get; internal set; } /// /// Gets the project being processed. diff --git a/Confuser.Core/ConfuserEngine.cs b/Confuser.Core/ConfuserEngine.cs index a4e737cce..68ac9c482 100644 --- a/Confuser.Core/ConfuserEngine.cs +++ b/Confuser.Core/ConfuserEngine.cs @@ -10,6 +10,7 @@ using dnlib.DotNet; using dnlib.DotNet.Emit; using dnlib.DotNet.Writer; +using Microsoft.Extensions.Logging; using Microsoft.Win32; using CopyrightAttribute = System.Reflection.AssemblyCopyrightAttribute; using InformationalAttribute = System.Reflection.AssemblyInformationalVersionAttribute; @@ -80,6 +81,7 @@ static void RunInternal(ConfuserParameters parameters, CancellationToken token) // 1. Setup context var context = new ConfuserContext(); context.Logger = parameters.GetLogger(); + context.ProgressReporter = parameters.GetProgressReporter(); context.Project = parameters.Project.Clone(); context.PackerInitiated = parameters.PackerInitiated; context.token = token; @@ -107,7 +109,7 @@ static void RunInternal(ConfuserParameters parameters, CancellationToken token) var modulePath = Path.Combine(context.BaseDirectory, firstModule.Path); foreach (var runtimePath in DotNetCorePathResolver.ResolveRuntimePaths(modulePath, context.Logger)) { asmResolver.PostSearchPaths.Add(runtimePath); - context.Logger.DebugFormat("Auto-detected .NET runtime path: {0}", runtimePath); + context.Logger.LogDebug("Auto-detected .NET runtime path: {0}", runtimePath); } } @@ -116,25 +118,25 @@ static void RunInternal(ConfuserParameters parameters, CancellationToken token) Marker marker = parameters.GetMarker(); // 2. Discover plugins - context.Logger.Debug("Discovering plugins..."); + context.Logger.LogDebug("Discovering plugins..."); IList prots; IList packers; IList components; parameters.GetPluginDiscovery().GetPlugins(context, out prots, out packers, out components); - context.Logger.InfoFormat("Discovered {0} protections, {1} packers.", prots.Count, packers.Count); + context.Logger.LogInformation("Discovered {0} protections, {1} packers.", prots.Count, packers.Count); context.CheckCancellation(); // 3. Resolve dependency - context.Logger.Debug("Resolving component dependency..."); + context.Logger.LogDebug("Resolving component dependency..."); try { var resolver = new DependencyResolver(prots); prots = resolver.SortDependency(); } catch (CircularDependencyException ex) { - context.Logger.ErrorException("", ex); + context.Logger.LogError(ex, ""); throw new ConfuserException(ex); } @@ -147,7 +149,7 @@ static void RunInternal(ConfuserParameters parameters, CancellationToken token) context.CheckCancellation(); // 4. Load modules - context.Logger.Info("Loading input modules..."); + context.Logger.LogInformation("Loading input modules..."); marker.Initialize(prots, packers); MarkerResult markings = marker.MarkProject(context.Project, context); context.Modules = new ModuleSorter(markings.Modules).Sort().ToList().AsReadOnly(); @@ -162,13 +164,13 @@ static void RunInternal(ConfuserParameters parameters, CancellationToken token) context.CheckCancellation(); // 5. Initialize components - context.Logger.Info("Initializing..."); + context.Logger.LogInformation("Initializing..."); foreach (ConfuserComponent comp in components) { try { comp.Initialize(context); } catch (Exception ex) { - context.Logger.ErrorException("Error occurred during initialization of '" + comp.Name + "'.", ex); + context.Logger.LogError(ex, "Error occurred during initialization of '" + comp.Name + "'."); throw new ConfuserException(ex); } context.CheckCancellation(); @@ -177,7 +179,7 @@ static void RunInternal(ConfuserParameters parameters, CancellationToken token) context.CheckCancellation(); // 6. Build pipeline - context.Logger.Debug("Building pipeline..."); + context.Logger.LogDebug("Building pipeline..."); var pipeline = new ProtectionPipeline(); context.Pipeline = pipeline; foreach (ConfuserComponent comp in components) { @@ -192,33 +194,33 @@ static void RunInternal(ConfuserParameters parameters, CancellationToken token) ok = true; } catch (AssemblyResolveException ex) { - context.Logger.ErrorException("Failed to resolve an assembly, check if all dependencies are present in the correct version.", ex); + context.Logger.LogError(ex, "Failed to resolve an assembly, check if all dependencies are present in the correct version."); PrintEnvironmentInfo(context); } catch (TypeResolveException ex) { - context.Logger.ErrorException("Failed to resolve a type, check if all dependencies are present in the correct version.", ex); + context.Logger.LogError(ex, "Failed to resolve a type, check if all dependencies are present in the correct version."); PrintEnvironmentInfo(context); } catch (MemberRefResolveException ex) { - context.Logger.ErrorException("Failed to resolve a member, check if all dependencies are present in the correct version.", ex); + context.Logger.LogError(ex, "Failed to resolve a member, check if all dependencies are present in the correct version."); PrintEnvironmentInfo(context); } catch (IOException ex) { - context.Logger.ErrorException("An IO error occurred, check if all input/output locations are readable/writable.", ex); + context.Logger.LogError(ex, "An IO error occurred, check if all input/output locations are readable/writable."); } catch (OperationCanceledException) { - context.Logger.Error("Operation cancelled."); + context.Logger.LogError("Operation cancelled."); } catch (ConfuserException) { // Exception is already handled/logged, so just ignore and report failure } catch (Exception ex) { - context.Logger.ErrorException("Unknown error occurred.", ex); + context.Logger.LogError(ex, "Unknown error occurred."); } finally { if (context.Resolver != null) context.InternalResolver.Clear(); - context.Logger.Finish(ok); + context.ProgressReporter.Finish(ok); } } @@ -268,27 +270,27 @@ static void RunPipeline(ProtectionPipeline pipeline, ConfuserContext context) { pipeline.ExecuteStage(PipelineStage.SaveModules, SaveModules, () => getAllDefs(), context); if (!context.PackerInitiated) - context.Logger.Info("Done."); + context.Logger.LogInformation("Done."); } static void Inspection(ConfuserContext context) { - context.Logger.Info("Resolving dependencies..."); + context.Logger.LogInformation("Resolving dependencies..."); foreach (var dependency in context.Modules .SelectMany(module => module.GetAssemblyRefs().Select(asmRef => Tuple.Create(asmRef, module)))) { var resolved = context.Resolver.Resolve(dependency.Item1, dependency.Item2); if (resolved == null) - context.Logger.WarnFormat("Failed to resolve dependency '{0}' of '{1}'. Some protections may not work correctly.", + context.Logger.LogWarning("Failed to resolve dependency '{0}' of '{1}'. Some protections may not work correctly.", dependency.Item1.FullName, dependency.Item2.Name); } - context.Logger.Debug("Checking Strong Name..."); + context.Logger.LogDebug("Checking Strong Name..."); foreach (var module in context.Modules) { CheckStrongName(context, module); } var marker = context.Registry.GetService(); - context.Logger.Debug("Creating global .cctors..."); + context.Logger.LogDebug("Creating global .cctors..."); foreach (ModuleDefMD module in context.Modules) { TypeDef modType = module.GlobalType; if (modType == null) { @@ -316,12 +318,12 @@ static void CheckStrongName(ConfuserContext context, ModuleDef module) { bool isKeyProvided = snKey != null || (snDelaySign && snPubKeyBytes != null); if (!isKeyProvided && moduleIsSignedOrDelayedSigned) - context.Logger.WarnFormat("[{0}] SN Key or SN public Key is not provided for a signed module, the output may not be working.", module.Name); + context.Logger.LogWarning("[{0}] SN Key or SN public Key is not provided for a signed module, the output may not be working.", module.Name); else if (isKeyProvided && !moduleIsSignedOrDelayedSigned) - context.Logger.WarnFormat("[{0}] SN Key or SN public Key is provided for an unsigned module, the output may not be working.", module.Name); + context.Logger.LogWarning("[{0}] SN Key or SN public Key is provided for an unsigned module, the output may not be working.", module.Name); else if (snPubKeyBytes != null && moduleIsSignedOrDelayedSigned && !module.Assembly.PublicKey.Data.SequenceEqual(snPubKeyBytes)) - context.Logger.WarnFormat("[{0}] Provided SN public Key and signed module's public key do not match, the output may not be working.", + context.Logger.LogWarning("[{0}] Provided SN public Key and signed module's public key do not match, the output may not be working.", module.Name); } @@ -338,7 +340,7 @@ static void CopyPEHeaders(PEHeadersOptions writerOptions, ModuleDefMD module) { } static void BeginModule(ConfuserContext context) { - context.Logger.InfoFormat("Processing module '{0}'...", context.CurrentModule.Name); + context.Logger.LogInformation("Processing module '{0}'...", context.CurrentModule.Name); context.CurrentModuleWriterOptions = new ModuleWriterOptions(context.CurrentModule); CopyPEHeaders(context.CurrentModuleWriterOptions.PEHeadersOptions, context.CurrentModule); @@ -393,7 +395,7 @@ static void EndModule(ConfuserContext context) { output = Path.Combine(context.BaseDirectory, output); string relativeOutput = Utils.GetRelativePath(output, context.BaseDirectory); if (relativeOutput is null) { - context.Logger.WarnFormat("Input file is not inside the base directory. Relative path can't be created. Placing file into output root." + + context.Logger.LogWarning("Input file is not inside the base directory. Relative path can't be created. Placing file into output root." + Environment.NewLine + "Responsible file is: {0}", output); output = Path.GetFileName(output); } @@ -408,7 +410,7 @@ static void EndModule(ConfuserContext context) { } static void WriteModule(ConfuserContext context) { - context.Logger.InfoFormat("Writing module '{0}'...", context.CurrentModule.Name); + context.Logger.LogInformation("Writing module '{0}'...", context.CurrentModule.Name); MemoryStream pdb = null, output = new MemoryStream(); @@ -430,7 +432,7 @@ static void WriteModule(ConfuserContext context) { } static void Debug(ConfuserContext context) { - context.Logger.Info("Finalizing..."); + context.Logger.LogInformation("Finalizing..."); if (!context.Project.Debug) return; for (int i = 0; i < context.OutputModules.Count; i++) { @@ -446,7 +448,7 @@ static void Debug(ConfuserContext context) { static void Pack(ConfuserContext context) { if (context.Packer != null) { - context.Logger.Info("Packing..."); + context.Logger.LogInformation("Packing..."); context.Packer.Pack(context, new ProtectionParameters(context.Packer, context.Modules.OfType().ToList())); } } @@ -458,7 +460,7 @@ static void SaveModules(ConfuserContext context) { string dir = Path.GetDirectoryName(path); if (!Directory.Exists(dir)) Directory.CreateDirectory(dir); - context.Logger.DebugFormat("Saving to '{0}'...", path); + context.Logger.LogDebug("Saving to '{0}'...", path); File.WriteAllBytes(path, context.OutputModules[i]); } } @@ -469,13 +471,13 @@ static void SaveModules(ConfuserContext context) { /// The working context. static void PrintInfo(ConfuserContext context) { if (context.PackerInitiated) { - context.Logger.Info("Protecting packer stub..."); + context.Logger.LogInformation("Protecting packer stub..."); } else { - context.Logger.InfoFormat("{0} {1}", Version, Copyright); + context.Logger.LogInformation("{0} {1}", Version, Copyright); Type mono = Type.GetType("Mono.Runtime"); - context.Logger.InfoFormat("Running on {0}, {1}, {2} bits", + context.Logger.LogInformation("Running on {0}, {1}, {2} bits", Environment.OSVersion, mono == null ? ".NET Framework v" + Environment.Version : @@ -542,27 +544,27 @@ static void PrintEnvironmentInfo(ConfuserContext context) { if (context.PackerInitiated) return; - context.Logger.Error("---BEGIN DEBUG INFO---"); + context.Logger.LogError("---BEGIN DEBUG INFO---"); - context.Logger.Error("Installed Framework Versions:"); + context.Logger.LogError("Installed Framework Versions:"); foreach (string ver in GetFrameworkVersions()) { - context.Logger.ErrorFormat(" {0}", ver.Trim()); + context.Logger.LogError(" {0}", ver.Trim()); } - context.Logger.Error(""); + context.Logger.LogError(""); if (context.Resolver != null) { - context.Logger.Error("Cached assemblies:"); + context.Logger.LogError("Cached assemblies:"); foreach (AssemblyDef asm in context.InternalResolver.GetCachedAssemblies()) { if (string.IsNullOrEmpty(asm.ManifestModule.Location)) - context.Logger.ErrorFormat(" {0}", asm.FullName); + context.Logger.LogError(" {0}", asm.FullName); else - context.Logger.ErrorFormat(" {0} ({1})", asm.FullName, asm.ManifestModule.Location); + context.Logger.LogError(" {0} ({1})", asm.FullName, asm.ManifestModule.Location); foreach (var reference in asm.Modules.OfType().SelectMany(m => m.GetAssemblyRefs())) - context.Logger.ErrorFormat(" {0}", reference.FullName); + context.Logger.LogError(" {0}", reference.FullName); } } - context.Logger.Error("---END DEBUG INFO---"); + context.Logger.LogError("---END DEBUG INFO---"); } } } diff --git a/Confuser.Core/ConfuserParameters.cs b/Confuser.Core/ConfuserParameters.cs index f3e05a2c1..5f9d00ba7 100644 --- a/Confuser.Core/ConfuserParameters.cs +++ b/Confuser.Core/ConfuserParameters.cs @@ -1,5 +1,6 @@ using System; using Confuser.Core.Project; +using Microsoft.Extensions.Logging; namespace Confuser.Core { /// @@ -16,7 +17,13 @@ public class ConfuserParameters { /// Gets or sets the logger that used to log the protection process. /// /// The logger, or null if logging is not needed. - public ILogger Logger { get; set; } + public Microsoft.Extensions.Logging.ILogger Logger { get; set; } + + /// + /// Gets or sets the progress reporter used to report protection progress. + /// + /// The progress reporter, or null if progress reporting is not needed. + public IProgressReporter ProgressReporter { get; set; } internal bool PackerInitiated { get; set; } @@ -36,8 +43,16 @@ public class ConfuserParameters { /// Gets the actual non-null logger. /// /// The logger. - internal ILogger GetLogger() { - return Logger ?? NullLogger.Instance; + internal Microsoft.Extensions.Logging.ILogger GetLogger() { + return Logger ?? Microsoft.Extensions.Logging.Abstractions.NullLogger.Instance; + } + + /// + /// Gets the actual non-null progress reporter. + /// + /// The progress reporter. + internal IProgressReporter GetProgressReporter() { + return ProgressReporter ?? NullProgressReporter.Instance; } /// diff --git a/Confuser.Core/Diagnostics/DiagnosticCollector.cs b/Confuser.Core/Diagnostics/DiagnosticCollector.cs new file mode 100644 index 000000000..3616a5e76 --- /dev/null +++ b/Confuser.Core/Diagnostics/DiagnosticCollector.cs @@ -0,0 +1,163 @@ +using System; +using System.Collections.Generic; +using Confuser.Core.Project; +using Microsoft.Extensions.Logging; + +namespace Confuser.Core.Diagnostics { + /// + /// A single captured log entry, rendered to text at capture time. + /// + public readonly struct DiagnosticLogEntry { + public DiagnosticLogEntry(LogLevel level, string message, string exception) { + Level = level; + Message = message; + Exception = exception; + } + + /// The severity of the entry. + public LogLevel Level { get; } + + /// The rendered message text. + public string Message { get; } + + /// The rendered exception (including stack trace), or null if none. + public string Exception { get; } + } + + /// + /// Wraps the real and used during an + /// obfuscation run, passing every call through while capturing a full-verbosity transcript, + /// timing and outcome. On completion — success or failure — it can produce a self-contained + /// markdown diagnostic report suitable for a bug report. + /// + /// + /// + /// Capture is intentionally independent of the inner logger's level: the collector reports + /// as true and keeps every entry, so a report from a default + /// (Information) run still contains the Debug detail needed to diagnose a failure. Display + /// filtering is preserved because entries are only forwarded to the inner logger when it is + /// enabled for that level. + /// + /// + /// The entry buffer is bounded (see ); once full, the oldest + /// entries are dropped and counted in so the report can note the + /// loss rather than silently mislead. + /// + /// + /// is last-wins: a packer runs a nested engine pass with the same + /// collector, so the top-level run — which finishes last — determines the reported outcome + /// and elapsed time. + /// + /// + public sealed class DiagnosticCollector : ILogger, IProgressReporter { + /// The default maximum number of log entries retained. + public const int DefaultCapacity = 2000; + + readonly ILogger inner; + readonly IProgressReporter innerReporter; + readonly int capacity; + readonly object sync = new object(); + readonly Queue entries; + readonly DateTime begin = DateTime.UtcNow; + int dropped; + bool? successful; + TimeSpan elapsed; + + /// + /// Initializes a new collector. + /// + /// The logger to forward display output to. Required. + /// The progress reporter to forward to, or null. + /// The maximum number of log entries to retain. + public DiagnosticCollector(ILogger inner, IProgressReporter innerReporter = null, int capacity = DefaultCapacity) { + this.inner = inner ?? throw new ArgumentNullException(nameof(inner)); + this.innerReporter = innerReporter; + this.capacity = capacity < 1 ? 1 : capacity; + entries = new Queue(Math.Min(this.capacity, 64)); + } + + /// + /// The project being processed, used to populate the report's configuration section. + /// + public ConfuserProject Project { get; set; } + + /// + /// The run outcome: true on success, false on failure, null if the run + /// never reported completion. + /// + public bool? Successful { + get { lock (sync) return successful; } + } + + /// The elapsed time recorded at the last call. + public TimeSpan Elapsed { + get { lock (sync) return elapsed; } + } + + /// The number of log entries dropped because the buffer was full. + public int DroppedCount { + get { lock (sync) return dropped; } + } + + /// + /// Returns an immutable copy of the currently retained log entries, oldest first. + /// + public IReadOnlyList Snapshot() { + lock (sync) return new List(entries); + } + + /// + /// Produces the markdown diagnostic report. Never throws. + /// + public string GenerateReport() => DiagnosticReport.Generate(this); + + #region ILogger + + public IDisposable BeginScope(TState state) => inner.BeginScope(state); + + public bool IsEnabled(LogLevel logLevel) => true; + + public void Log(LogLevel logLevel, EventId eventId, TState state, Exception exception, + Func formatter) { + string message; + try { + message = formatter != null ? formatter(state, exception) : state?.ToString() ?? string.Empty; + } + catch { + message = state?.ToString() ?? string.Empty; + } + + var entry = new DiagnosticLogEntry(logLevel, message, exception?.ToString()); + lock (sync) { + entries.Enqueue(entry); + while (entries.Count > capacity) { + entries.Dequeue(); + dropped++; + } + } + + // Forward to the inner logger for display; it applies its own level filter. + if (inner.IsEnabled(logLevel)) + inner.Log(logLevel, eventId, state, exception, formatter); + } + + #endregion + + #region IProgressReporter + + public void Progress(int progress, int overall) => innerReporter?.Progress(progress, overall); + + public void EndProgress() => innerReporter?.EndProgress(); + + public void Finish(bool successful) { + lock (sync) { + this.successful = successful; + elapsed = DateTime.UtcNow - begin; + } + + innerReporter?.Finish(successful); + } + + #endregion + } +} diff --git a/Confuser.Core/Diagnostics/DiagnosticRedactor.cs b/Confuser.Core/Diagnostics/DiagnosticRedactor.cs new file mode 100644 index 000000000..9d1059099 --- /dev/null +++ b/Confuser.Core/Diagnostics/DiagnosticRedactor.cs @@ -0,0 +1,52 @@ +using System; +using System.Text; + +namespace Confuser.Core.Diagnostics { + /// + /// Scrubs sensitive information from text destined for a diagnostic report. + /// + /// + /// Diagnostic reports are meant to be pasted into public issue trackers, so any text + /// that flows into one must have the reporter's identity removed. The most common leak + /// is the user-profile path (e.g. C:\Users\alice\...) which appears in absolute + /// paths throughout log output and project configuration. + /// + public static class DiagnosticRedactor { + /// + /// The placeholder substituted for the user-profile directory. + /// + public const string UserPlaceholder = "%USER%"; + + /// + /// Replaces every occurrence of the user-profile directory in + /// with . The match is case-insensitive because Windows + /// paths are. + /// + /// The text to scrub. Returned unchanged if null or empty. + /// The user-profile directory to redact, or null to skip. + /// The scrubbed text. + public static string Redact(string text, string userProfile) { + if (string.IsNullOrEmpty(text) || string.IsNullOrEmpty(userProfile)) + return text; + return ReplaceCaseInsensitive(text, userProfile, UserPlaceholder); + } + + static string ReplaceCaseInsensitive(string input, string search, string replacement) { + var sb = new StringBuilder(input.Length); + int index = 0; + while (true) { + int found = input.IndexOf(search, index, StringComparison.OrdinalIgnoreCase); + if (found < 0) { + sb.Append(input, index, input.Length - index); + break; + } + + sb.Append(input, index, found - index); + sb.Append(replacement); + index = found + search.Length; + } + + return sb.ToString(); + } + } +} diff --git a/Confuser.Core/Diagnostics/DiagnosticReport.cs b/Confuser.Core/Diagnostics/DiagnosticReport.cs new file mode 100644 index 000000000..afae508bd --- /dev/null +++ b/Confuser.Core/Diagnostics/DiagnosticReport.cs @@ -0,0 +1,261 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text; +using Confuser.Core.Project; +using dnlib.DotNet; +using Microsoft.Extensions.Logging; + +namespace Confuser.Core.Diagnostics { + /// + /// Formats the data captured by a into a self-contained + /// markdown report suitable for pasting into a bug report. + /// + public static class DiagnosticReport { + /// + /// Generates the report for the given collector. Never throws — on any failure it returns + /// a minimal report noting the failure, because this runs precisely when things are already + /// going wrong. + /// + public static string Generate(DiagnosticCollector collector) { + if (collector == null) + return string.Empty; + + try { + return Build(collector); + } + catch (Exception ex) { + return "# ConfuserEx Diagnostic Report" + Environment.NewLine + Environment.NewLine + + "Report generation failed: " + ex.Message + Environment.NewLine; + } + } + + static string Build(DiagnosticCollector collector) { + string userProfile = SafeUserProfile(); + var sb = new StringBuilder(); + sb.AppendLine("# ConfuserEx Diagnostic Report"); + sb.AppendLine(); + AppendSystem(sb); + AppendProject(sb, collector.Project, userProfile); + AppendResult(sb, collector.Successful); + AppendLog(sb, collector, userProfile); + AppendElapsed(sb, collector.Elapsed); + return sb.ToString(); + } + + static void AppendSystem(StringBuilder sb) { + sb.AppendLine("## System"); + sb.AppendLine("- OS: " + SafeOsDescription()); + sb.AppendLine("- Runtime: " + SafeRuntimeDescription()); + sb.AppendLine("- Architecture: " + RuntimeInformation.OSArchitecture + + " (process " + RuntimeInformation.ProcessArchitecture + ")"); + sb.AppendLine("- ConfuserExx: " + ConfuserEngine.Version); + sb.AppendLine(); + } + + static void AppendProject(StringBuilder sb, ConfuserProject project, string userProfile) { + sb.AppendLine("## Project Configuration"); + if (project == null) { + sb.AppendLine("- (no project information available)"); + sb.AppendLine(); + return; + } + + sb.AppendLine("- Base Directory: " + Show(DiagnosticRedactor.Redact(project.BaseDirectory, userProfile))); + sb.AppendLine("- Output Directory: " + Show(DiagnosticRedactor.Redact(project.OutputDirectory, userProfile))); + + var modules = project.Where(m => !m.IsExternal).Select(m => m.Path) + .Where(p => !string.IsNullOrEmpty(p)).ToList(); + sb.AppendLine("- Modules: " + (modules.Count > 0 ? string.Join(", ", modules) : "(none)")); + + var targetFrameworks = modules + .Select(m => TryReadTargetFramework(ResolveModulePath(project, m))) + .Where(tfm => !string.IsNullOrEmpty(tfm)) + .Distinct() + .ToList(); + if (targetFrameworks.Count > 0) + sb.AppendLine("- Target Framework: " + string.Join(", ", targetFrameworks)); + + var externals = project.Where(m => m.IsExternal).Select(m => m.Path) + .Where(p => !string.IsNullOrEmpty(p)).ToList(); + if (externals.Count > 0) + sb.AppendLine("- External Modules: " + string.Join(", ", externals)); + + var protections = project.Rules + .SelectMany(r => r) + .Select(s => s.Id) + .Where(id => !string.IsNullOrEmpty(id)) + .Distinct(StringComparer.OrdinalIgnoreCase) + .ToList(); + sb.AppendLine("- Protections: " + (protections.Count > 0 ? string.Join(", ", protections) : "(preset only / none)")); + + var presets = project.Rules.Where(r => r.Preset != ProtectionPreset.None) + .Select(r => r.Preset.ToString().ToLowerInvariant()) + .Distinct().ToList(); + if (presets.Count > 0) + sb.AppendLine("- Presets: " + string.Join(", ", presets)); + + sb.AppendLine("- Packer: " + (project.Packer != null && !string.IsNullOrEmpty(project.Packer.Id) + ? project.Packer.Id : "(none)")); + + var probePaths = (project.ProbePaths ?? Enumerable.Empty()) + .Select(p => DiagnosticRedactor.Redact(p, userProfile)).ToList(); + sb.AppendLine("- Probe Paths: " + (probePaths.Count > 0 ? string.Join(", ", probePaths) : "(none)")); + + var pluginPaths = (project.PluginPaths ?? Enumerable.Empty()) + .Select(p => DiagnosticRedactor.Redact(p, userProfile)).ToList(); + if (pluginPaths.Count > 0) + sb.AppendLine("- Plugins: " + string.Join(", ", pluginPaths)); + + sb.AppendLine(); + } + + static void AppendResult(StringBuilder sb, bool? successful) { + string status = successful == true ? "SUCCESS" : successful == false ? "FAILED" : "(did not complete)"; + sb.AppendLine("## Result: " + status); + sb.AppendLine(); + } + + static void AppendLog(StringBuilder sb, DiagnosticCollector collector, string userProfile) { + sb.AppendLine("## Log Output"); + + var lines = new List(); + if (collector.DroppedCount > 0) + lines.Add("... " + collector.DroppedCount + " earlier log entries truncated ..."); + + foreach (var entry in collector.Snapshot()) { + lines.Add(Prefix(entry.Level) + DiagnosticRedactor.Redact(entry.Message, userProfile)); + if (!string.IsNullOrEmpty(entry.Exception)) + foreach (var exLine in entry.Exception.Split('\n')) + lines.Add(DiagnosticRedactor.Redact(exLine.TrimEnd('\r'), userProfile)); + } + + string body = string.Join(Environment.NewLine, lines); + string fence = MakeFence(body); + sb.AppendLine(fence); + sb.AppendLine(body); + sb.AppendLine(fence); + sb.AppendLine(); + } + + static void AppendElapsed(StringBuilder sb, TimeSpan elapsed) { + sb.AppendLine("## Elapsed: " + + elapsed.TotalSeconds.ToString("F1", CultureInfo.InvariantCulture) + " s"); + } + + /// + /// Chooses a code-fence longer than the longest run of backticks in , + /// so log content containing its own ``` fences cannot break out of the block. + /// + static string MakeFence(string body) { + int max = 0, run = 0; + foreach (char c in body) { + if (c == '`') { + run++; + if (run > max) max = run; + } + else { + run = 0; + } + } + + return new string('`', Math.Max(3, max + 1)); + } + + static string Prefix(LogLevel level) { + switch (level) { + case LogLevel.Trace: + case LogLevel.Debug: + return "[DEBUG] "; + case LogLevel.Information: + return "[INFO] "; + case LogLevel.Warning: + return "[WARN] "; + case LogLevel.Error: + case LogLevel.Critical: + return "[ERROR] "; + default: + return ""; + } + } + + static string Show(string value) => string.IsNullOrEmpty(value) ? "(not set)" : value; + + static string ResolveModulePath(ConfuserProject project, string modulePath) { + try { + if (!string.IsNullOrEmpty(project.BaseDirectory)) + return Path.Combine(project.BaseDirectory, modulePath); + } + catch { + // Fall through to the bare module path. + } + + return modulePath; + } + + /// + /// Best-effort read of an assembly's target-framework moniker (e.g. + /// .NETCoreApp,Version=v8.0) from its TargetFrameworkAttribute. Returns + /// null if the file is missing, is not a valid assembly, or carries no such + /// attribute. The file is read into memory so it is never locked. + /// + public static string TryReadTargetFramework(string assemblyPath) { + try { + if (string.IsNullOrEmpty(assemblyPath) || !File.Exists(assemblyPath)) + return null; + + using (var module = ModuleDefMD.Load(File.ReadAllBytes(assemblyPath))) { + var assembly = module.Assembly; + if (assembly == null) + return null; + + foreach (var attr in assembly.CustomAttributes) { + if (attr.TypeFullName != "System.Runtime.Versioning.TargetFrameworkAttribute") + continue; + if (attr.ConstructorArguments.Count == 0) + continue; + + var moniker = attr.ConstructorArguments[0].Value?.ToString(); + if (!string.IsNullOrEmpty(moniker)) + return moniker; + } + } + } + catch { + // Diagnostic best-effort: any failure to read the framework is non-fatal. + } + + return null; + } + + static string SafeOsDescription() { + try { + return RuntimeInformation.OSDescription.Trim(); + } + catch { + return Environment.OSVersion.ToString(); + } + } + + static string SafeRuntimeDescription() { + try { + return RuntimeInformation.FrameworkDescription; + } + catch { + return ".NET " + Environment.Version; + } + } + + static string SafeUserProfile() { + try { + return Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); + } + catch { + return null; + } + } + } +} diff --git a/Confuser.Core/DnlibUtils.cs b/Confuser.Core/DnlibUtils.cs index 440b1af97..dd5e12bd6 100644 --- a/Confuser.Core/DnlibUtils.cs +++ b/Confuser.Core/DnlibUtils.cs @@ -166,7 +166,9 @@ public static bool IsVisibleOutside(this TypeDef typeDef, bool exeNonPublic = tr /// The full name of the type of custom attribute. /// true if the specified object has custom attribute; otherwise, false. public static bool HasAttribute(this IHasCustomAttribute obj, string fullName) { - return obj.CustomAttributes.Any(attr => attr.TypeFullName == fullName); + // dnlib's CustomAttributeCollection.IsDefined is the by-full-name lookup that dnlib + // 4.2 optimized — prefer it over a manual LINQ scan. + return obj.CustomAttributes.IsDefined(fullName); } /// diff --git a/Confuser.Core/DotNetCorePathResolver.cs b/Confuser.Core/DotNetCorePathResolver.cs index e056613d6..b35503ce4 100644 --- a/Confuser.Core/DotNetCorePathResolver.cs +++ b/Confuser.Core/DotNetCorePathResolver.cs @@ -3,6 +3,7 @@ using System.IO; using System.Linq; using System.Text.RegularExpressions; +using Microsoft.Extensions.Logging; namespace Confuser.Core { /// @@ -13,7 +14,7 @@ internal static class DotNetCorePathResolver { /// Resolves runtime assembly paths for a given module by parsing its runtimeconfig.json /// or probing standard dotnet installation directories. /// - public static IEnumerable ResolveRuntimePaths(string modulePath, ILogger logger) { + public static IEnumerable ResolveRuntimePaths(string modulePath, Microsoft.Extensions.Logging.ILogger logger) { // 1. Try runtimeconfig.json next to the module (target framework gets priority) var runtimeConfigPaths = Enumerable.Empty(); var runtimeConfig = FindRuntimeConfig(modulePath); @@ -39,36 +40,36 @@ static string FindRuntimeConfig(string modulePath) { return File.Exists(configPath) ? configPath : null; } - static IEnumerable GetPathsFromRuntimeConfig(string configPath, ILogger logger) { + static IEnumerable GetPathsFromRuntimeConfig(string configPath, Microsoft.Extensions.Logging.ILogger logger) { string content; try { content = File.ReadAllText(configPath); } catch (IOException ex) { - logger.WarnFormat("Failed to read runtime config '{0}': {1}", configPath, ex.Message); + logger.LogWarning("Failed to read runtime config '{0}': {1}", configPath, ex.Message); yield break; } catch (UnauthorizedAccessException ex) { - logger.WarnFormat("Access denied reading runtime config '{0}': {1}", configPath, ex.Message); + logger.LogWarning("Access denied reading runtime config '{0}': {1}", configPath, ex.Message); yield break; } var frameworks = ParseFrameworks(content); if (frameworks.Count == 0) { - logger.DebugFormat("No framework references found in '{0}'.", configPath); + logger.LogDebug("No framework references found in '{0}'.", configPath); yield break; } var dotnetRoot = GetDotNetRoot(); if (dotnetRoot == null) { - logger.Warn("Could not locate .NET installation directory. Set DOTNET_ROOT environment variable if installed in a non-standard location."); + logger.LogWarning("Could not locate .NET installation directory. Set DOTNET_ROOT environment variable if installed in a non-standard location."); yield break; } foreach (var fw in frameworks) { var sharedDir = Path.Combine(dotnetRoot, "shared", fw.Name); if (!Directory.Exists(sharedDir)) { - logger.DebugFormat("Framework directory not found: {0}", sharedDir); + logger.LogDebug("Framework directory not found: {0}", sharedDir); continue; } @@ -88,7 +89,7 @@ static IEnumerable GetPathsFromRuntimeConfig(string configPath, ILogger if (best != null) yield return best; else - logger.WarnFormat("No installed runtime found matching {0} {1} in {2}", fw.Name, fw.Version, sharedDir); + logger.LogWarning("No installed runtime found matching {0} {1} in {2}", fw.Name, fw.Version, sharedDir); } } @@ -137,16 +138,16 @@ static string GetMajorMinor(string version) { return parts.Length >= 2 ? parts[0] + "." + parts[1] : version; } - static IEnumerable ProbeAllInstalledRuntimes(ILogger logger) { + static IEnumerable ProbeAllInstalledRuntimes(Microsoft.Extensions.Logging.ILogger logger) { var dotnetRoot = GetDotNetRoot(); if (dotnetRoot == null) { - logger.Warn("Could not locate .NET installation directory for runtime probing."); + logger.LogWarning("Could not locate .NET installation directory for runtime probing."); yield break; } var sharedDir = Path.Combine(dotnetRoot, "shared"); if (!Directory.Exists(sharedDir)) { - logger.WarnFormat("Shared framework directory not found: {0}", sharedDir); + logger.LogWarning("Shared framework directory not found: {0}", sharedDir); yield break; } diff --git a/Confuser.Core/ILogger.cs b/Confuser.Core/ILogger.cs deleted file mode 100644 index ec55db448..000000000 --- a/Confuser.Core/ILogger.cs +++ /dev/null @@ -1,104 +0,0 @@ -using System; - -namespace Confuser.Core { - /// - /// Defines a logger used to log Confuser events - /// - public interface ILogger { - /// - /// Logs a message at DEBUG level. - /// - /// The message. - void Debug(string msg); - - /// - /// Logs a message at DEBUG level with specified parameters. - /// - /// The format. - /// The arguments. - void DebugFormat(string format, params object[] args); - - /// - /// Logs a message at INFO level. - /// - /// The message. - void Info(string msg); - - /// - /// Logs a message at INFO level with specified parameters. - /// - /// The format. - /// The arguments. - void InfoFormat(string format, params object[] args); - - /// - /// Logs a message at WARN level. - /// - /// The message. - void Warn(string msg); - - /// - /// Logs a message at WARN level with specified parameters. - /// - /// The format. - /// The arguments. - void WarnFormat(string format, params object[] args); - - /// - /// Logs a message at WARN level with specified exception. - /// - /// The message. - /// The exception. - void WarnException(string msg, Exception ex); - - /// - /// Logs a message at ERROR level. - /// - /// The message. - void Error(string msg); - - /// - /// Logs a message at ERROR level with specified parameters. - /// - /// The format. - /// The arguments. - void ErrorFormat(string format, params object[] args); - - /// - /// Logs a message at ERROR level with specified exception. - /// - /// The message. - /// The exception. - void ErrorException(string msg, Exception ex); - - /// - /// Logs the progress of protection. - /// - /// - /// This method is intended to be used with . - /// - /// - /// - /// for (int i = 0; i < defs.Length; i++) { - /// logger.Progress(i + 1, defs.Length); - /// } - /// logger.EndProgress(); - /// - /// - /// The total work amount . - /// The amount of work done. - void Progress(int progress, int overall); - - /// - /// End the progress of protection. - /// - /// - void EndProgress(); - - /// - /// Logs the finish of protection. - /// - /// Indicated whether the protection process is successful. - void Finish(bool successful); - } -} diff --git a/Confuser.Core/IProgressReporter.cs b/Confuser.Core/IProgressReporter.cs new file mode 100644 index 000000000..eb1812e67 --- /dev/null +++ b/Confuser.Core/IProgressReporter.cs @@ -0,0 +1,24 @@ +namespace Confuser.Core { + /// + /// Reports progress and completion of a protection process. + /// + public interface IProgressReporter { + /// + /// Reports the progress of protection. + /// + /// The amount of work done. + /// The total work amount. + void Progress(int progress, int overall); + + /// + /// Signals the end of a progress sequence. + /// + void EndProgress(); + + /// + /// Signals the finish of the protection process. + /// + /// Whether the protection process succeeded. + void Finish(bool successful); + } +} diff --git a/Confuser.Core/Marker.cs b/Confuser.Core/Marker.cs index 889ae036d..95709a058 100644 --- a/Confuser.Core/Marker.cs +++ b/Confuser.Core/Marker.cs @@ -7,6 +7,7 @@ using Confuser.Core.Project; using Confuser.Core.Project.Patterns; using dnlib.DotNet; +using Microsoft.Extensions.Logging; namespace Confuser.Core { using Rules = Dictionary; @@ -83,7 +84,7 @@ public static StrongNamePublicKey LoadSNPubKey(ConfuserContext context, string p return new StrongNamePublicKey(path); } catch (Exception ex) { - context.Logger.ErrorException("Cannot load the Strong Name Public Key located at: " + path, ex); + context.Logger.LogError(ex, "Cannot load the Strong Name Public Key located at: " + path); throw new ConfuserException(ex); } } @@ -117,7 +118,7 @@ public static StrongNameKey LoadSNKey(ConfuserContext context, string path, stri return new StrongNameKey(path); } catch (Exception ex) { - context.Logger.ErrorException("Cannot load the Strong Name Key located at: " + path, ex); + context.Logger.LogError(ex, "Cannot load the Strong Name Key located at: " + path); throw new ConfuserException(ex); } } @@ -134,11 +135,11 @@ protected internal virtual MarkerResult MarkProject(ConfuserProject proj, Confus if (proj.Packer != null) { if (!packers.ContainsKey(proj.Packer.Id)) { - context.Logger.ErrorFormat("Cannot find packer with ID '{0}'.", proj.Packer.Id); + context.Logger.LogError("Cannot find packer with ID '{0}'.", proj.Packer.Id); throw new ConfuserException(null); } if (proj.Debug) - context.Logger.Warn("Generated Debug symbols might not be usable with packers!"); + context.Logger.LogWarning("Generated Debug symbols might not be usable with packers!"); packer = packers[proj.Packer.Id]; packerParams = new Dictionary(proj.Packer, StringComparer.OrdinalIgnoreCase); @@ -167,7 +168,7 @@ protected internal virtual MarkerResult MarkProject(ConfuserProject proj, Confus } foreach (var module in modules) { - context.Logger.InfoFormat("Loading '{0}'...", module.Item1.Path); + context.Logger.LogInformation("Loading '{0}'...", module.Item1.Path); Rules rules = ParseRules(proj, module.Item1, context); context.Annotations.Set(module.Item2, SNKey, LoadSNKey(context, module.Item1.SNKeyPath == null ? null : Path.Combine(proj.BaseDirectory, module.Item1.SNKeyPath), module.Item1.SNKeyPassword)); @@ -219,12 +220,12 @@ protected Rules ParseRules(ConfuserProject proj, ProjectModule module, ConfuserC ret.Add(rule, parser.Parse(rule.Pattern)); } catch (InvalidPatternException ex) { - context.Logger.ErrorFormat("Invalid rule pattern: " + rule.Pattern + ": {0}", ex.Message); + context.Logger.LogError("Invalid rule pattern: " + rule.Pattern + ": {0}", ex.Message); throw new ConfuserException(ex); } foreach (var setting in rule) { if (!protections.ContainsKey(setting.Id)) { - context.Logger.ErrorFormat("Cannot find protection with ID '{0}'.", setting.Id); + context.Logger.LogError("Cannot find protection with ID '{0}'.", setting.Id); throw new ConfuserException(null); } } diff --git a/Confuser.Core/NullLogger.cs b/Confuser.Core/NullLogger.cs deleted file mode 100644 index c40382274..000000000 --- a/Confuser.Core/NullLogger.cs +++ /dev/null @@ -1,64 +0,0 @@ -using System; -using dnlib.DotNet; - -namespace Confuser.Core { - /// - /// An implementation that doesn't actually do any logging. - /// - internal class NullLogger : ILogger { - /// - /// The singleton instance of . - /// - public static readonly NullLogger Instance = new NullLogger(); - - /// - /// Prevents a default instance of the class from being created. - /// - NullLogger() { } - - /// - public void Debug(string msg) { } - - /// - public void DebugFormat(string format, params object[] args) { } - - /// - public void Info(string msg) { } - - /// - public void InfoFormat(string format, params object[] args) { } - - /// - public void Warn(string msg) { } - - /// - public void WarnFormat(string format, params object[] args) { } - - /// - public void WarnException(string msg, Exception ex) { } - - /// - public void Error(string msg) { } - - /// - public void ErrorFormat(string format, params object[] args) { } - - /// - public void ErrorException(string msg, Exception ex) { } - - /// - public void Progress(int overall, int progress) { } - - /// - public void EndProgress() { } - - /// - public void Finish(bool successful) { } - - /// - public void BeginModule(ModuleDef module) { } - - /// - public void EndModule(ModuleDef module) { } - } -} diff --git a/Confuser.Core/NullProgressReporter.cs b/Confuser.Core/NullProgressReporter.cs new file mode 100644 index 000000000..53901f472 --- /dev/null +++ b/Confuser.Core/NullProgressReporter.cs @@ -0,0 +1,14 @@ +namespace Confuser.Core { + /// + /// An implementation that discards all progress reports. + /// + internal sealed class NullProgressReporter : IProgressReporter { + public static readonly NullProgressReporter Instance = new NullProgressReporter(); + + NullProgressReporter() { } + + public void Progress(int progress, int overall) { } + public void EndProgress() { } + public void Finish(bool successful) { } + } +} diff --git a/Confuser.Core/ObfAttrMarker.cs b/Confuser.Core/ObfAttrMarker.cs index 1c8cb068c..8dbe5ecca 100644 --- a/Confuser.Core/ObfAttrMarker.cs +++ b/Confuser.Core/ObfAttrMarker.cs @@ -8,6 +8,7 @@ using Confuser.Core.Project; using Confuser.Core.Project.Patterns; using dnlib.DotNet; +using Microsoft.Extensions.Logging; namespace Confuser.Core { using Rules = Dictionary; @@ -221,7 +222,7 @@ bool ToInfo(ObfuscationAttributeInfo attr, out ProtectionSettingsInfo info) { } if (!ok) { - context.Logger.WarnFormat("Ignoring rule '{0}' in {1}.", info.Settings, attr.Owner); + context.Logger.LogWarning("Ignoring rule '{0}' in {1}.", info.Settings, attr.Owner); return false; } @@ -299,7 +300,7 @@ protected internal override MarkerResult MarkProject(ConfuserProject proj, Confu if (proj.Packer != null) { if (!packers.ContainsKey(proj.Packer.Id)) { - context.Logger.ErrorFormat("Cannot find packer with ID '{0}'.", proj.Packer.Id); + context.Logger.LogError("Cannot find packer with ID '{0}'.", proj.Packer.Id); throw new ConfuserException(null); } @@ -325,20 +326,20 @@ protected internal override MarkerResult MarkProject(ConfuserProject proj, Confu modules.Add(Tuple.Create(module, modDef)); } catch (BadImageFormatException ex) { - context.Logger.ErrorFormat("Failed to load \"{0}\" - Assembly does not appear to be a .NET assembly: \"{1}\".", module.Path, ex.Message); + context.Logger.LogError("Failed to load \"{0}\" - Assembly does not appear to be a .NET assembly: \"{1}\".", module.Path, ex.Message); if (module.Path.EndsWith(".exe", StringComparison.OrdinalIgnoreCase)) { var dllPath = Path.ChangeExtension(module.Path, ".dll"); var fullDllPath = Path.Combine(proj.BaseDirectory, dllPath); if (File.Exists(fullDllPath)) - context.Logger.ErrorFormat("Hint: .NET 6+ apps use a native host .exe — try obfuscating \"{0}\" instead.", dllPath); + context.Logger.LogError("Hint: .NET 6+ apps use a native host .exe — try obfuscating \"{0}\" instead.", dllPath); else - context.Logger.Error("Hint: For .NET 6+ apps, obfuscate the .dll file, not the .exe (which is a native host stub)."); + context.Logger.LogError("Hint: For .NET 6+ apps, obfuscate the .dll file, not the .exe (which is a native host stub)."); } throw new ConfuserException(ex); } } foreach (var module in modules) { - context.Logger.InfoFormat("Loading '{0}'...", module.Item1.Path); + context.Logger.LogInformation("Loading '{0}'...", module.Item1.Path); Rules rules = ParseRules(proj, module.Item1, context); MarkModule(module.Item1, module.Item2, rules, module == modules[0]); @@ -351,7 +352,7 @@ protected internal override MarkerResult MarkProject(ConfuserProject proj, Confu } if (proj.Debug && proj.Packer != null) - context.Logger.Warn("Generated Debug symbols might not be usable with packers!"); + context.Logger.LogWarning("Generated Debug symbols might not be usable with packers!"); return new MarkerResult(modules.Select(module => module.Item2).ToList(), packer, extModules); } @@ -384,7 +385,7 @@ ProtectionSettingsInfo AddRule(ObfuscationAttributeInfo attr, List @@ -73,7 +74,8 @@ protected void ProtectStub(ConfuserContext context, string fileName, byte[] modu ConfuserEngine .Run( new ConfuserParameters { - Logger = new PackerLogger(context.Logger), + Logger = context.Logger, + ProgressReporter = new PackerProgressReporter(context.ProgressReporter, context.Logger), PluginDiscovery = discovery, Marker = new PackerMarker(snKey, snPubKey, snDelaySig, snSigKey, snPubSigKey), Project = proj, @@ -81,7 +83,7 @@ protected void ProtectStub(ConfuserContext context, string fileName, byte[] modu }, context.token).Wait(); } catch (AggregateException ex) { - context.Logger.Error("Failed to protect packer stub."); + context.Logger.LogError("Failed to protect packer stub."); throw new ConfuserException(ex); } @@ -95,71 +97,33 @@ protected void ProtectStub(ConfuserContext context, string fileName, byte[] modu } } catch (IOException ex) { - context.Logger.WarnException("Failed to remove temporary files of packer.", ex); + context.Logger.LogWarning(ex, "Failed to remove temporary files of packer."); } } } } - internal class PackerLogger : ILogger { - readonly ILogger baseLogger; + internal class PackerProgressReporter : IProgressReporter { + readonly IProgressReporter baseReporter; + readonly Microsoft.Extensions.Logging.ILogger baseLogger; - public PackerLogger(ILogger baseLogger) { + public PackerProgressReporter(IProgressReporter baseReporter, Microsoft.Extensions.Logging.ILogger baseLogger) { + this.baseReporter = baseReporter; this.baseLogger = baseLogger; } - public void Debug(string msg) { - baseLogger.Debug(msg); - } - - public void DebugFormat(string format, params object[] args) { - baseLogger.DebugFormat(format, args); - } - - public void Info(string msg) { - baseLogger.Info(msg); - } - - public void InfoFormat(string format, params object[] args) { - baseLogger.InfoFormat(format, args); - } - - public void Warn(string msg) { - baseLogger.Warn(msg); - } - - public void WarnFormat(string format, params object[] args) { - baseLogger.WarnFormat(format, args); - } - - public void WarnException(string msg, Exception ex) { - baseLogger.WarnException(msg, ex); - } - - public void Error(string msg) { - baseLogger.Error(msg); - } - - public void ErrorFormat(string format, params object[] args) { - baseLogger.ErrorFormat(format, args); - } - - public void ErrorException(string msg, Exception ex) { - baseLogger.ErrorException(msg, ex); - } - public void Progress(int progress, int overall) { - baseLogger.Progress(progress, overall); + baseReporter.Progress(progress, overall); } public void EndProgress() { - baseLogger.EndProgress(); + baseReporter.EndProgress(); } public void Finish(bool successful) { if (!successful) throw new ConfuserException(null); - baseLogger.Info("Finish protecting packer stub."); + baseLogger.LogInformation("Finish protecting packer stub."); } } diff --git a/Confuser.Core/PluginDiscovery.cs b/Confuser.Core/PluginDiscovery.cs index 642dafcf8..c42de92d6 100644 --- a/Confuser.Core/PluginDiscovery.cs +++ b/Confuser.Core/PluginDiscovery.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.IO; using System.Reflection; +using Microsoft.Extensions.Logging; namespace Confuser.Core { /// @@ -64,7 +65,7 @@ protected static void AddPlugins( protections.Add((Protection)Activator.CreateInstance(i)); } catch (Exception ex) { - context.Logger.ErrorException("Failed to instantiate protection '" + i.Name + "'.", ex); + context.Logger.LogError(ex, "Failed to instantiate protection '" + i.Name + "'."); } } else if (typeof(Packer).IsAssignableFrom(i)) { @@ -72,7 +73,7 @@ protected static void AddPlugins( packers.Add((Packer)Activator.CreateInstance(i)); } catch (Exception ex) { - context.Logger.ErrorException("Failed to instantiate packer '" + i.Name + "'.", ex); + context.Logger.LogError(ex, "Failed to instantiate packer '" + i.Name + "'."); } } else if (typeof(ConfuserComponent).IsAssignableFrom(i)) { @@ -80,7 +81,7 @@ protected static void AddPlugins( components.Add((ConfuserComponent)Activator.CreateInstance(i)); } catch (Exception ex) { - context.Logger.ErrorException("Failed to instantiate component '" + i.Name + "'.", ex); + context.Logger.LogError(ex, "Failed to instantiate component '" + i.Name + "'."); } } } @@ -103,7 +104,7 @@ protected virtual void GetPluginsInternal( AddPlugins(context, protections, packers, components, protAsm); } catch (Exception ex) { - context.Logger.WarnException("Failed to load built-in protections.", ex); + context.Logger.LogWarning(ex, "Failed to load built-in protections."); } try { @@ -111,7 +112,7 @@ protected virtual void GetPluginsInternal( AddPlugins(context, protections, packers, components, renameAsm); } catch (Exception ex) { - context.Logger.WarnException("Failed to load renamer.", ex); + context.Logger.LogWarning(ex, "Failed to load renamer."); } try { @@ -119,7 +120,7 @@ protected virtual void GetPluginsInternal( AddPlugins(context, protections, packers, components, renameAsm); } catch (Exception ex) { - context.Logger.WarnException("Failed to load dynamic cipher library.", ex); + context.Logger.LogWarning(ex, "Failed to load dynamic cipher library."); } foreach (string pluginPath in context.Project.PluginPaths) { @@ -129,7 +130,7 @@ protected virtual void GetPluginsInternal( AddPlugins(context, protections, packers, components, plugin); } catch (Exception ex) { - context.Logger.WarnException("Failed to load plugin '" + pluginPath + "'.", ex); + context.Logger.LogWarning(ex, "Failed to load plugin '" + pluginPath + "'."); } } } diff --git a/Confuser.Core/ProtectionPipeline.cs b/Confuser.Core/ProtectionPipeline.cs index 79da655dc..ba798f559 100644 --- a/Confuser.Core/ProtectionPipeline.cs +++ b/Confuser.Core/ProtectionPipeline.cs @@ -3,6 +3,7 @@ using System.Diagnostics; using System.Linq; using dnlib.DotNet; +using Microsoft.Extensions.Logging; namespace Confuser.Core { /// @@ -127,14 +128,14 @@ public T FindPhase() where T : ProtectionPhase { internal void ExecuteStage(PipelineStage stage, Action func, Func> targets, ConfuserContext context) { foreach (ProtectionPhase pre in preStage[stage]) { context.CheckCancellation(); - context.Logger.DebugFormat("Executing '{0}' phase...", pre.Name); + context.Logger.LogDebug("Executing '{0}' phase...", pre.Name); pre.Execute(context, new ProtectionParameters(pre.Parent, Filter(context, targets(), pre))); } context.CheckCancellation(); func(context); context.CheckCancellation(); foreach (ProtectionPhase post in postStage[stage]) { - context.Logger.DebugFormat("Executing '{0}' phase...", post.Name); + context.Logger.LogDebug("Executing '{0}' phase...", post.Name); post.Execute(context, new ProtectionParameters(post.Parent, Filter(context, targets(), post))); context.CheckCancellation(); } @@ -170,7 +171,7 @@ static IList Filter(ConfuserContext context, IList targets ProtectionSettings parameters = ProtectionParameters.GetParameters(context, def); Debug.Assert(parameters != null); if (parameters == null) { - context.Logger.ErrorFormat("'{0}' not marked for obfuscation, possibly a bug.", def); + context.Logger.LogError("'{0}' not marked for obfuscation, possibly a bug.", def); throw new ConfuserException(null); } return parameters.ContainsKey(phase.Parent); diff --git a/Confuser.Core/Utils.cs b/Confuser.Core/Utils.cs index e941cc1e6..9df5c0d01 100644 --- a/Confuser.Core/Utils.cs +++ b/Confuser.Core/Utils.cs @@ -204,33 +204,33 @@ public static void RemoveWhere(this IList self, Predicate match) { } /// - /// Returns a that log the progress of iterating the specified list. + /// Returns a that reports the progress of iterating the specified list. /// /// The type of list element /// The list. - /// The logger. + /// The progress reporter. /// A wrapper of the list. - public static IEnumerable WithProgress(this IEnumerable enumerable, ILogger logger) { + public static IEnumerable WithProgress(this IEnumerable enumerable, IProgressReporter reporter) { switch (enumerable) { case IReadOnlyCollection readOnlyCollection: - return WithProgress(enumerable, readOnlyCollection.Count, logger); + return WithProgress(enumerable, readOnlyCollection.Count, reporter); case ICollection collection: - return WithProgress(enumerable, collection.Count, logger); + return WithProgress(enumerable, collection.Count, reporter); default: var buffered = enumerable.ToList(); - return WithProgress(buffered, buffered.Count, logger); + return WithProgress(buffered, buffered.Count, reporter); } } - public static IEnumerable WithProgress(this IEnumerable enumerable, int totalCount, ILogger logger) { + public static IEnumerable WithProgress(this IEnumerable enumerable, int totalCount, IProgressReporter reporter) { var counter = 0; foreach (var obj in enumerable) { - logger.Progress(counter, totalCount); + reporter.Progress(counter, totalCount); yield return obj; counter++; } - logger.Progress(totalCount, totalCount); - logger.EndProgress(); + reporter.Progress(totalCount, totalCount); + reporter.EndProgress(); } } } diff --git a/Confuser.Core/WatermarkingProtection.cs b/Confuser.Core/WatermarkingProtection.cs index 1b1cdb967..7fff32205 100644 --- a/Confuser.Core/WatermarkingProtection.cs +++ b/Confuser.Core/WatermarkingProtection.cs @@ -2,6 +2,7 @@ using Confuser.Core.Services; using dnlib.DotNet; using dnlib.DotNet.Emit; +using Microsoft.Extensions.Logging; namespace Confuser.Core { public sealed class WatermarkingProtection : Protection { @@ -45,7 +46,7 @@ public WatermarkingPhase(ConfuserComponent parent) : base(parent) { } protected internal override void Execute(ConfuserContext context, ProtectionParameters parameters) { var marker = context.Registry.GetService(); - context.Logger.Debug("Watermarking..."); + context.Logger.LogDebug("Watermarking..."); foreach (var module in parameters.Targets.OfType()) { var attrRef = module.CorLibTypes.GetTypeRef("System", "Attribute"); var attrType = module.FindNormal("ConfusedByAttribute"); diff --git a/Confuser.MSBuild.Tasks/ConfuseTask.cs b/Confuser.MSBuild.Tasks/ConfuseTask.cs index f4526c910..02986a3f0 100644 --- a/Confuser.MSBuild.Tasks/ConfuseTask.cs +++ b/Confuser.MSBuild.Tasks/ConfuseTask.cs @@ -24,17 +24,18 @@ public override bool Execute() { project.Load(xmlDoc); project.OutputDirectory = Path.GetDirectoryName(Path.GetFullPath(OutputAssembly.ItemSpec)); - var logger = new MSBuildLogger(Log); + var progressReporter = new MSBuildProgressReporter(); var parameters = new ConfuserParameters { Project = project, - Logger = logger + Logger = new MSBuildMelLogger(Log), + ProgressReporter = progressReporter }; ConfuserEngine.Run(parameters).Wait(); ConfusedFiles = project.Select(m => new TaskItem(Path.Combine(project.OutputDirectory, m.Path))).Cast().ToArray(); - return !logger.HasError; + return !progressReporter.HasError; } } } diff --git a/Confuser.MSBuild.Tasks/MSBuildLogger.cs b/Confuser.MSBuild.Tasks/MSBuildLogger.cs index fdcb2319b..8f1f7c81f 100644 --- a/Confuser.MSBuild.Tasks/MSBuildLogger.cs +++ b/Confuser.MSBuild.Tasks/MSBuildLogger.cs @@ -1,61 +1,54 @@ -using System; +using System; using Microsoft.Build.Framework; using Microsoft.Build.Utilities; -using ILogger = Confuser.Core.ILogger; +using Microsoft.Extensions.Logging; namespace Confuser.MSBuild.Tasks { - internal sealed class MSBuildLogger : ILogger { + internal sealed class MSBuildMelLogger : Microsoft.Extensions.Logging.ILogger { private readonly TaskLoggingHelper loggingHelper; - internal bool HasError { get; private set; } - - internal MSBuildLogger(TaskLoggingHelper loggingHelper) => + internal MSBuildMelLogger(TaskLoggingHelper loggingHelper) => this.loggingHelper = loggingHelper ?? throw new ArgumentNullException(nameof(loggingHelper)); - void ILogger.Debug(string msg) => loggingHelper.LogMessage(MessageImportance.Low, "[DEBUG] " + msg); - - void ILogger.DebugFormat(string format, params object[] args) { - loggingHelper.LogMessage(MessageImportance.Low, "[DEBUG] " + format, args); + public IDisposable BeginScope(TState state) => null; + + public bool IsEnabled(LogLevel logLevel) => true; + + public void Log(LogLevel logLevel, EventId eventId, TState state, + Exception exception, Func formatter) { + var message = formatter(state, exception); + switch (logLevel) { + case LogLevel.Trace: + case LogLevel.Debug: + loggingHelper.LogMessage(MessageImportance.Low, message); + break; + case LogLevel.Information: + loggingHelper.LogMessage(MessageImportance.Normal, message); + break; + case LogLevel.Warning: + loggingHelper.LogWarning(message); + if (exception != null) loggingHelper.LogWarningFromException(exception); + break; + case LogLevel.Error: + case LogLevel.Critical: + loggingHelper.LogError(message); + if (exception != null) loggingHelper.LogErrorFromException(exception); + break; + } } + } - void ILogger.EndProgress() { } + internal sealed class MSBuildProgressReporter : Confuser.Core.IProgressReporter { + internal bool HasError { get; private set; } - void ILogger.Error(string msg) { - loggingHelper.LogError(msg); - HasError = true; - } + void Confuser.Core.IProgressReporter.Progress(int progress, int overall) { } - void ILogger.ErrorException(string msg, Exception ex) { - loggingHelper.LogError(msg); - loggingHelper.LogErrorFromException(ex); - HasError = true; - } + void Confuser.Core.IProgressReporter.EndProgress() { } - void ILogger.ErrorFormat(string format, params object[] args) { - loggingHelper.LogError(format, args); - HasError = true; - } - - void ILogger.Finish(bool successful) { + void Confuser.Core.IProgressReporter.Finish(bool successful) { if (!successful) { - HasError = false; + HasError = true; } } - - void ILogger.Info(string msg) => loggingHelper.LogMessage(MessageImportance.Normal, msg); - - void ILogger.InfoFormat(string format, params object[] args) => - loggingHelper.LogMessage(MessageImportance.Normal, format, args); - - void ILogger.Progress(int progress, int overall) { } - - void ILogger.Warn(string msg) => loggingHelper.LogWarning(msg); - - void ILogger.WarnException(string msg, Exception ex) { - loggingHelper.LogWarning(msg); - loggingHelper.LogWarningFromException(ex); - } - - void ILogger.WarnFormat(string format, params object[] args) => loggingHelper.LogWarning(format, args); } } diff --git a/Confuser.Protections/AntiTamper/JITBody.cs b/Confuser.Protections/AntiTamper/JITBody.cs index f6c799eee..7149899f8 100644 --- a/Confuser.Protections/AntiTamper/JITBody.cs +++ b/Confuser.Protections/AntiTamper/JITBody.cs @@ -47,6 +47,12 @@ public uint GetVirtualSize() { return GetFileLength(); } + // dnlib 4.x added IChunk.CalculateAlignment. Return 0 for default/no alignment, + // matching the implicit behaviour before the method existed in 3.x. + public uint CalculateAlignment() { + return 0; + } + public void WriteTo(DataWriter writer) { writer.WriteUInt32((uint)(Body.Length >> 2)); writer.WriteBytes(Body); @@ -224,6 +230,12 @@ public uint GetVirtualSize() { return GetFileLength(); } + // dnlib 4.x added IChunk.CalculateAlignment. Return 0 for default/no alignment, + // matching the implicit behaviour before the method existed in 3.x. + public uint CalculateAlignment() { + return 0; + } + public void WriteTo(DataWriter writer) { uint length = GetFileLength() - 4; // minus length field writer.WriteUInt32((uint)bodies.Count); diff --git a/Confuser.Protections/AntiTamper/JITMode.cs b/Confuser.Protections/AntiTamper/JITMode.cs index 23f41e90f..074c54b40 100644 --- a/Confuser.Protections/AntiTamper/JITMode.cs +++ b/Confuser.Protections/AntiTamper/JITMode.cs @@ -12,6 +12,7 @@ using dnlib.DotNet.Emit; using dnlib.DotNet.MD; using dnlib.DotNet.Writer; +using Microsoft.Extensions.Logging; namespace Confuser.Protections.AntiTamper { internal class JITMode : IModeHandler { @@ -154,11 +155,11 @@ public void HandleMD(AntiTamperProtection parent, ConfuserContext context, Prote void OnWriterEvent(object sender, ModuleWriterEventArgs e) { var writer = (ModuleWriterBase)sender; if (e.Event == ModuleWriterEvent.MDBeginWriteMethodBodies) { - context.Logger.Debug("Extracting method bodies..."); + context.Logger.LogDebug("Extracting method bodies..."); CreateSection(writer); } else if (e.Event == ModuleWriterEvent.BeginStrongNameSign) { - context.Logger.Debug("Encrypting method section..."); + context.Logger.LogDebug("Encrypting method section..."); EncryptSection(writer); } } @@ -210,7 +211,7 @@ void CreateSection(ModuleWriterBase writer) { newSection.Add(bodyIndex, 0x10); // save methods - foreach (MethodDef method in methods.WithProgress(context.Logger)) { + foreach (MethodDef method in methods.WithProgress(context.ProgressReporter)) { if (!method.HasBody) continue; diff --git a/Confuser.Protections/Compress/Compressor.cs b/Confuser.Protections/Compress/Compressor.cs index 28e8ec5cd..ca81339db 100644 --- a/Confuser.Protections/Compress/Compressor.cs +++ b/Confuser.Protections/Compress/Compressor.cs @@ -15,6 +15,7 @@ using dnlib.DotNet.MD; using dnlib.DotNet.Writer; using dnlib.PE; +using Microsoft.Extensions.Logging; using FileAttributes = dnlib.DotNet.FileAttributes; using SR = System.Reflection; @@ -50,7 +51,7 @@ protected override void PopulatePipeline(ProtectionPipeline pipeline) { protected override void Pack(ConfuserContext context, ProtectionParameters parameters) { var ctx = context.Annotations.Get(context, ContextKey); if (ctx == null) { - context.Logger.Error("No executable module!"); + context.Logger.LogError("No executable module!"); throw new ConfuserException(null); } @@ -164,7 +165,7 @@ void PackModules(ConfuserContext context, CompressorContext compCtx, ModuleDef s state = state * 0x5e3f1f + chr; byte[] encrypted = compCtx.Encrypt(comp, entry.Value, state, progress => { progress = (progress + moduleIndex) / modules.Count; - context.Logger.Progress((int)(progress * 10000), 10000); + context.ProgressReporter.Progress((int)(progress * 10000), 10000); }); context.CheckCancellation(); @@ -172,7 +173,7 @@ void PackModules(ConfuserContext context, CompressorContext compCtx, ModuleDef s stubModule.Resources.Add(resource); moduleIndex++; } - context.Logger.EndProgress(); + context.ProgressReporter.EndProgress(); } void InjectData(ConfuserContext context, ModuleDef stubModule, MethodDef method, byte[] data) { @@ -222,7 +223,7 @@ void InjectStub(ConfuserContext context, CompressorContext compCtx, ProtectionPa } compCtx.Deriver.Init(context, random); - context.Logger.Debug("Encrypting modules..."); + context.Logger.LogDebug("Encrypting modules..."); // Main MethodDef entryPoint = defs.OfType().Single(method => method.Name == "Main"); @@ -245,8 +246,8 @@ void InjectStub(ConfuserContext context, CompressorContext compCtx, ProtectionPa compCtx.OriginModule = context.OutputModules[compCtx.ModuleIndex]; byte[] encryptedModule = compCtx.Encrypt(comp, compCtx.OriginModule, seed, - progress => context.Logger.Progress((int)(progress * 10000), 10000)); - context.Logger.EndProgress(); + progress => context.ProgressReporter.Progress((int)(progress * 10000), 10000)); + context.ProgressReporter.EndProgress(); context.CheckCancellation(); compCtx.EncryptedModule = encryptedModule; diff --git a/Confuser.Protections/Compress/ExtractPhase.cs b/Confuser.Protections/Compress/ExtractPhase.cs index 24fd835b0..6fc5e39aa 100644 --- a/Confuser.Protections/Compress/ExtractPhase.cs +++ b/Confuser.Protections/Compress/ExtractPhase.cs @@ -7,6 +7,7 @@ using dnlib.DotNet; using dnlib.DotNet.MD; using dnlib.DotNet.Writer; +using Microsoft.Extensions.Logging; namespace Confuser.Protections.Compress { internal class ExtractPhase : ProtectionPhase { @@ -29,7 +30,7 @@ protected override void Execute(ConfuserContext context, ProtectionParameters pa if (context.Annotations.Get(context, Compressor.ContextKey) != null) { if (isExe) { - context.Logger.Error("Too many executable modules!"); + context.Logger.LogError("Too many executable modules!"); throw new ConfuserException(null); } return; diff --git a/Confuser.Protections/Constants/EncodePhase.cs b/Confuser.Protections/Constants/EncodePhase.cs index c108d14d4..b3db5bd68 100644 --- a/Confuser.Protections/Constants/EncodePhase.cs +++ b/Confuser.Protections/Constants/EncodePhase.cs @@ -38,12 +38,12 @@ protected override void Execute(ConfuserContext context, ProtectionParameters pa // Encode constants moduleCtx.ReferenceRepl = new Dictionary>>(); moduleCtx.EncodedBuffer = new List(); - foreach (var entry in ldInit.WithProgress(context.Logger)) // Ensure the array length haven't been encoded yet + foreach (var entry in ldInit.WithProgress(context.ProgressReporter)) // Ensure the array length haven't been encoded yet { EncodeInitializer(moduleCtx, entry.Key, entry.Value); context.CheckCancellation(); } - foreach (var entry in ldc.WithProgress(context.Logger)) { + foreach (var entry in ldc.WithProgress(context.ProgressReporter)) { if (entry.Key is string) { EncodeString(moduleCtx, (string)entry.Key, entry.Value); } @@ -234,7 +234,7 @@ void ExtractConstants( Dictionary>> ldInit) { var dataFields = new HashSet(); var fieldRefs = new HashSet(); - foreach (MethodDef method in parameters.Targets.OfType().WithProgress(context.Logger)) { + foreach (MethodDef method in parameters.Targets.OfType().WithProgress(context.ProgressReporter)) { if (!method.HasBody) continue; diff --git a/Confuser.Protections/ControlFlow/ControlFlowPhase.cs b/Confuser.Protections/ControlFlow/ControlFlowPhase.cs index 3f1b15e28..addeb6257 100644 --- a/Confuser.Protections/ControlFlow/ControlFlowPhase.cs +++ b/Confuser.Protections/ControlFlow/ControlFlowPhase.cs @@ -9,6 +9,7 @@ using dnlib.DotNet.MD; using dnlib.DotNet.Pdb; using dnlib.DotNet.Writer; +using Microsoft.Extensions.Logging; namespace Confuser.Protections.ControlFlow { internal class ControlFlowPhase : ProtectionPhase { @@ -74,7 +75,7 @@ protected override void Execute(ConfuserContext context, ProtectionParameters pa bool disabledOpti = DisabledOptimization(context.CurrentModule); RandomGenerator random = context.Registry.GetService().GetRandomGenerator(ControlFlowProtection._FullId); - foreach (MethodDef method in parameters.Targets.OfType().WithProgress(context.Logger)) + foreach (MethodDef method in parameters.Targets.OfType().WithProgress(context.ProgressReporter)) if (method.HasBody && method.Body.Instructions.Count > 0) { ProcessMethod(method.Body, ParseParameters(method, context, parameters, random, disabledOpti)); context.CheckCancellation(); @@ -90,7 +91,7 @@ static ManglerBase GetMangler(CFType type) { void ProcessMethod(CilBody body, CFContext ctx) { uint maxStack; if (!MaxStackCalculator.GetMaxStack(body.Instructions, body.ExceptionHandlers, out maxStack)) { - ctx.Context.Logger.Error("Failed to calcuate maxstack."); + ctx.Context.Logger.LogError("Failed to calcuate maxstack."); throw new ConfuserException(null); } body.MaxStack = (ushort)maxStack; diff --git a/Confuser.Protections/HardeningPhase.cs b/Confuser.Protections/HardeningPhase.cs index 17dabdfe4..0e5defa8c 100644 --- a/Confuser.Protections/HardeningPhase.cs +++ b/Confuser.Protections/HardeningPhase.cs @@ -4,6 +4,7 @@ using Confuser.Core.Services; using dnlib.DotNet; using dnlib.DotNet.Emit; +using Microsoft.Extensions.Logging; namespace Confuser.Protections { internal sealed class HardeningPhase : ProtectionPhase { @@ -31,7 +32,7 @@ protected override void Execute(ConfuserContext context, ProtectionParameters pa private static void HardenMethod(ConfuserContext context, ModuleDef module) { var cctor = module.GlobalType.FindStaticConstructor(); if (cctor == null) { - context.Logger.Debug("No .cctor containing protection code found. Nothing to do."); + context.Logger.LogDebug("No .cctor containing protection code found. Nothing to do."); return; } diff --git a/Confuser.Protections/ReferenceProxy/ReferenceProxyPhase.cs b/Confuser.Protections/ReferenceProxy/ReferenceProxyPhase.cs index 3e299f2fa..e08baf3d4 100644 --- a/Confuser.Protections/ReferenceProxy/ReferenceProxyPhase.cs +++ b/Confuser.Protections/ReferenceProxy/ReferenceProxyPhase.cs @@ -103,7 +103,7 @@ protected override void Execute(ConfuserContext context, ProtectionParameters pa var store = new RPStore { random = random }; - foreach (MethodDef method in parameters.Targets.OfType().WithProgress(context.Logger)) + foreach (MethodDef method in parameters.Targets.OfType().WithProgress(context.ProgressReporter)) if (method.HasBody && method.Body.Instructions.Count > 0) { ProcessMethod(ParseParameters(method, context, parameters, store)); context.CheckCancellation(); diff --git a/Confuser.Protections/Resources/InjectPhase.cs b/Confuser.Protections/Resources/InjectPhase.cs index 3d086568b..de803fd58 100644 --- a/Confuser.Protections/Resources/InjectPhase.cs +++ b/Confuser.Protections/Resources/InjectPhase.cs @@ -10,6 +10,7 @@ using Confuser.Renamer; using dnlib.DotNet; using dnlib.DotNet.Emit; +using Microsoft.Extensions.Logging; namespace Confuser.Protections.Resources { internal class InjectPhase : ProtectionPhase { @@ -27,7 +28,7 @@ public override string Name { protected override void Execute(ConfuserContext context, ProtectionParameters parameters) { if (parameters.Targets.Any()) { if (!UTF8String.IsNullOrEmpty(context.CurrentModule.Assembly.Culture)) { - context.Logger.DebugFormat("Skipping resource encryption for satellite assembly '{0}'.", + context.Logger.LogDebug("Skipping resource encryption for satellite assembly '{0}'.", context.CurrentModule.Assembly.FullName); return; } diff --git a/Confuser.Protections/Resources/MDPhase.cs b/Confuser.Protections/Resources/MDPhase.cs index 0f676cd26..741ebbf32 100644 --- a/Confuser.Protections/Resources/MDPhase.cs +++ b/Confuser.Protections/Resources/MDPhase.cs @@ -10,6 +10,7 @@ using dnlib.DotNet; using dnlib.DotNet.MD; using dnlib.DotNet.Writer; +using Microsoft.Extensions.Logging; namespace Confuser.Protections.Resources { internal class MDPhase { @@ -28,7 +29,7 @@ void OnWriterEvent(object sender, ModuleWriterEventArgs e) { var writer = (ModuleWriterBase)sender; if (e.Event == ModuleWriterEvent.MDBeginAddResources) { ctx.Context.CheckCancellation(); - ctx.Context.Logger.Debug("Encrypting resources..."); + ctx.Context.Logger.LogDebug("Encrypting resources..."); bool hasPacker = ctx.Context.Packer != null; List resources = ctx.Module.Resources.OfType().ToList(); @@ -68,8 +69,8 @@ void OnWriterEvent(object sender, ModuleWriterEventArgs e) { // compress moduleBuff = ctx.Context.Registry.GetService().Compress( moduleBuff, - progress => ctx.Context.Logger.Progress((int)(progress * 10000), 10000)); - ctx.Context.Logger.EndProgress(); + progress => ctx.Context.ProgressReporter.Progress((int)(progress * 10000), 10000)); + ctx.Context.ProgressReporter.EndProgress(); ctx.Context.CheckCancellation(); uint compressedLen = (uint)(moduleBuff.Length + 3) / 4; diff --git a/Confuser.Protections/TypeScrambler/AnalyzePhase.cs b/Confuser.Protections/TypeScrambler/AnalyzePhase.cs index fbec05899..5c8304208 100644 --- a/Confuser.Protections/TypeScrambler/AnalyzePhase.cs +++ b/Confuser.Protections/TypeScrambler/AnalyzePhase.cs @@ -19,7 +19,7 @@ protected override void Execute(ConfuserContext context, ProtectionParameters pa var typeService = context.Registry.GetService(); Debug.Assert(typeService != null, $"{nameof(typeService)} != null"); - foreach (var target in parameters.Targets.WithProgress(context.Logger)) { + foreach (var target in parameters.Targets.WithProgress(context.ProgressReporter)) { switch (target) { case TypeDef typeDef: typeService.AddScannedItem(new ScannedType(typeDef)); diff --git a/Confuser.Protections/TypeScrambler/ScramblePhase.cs b/Confuser.Protections/TypeScrambler/ScramblePhase.cs index 6fc86bfd0..1af58491f 100644 --- a/Confuser.Protections/TypeScrambler/ScramblePhase.cs +++ b/Confuser.Protections/TypeScrambler/ScramblePhase.cs @@ -27,7 +27,7 @@ protected override void Execute(ConfuserContext context, ProtectionParameters pa // In this stage the references to the scrambled types need to be fixed. This needs to be done for all // methods in the assembly, because all methods may contain references to the scrambled types and methods. - foreach (var def in context.CurrentModule.FindDefinitions().WithProgress(context.Logger)) { + foreach (var def in context.CurrentModule.FindDefinitions().WithProgress(context.ProgressReporter)) { switch (def) { case MethodDef md: if (md.HasReturnType) diff --git a/Confuser.Renamer/AnalyzePhase.cs b/Confuser.Renamer/AnalyzePhase.cs index 4d95e27d1..94e9f7753 100644 --- a/Confuser.Renamer/AnalyzePhase.cs +++ b/Confuser.Renamer/AnalyzePhase.cs @@ -6,6 +6,7 @@ using Confuser.Core.Services; using Confuser.Renamer.Analyzers; using dnlib.DotNet; +using Microsoft.Extensions.Logging; namespace Confuser.Renamer { internal class AnalyzePhase : ProtectionPhase { @@ -34,12 +35,12 @@ void ParseParameters(IDnlibDef def, ConfuserContext context, NameService service protected override void Execute(ConfuserContext context, ProtectionParameters parameters) { var service = (NameService)context.Registry.GetService(); - context.Logger.Debug("Building VTables & identifier list..."); + context.Logger.LogDebug("Building VTables & identifier list..."); foreach (ModuleDef moduleDef in parameters.Targets.OfType()) moduleDef.EnableTypeDefFindCache = true; - foreach (IDnlibDef def in parameters.Targets.WithProgress(context.Logger)) { + foreach (IDnlibDef def in parameters.Targets.WithProgress(context.ProgressReporter)) { ParseParameters(def, context, service, parameters); if (def is ModuleDef module) { @@ -56,10 +57,10 @@ protected override void Execute(ConfuserContext context, ProtectionParameters pa context.CheckCancellation(); } - context.Logger.Debug("Analyzing..."); + context.Logger.LogDebug("Analyzing..."); RegisterRenamers(context, service); IList renamers = service.Renamers; - foreach (IDnlibDef def in parameters.Targets.WithProgress(context.Logger)) { + foreach (IDnlibDef def in parameters.Targets.WithProgress(context.ProgressReporter)) { Analyze(service, context, parameters, def, true); context.CheckCancellation(); } @@ -106,35 +107,35 @@ void RegisterRenamers(ConfuserContext context, NameService service) { if (wpf) { var wpfAnalyzer = new WPFAnalyzer(); - context.Logger.Debug("WPF found, enabling compatibility."); + context.Logger.LogDebug("WPF found, enabling compatibility."); service.Renamers.Add(wpfAnalyzer); if (caliburn) { - context.Logger.Debug("Caliburn.Micro found, enabling compatibility."); + context.Logger.LogDebug("Caliburn.Micro found, enabling compatibility."); service.Renamers.Add(new CaliburnAnalyzer(wpfAnalyzer)); } } if (winforms) { var winformsAnalyzer = new WinFormsAnalyzer(); - context.Logger.Debug("WinForms found, enabling compatibility."); + context.Logger.LogDebug("WinForms found, enabling compatibility."); service.Renamers.Add(winformsAnalyzer); } if (json) { var jsonAnalyzer = new JsonAnalyzer(); - context.Logger.Debug("Newtonsoft.Json found, enabling compatibility."); + context.Logger.LogDebug("Newtonsoft.Json found, enabling compatibility."); service.Renamers.Add(jsonAnalyzer); } if (visualBasic) { var vbAnalyzer = new VisualBasicRuntimeAnalyzer(); - context.Logger.Debug("Visual Basic Embedded Runtime found, enabling compatibility."); + context.Logger.LogDebug("Visual Basic Embedded Runtime found, enabling compatibility."); service.Renamers.Add(vbAnalyzer); } if (vsComposition) { var analyzer = new VsCompositionAnalyzer(); - context.Logger.Debug("Visual Studio Composition found, enabling compatibility."); + context.Logger.LogDebug("Visual Studio Composition found, enabling compatibility."); service.Renamers.Add(analyzer); } } diff --git a/Confuser.Renamer/Analyzers/CallSiteAnalyzer.cs b/Confuser.Renamer/Analyzers/CallSiteAnalyzer.cs index b4cf6f007..0a7cca3fa 100644 --- a/Confuser.Renamer/Analyzers/CallSiteAnalyzer.cs +++ b/Confuser.Renamer/Analyzers/CallSiteAnalyzer.cs @@ -4,6 +4,7 @@ using Confuser.Renamer.References; using dnlib.DotNet; using dnlib.DotNet.Emit; +using Microsoft.Extensions.Logging; namespace Confuser.Renamer.Analyzers { internal sealed class CallSiteAnalyzer : IRenamer { @@ -61,7 +62,7 @@ private static void HandleBinderInvokeMember(ConfuserContext context, MethodDef BuildMemberReferences(context, typeDefOrRef, boundMemberName, nameInstruction); } else { - context.Logger.WarnFormat( + context.Logger.LogWarning( "Failed to resolve type for dynamic invoke member in {0} - blocking all members with name {1} from renaming.", method, boundMemberName); diff --git a/Confuser.Renamer/Analyzers/ManifestResourceAnalyzer.cs b/Confuser.Renamer/Analyzers/ManifestResourceAnalyzer.cs index 6a911d0b4..3d23808b7 100644 --- a/Confuser.Renamer/Analyzers/ManifestResourceAnalyzer.cs +++ b/Confuser.Renamer/Analyzers/ManifestResourceAnalyzer.cs @@ -28,8 +28,11 @@ public static void PreRename(ModuleDef currentModule, ITraceService trace, Metho !UTF8String.Equals(targetMethodDefOrRef.Name, "GetManifestResourceStream") || !UTF8String.Equals(targetMethodDefOrRef.DeclaringType.FullName, "System.Reflection.Assembly")) continue; - var targetMethodDef = targetMethodDefOrRef.ResolveMethodDefThrow(); - if (targetMethodDef.Parameters.Count != 3) continue; + // The two-argument overload GetManifestResourceStream(Type, String) has two + // signature parameters (the instance 'this' is separate). Check the signature + // directly so we don't depend on resolving the BCL method. + var targetSig = targetMethodDefOrRef.MethodSig; + if (targetSig == null || targetSig.Params.Count != 2) continue; var argumentIdx = methodTrace.Value.TraceArguments(instruction); if (argumentIdx.Length != 3) continue; @@ -53,11 +56,23 @@ public static void PreRename(ModuleDef currentModule, ITraceService trace, Metho var resourceName = refTypeDefOrRef.Namespace + '.' + resName; - var getManifestMethodDef = targetMethodDefOrRef.ResolveMethodDefThrow(); - var assemblyTypeDef = getManifestMethodDef.DeclaringType; - var expectedSig = MethodSig.CreateInstance(getManifestMethodDef.MethodSig.RetType, getManifestMethodDef.MethodSig.Params.Last()); - var newMethodDef = assemblyTypeDef.FindMethod("GetManifestResourceStream", expectedSig); - var newMethodRef = currentModule.Import(newMethodDef); + // Build the reference to the single-argument overload: + // Stream System.Reflection.Assembly::GetManifestResourceStream(String). + // Prefer resolving the real overload (matches production exactly); if the BCL + // declaring type can't be resolved — e.g. in a minimal analysis context where the + // runtime assemblies aren't on the resolver's search path — construct the member + // reference directly from the original call's signature. + var expectedSig = MethodSig.CreateInstance(targetSig.RetType, targetSig.Params[1]); + var assemblyTypeDef = targetMethodDefOrRef.ResolveMethodDef()?.DeclaringType; + IMethod newMethodRef; + if (assemblyTypeDef != null) { + var newMethodDef = assemblyTypeDef.FindMethod("GetManifestResourceStream", expectedSig); + newMethodRef = currentModule.Import(newMethodDef); + } + else { + newMethodRef = currentModule.Import( + new MemberRefUser(currentModule, "GetManifestResourceStream", expectedSig, targetMethodDefOrRef.DeclaringType)); + } resNameInstruction.Operand = resourceName; instruction.Operand = newMethodRef; diff --git a/Confuser.Renamer/Analyzers/ReflectionAnalyzer.cs b/Confuser.Renamer/Analyzers/ReflectionAnalyzer.cs index 5deeeaa4a..f6bbc267c 100644 --- a/Confuser.Renamer/Analyzers/ReflectionAnalyzer.cs +++ b/Confuser.Renamer/Analyzers/ReflectionAnalyzer.cs @@ -7,7 +7,7 @@ using Confuser.Renamer.References; using dnlib.DotNet; using dnlib.DotNet.Emit; -using ILogger = Confuser.Core.ILogger; +using Microsoft.Extensions.Logging; namespace Confuser.Renamer.Analyzers { /// @@ -20,7 +20,7 @@ void IRenamer.Analyze(ConfuserContext context, INameService service, ProtectionP Analyze(service, context.Registry.GetService(), context.Modules.Cast().ToArray(), context.Logger, method); } - public void Analyze(INameService nameService, ITraceService traceService, IReadOnlyList moduleDefs, ILogger logger, MethodDef method) { + public void Analyze(INameService nameService, ITraceService traceService, IReadOnlyList moduleDefs, Microsoft.Extensions.Logging.ILogger logger, MethodDef method) { if (!method.HasBody) return; MethodTrace methodTrace = null; @@ -49,7 +49,7 @@ MethodTrace GetMethodTrace() { var trace = GetMethodTrace(); var arguments = trace.TraceArguments(instr); if (arguments == null) { - logger.WarnFormat(Resources.ReflectionAnalyzer_Analyze_TracingArgumentsFailed, calledMethod.FullName, method.FullName); + logger.LogWarning(Resources.ReflectionAnalyzer_Analyze_TracingArgumentsFailed, calledMethod.FullName, method.FullName); } else if (arguments.Length >= 2) { var types = GetReferencedTypes(method.Body.Instructions[arguments[0]], method, trace); diff --git a/Confuser.Renamer/Analyzers/ResourceAnalyzer.cs b/Confuser.Renamer/Analyzers/ResourceAnalyzer.cs index 9f58d66ce..fccfc9d40 100644 --- a/Confuser.Renamer/Analyzers/ResourceAnalyzer.cs +++ b/Confuser.Renamer/Analyzers/ResourceAnalyzer.cs @@ -6,6 +6,7 @@ using Confuser.Renamer.References; using dnlib.DotNet; using dnlib.DotNet.Emit; +using Microsoft.Extensions.Logging; namespace Confuser.Renamer.Analyzers { internal class ResourceAnalyzer : IRenamer { @@ -23,7 +24,7 @@ public void Analyze(ConfuserContext context, INameService service, ProtectionPar string nameAsmName = asmName.Substring(0, asmName.Length - ".resources".Length); ModuleDef mainModule = context.Modules.SingleOrDefault(mod => mod.Assembly.Name == nameAsmName); if (mainModule == null) { - context.Logger.ErrorFormat("Could not find main assembly of satellite assembly '{0}'.", module.Assembly.FullName); + context.Logger.LogError("Could not find main assembly of satellite assembly '{0}'.", module.Assembly.FullName); throw new ConfuserException(null); } @@ -35,7 +36,7 @@ public void Analyze(ConfuserContext context, INameService service, ProtectionPar TypeDef type = mainModule.FindReflection(typeName); if (type == null) { - context.Logger.WarnFormat(Resources.ResourceAnalyzer_Analyze_CouldNotFindResourceType, typeName); + context.Logger.LogWarning(Resources.ResourceAnalyzer_Analyze_CouldNotFindResourceType, typeName); continue; } string format = $"{{0}}.{culture}.resources"; @@ -68,7 +69,7 @@ public void Analyze(ConfuserContext context, INameService service, ProtectionPar } if (type == null) { - context.Logger.WarnFormat(Resources.ResourceAnalyzer_Analyze_CouldNotFindResourceType, typeName); + context.Logger.LogWarning(Resources.ResourceAnalyzer_Analyze_CouldNotFindResourceType, typeName); continue; } service.ReduceRenameMode(type, RenameMode.Reflection); diff --git a/Confuser.Renamer/Analyzers/TypeBlobAnalyzer.cs b/Confuser.Renamer/Analyzers/TypeBlobAnalyzer.cs index 15a3e94f1..1d2f69496 100644 --- a/Confuser.Renamer/Analyzers/TypeBlobAnalyzer.cs +++ b/Confuser.Renamer/Analyzers/TypeBlobAnalyzer.cs @@ -7,6 +7,7 @@ using dnlib.DotNet; using dnlib.DotNet.Emit; using dnlib.DotNet.MD; +using Microsoft.Extensions.Logging; namespace Confuser.Renamer.Analyzers { public sealed class TypeBlobAnalyzer : IRenamer { @@ -16,7 +17,7 @@ void IRenamer.Analyze(ConfuserContext context, INameService service, ProtectionP Analyze(service, context.Modules, context.Logger, moduleDef); } - public static void Analyze(INameService service, ICollection modules, Core.ILogger logger, ModuleDefMD module) { + public static void Analyze(INameService service, ICollection modules, Microsoft.Extensions.Logging.ILogger logger, ModuleDefMD module) { // MemberRef var table = module.TablesStream.Get(Table.Method); var len = table.Rows; @@ -68,14 +69,18 @@ public static void Analyze(INameService service, ICollection module foreach (CANamedArgument arg in attr.Properties) AnalyzeCAArgument(modules, service, arg.Argument); - TypeDef attrType = attr.AttributeType.ResolveTypeDefThrow(); - if (!modules.Contains((ModuleDefMD)attrType.Module)) + // External attribute types (e.g. compiler-emitted BCL attributes like + // RefSafetyRulesAttribute) may not be resolvable, and are never renamed anyway + // since they aren't defined in the modules being obfuscated. Skip rather than + // throw so obfuscation doesn't fail on an unresolvable external attribute type. + TypeDef attrType = attr.AttributeType.ResolveTypeDef(); + if (attrType == null || !modules.Contains((ModuleDefMD)attrType.Module)) continue; foreach (var arg in attr.NamedArguments) { var memberDef = FindArgumentMemberDef(arg, attrType); if (memberDef == null) - logger.WarnFormat( + logger.LogWarning( arg.IsField ? "Failed to resolve CA field '{0}::{1} : {2}'." : "Failed to resolve CA property '{0}::{1} : {2}'.", attrType, arg.Name, arg.Type); else @@ -130,8 +135,9 @@ private static void AnalyzeCAArgument(ICollection modules, INameSer if (arg.Type.DefinitionAssembly.IsCorLib() && arg.Type.FullName == "System.Type") { var typeSig = (TypeSig)arg.Value; foreach (ITypeDefOrRef typeRef in typeSig.FindTypeRefs()) { - TypeDef typeDef = typeRef.ResolveTypeDefThrow(); - if (modules.Contains((ModuleDefMD)typeDef.Module)) { + // Skip external/unresolvable types — only types in the obfuscated modules are renamed. + TypeDef typeDef = typeRef.ResolveTypeDef(); + if (typeDef != null && modules.Contains((ModuleDefMD)typeDef.Module)) { if (typeRef is TypeRef) service.AddReference(typeDef, new TypeRefReference((TypeRef)typeRef, typeDef)); service.ReduceRenameMode(typeDef, RenameMode.Reflection); @@ -159,8 +165,9 @@ private static void AnalyzeMemberRef(ICollection modules, INameServ if (sig is GenericInstSig) { var inst = (GenericInstSig)sig; Debug.Assert(!(inst.GenericType.TypeDefOrRef is TypeSpec)); - TypeDef openType = inst.GenericType.TypeDefOrRef.ResolveTypeDefThrow(); - if (!modules.Contains((ModuleDefMD)openType.Module) || + // Skip external/unresolvable generic types — only in-module members are tracked. + TypeDef openType = inst.GenericType.TypeDefOrRef.ResolveTypeDef(); + if (openType == null || !modules.Contains((ModuleDefMD)openType.Module) || memberRef.IsArrayAccessors()) return; diff --git a/Confuser.Renamer/Analyzers/WPFAnalyzer.cs b/Confuser.Renamer/Analyzers/WPFAnalyzer.cs index ebf49dceb..51840bfd9 100644 --- a/Confuser.Renamer/Analyzers/WPFAnalyzer.cs +++ b/Confuser.Renamer/Analyzers/WPFAnalyzer.cs @@ -14,6 +14,7 @@ using dnlib.DotNet; using dnlib.DotNet.Emit; using dnlib.IO; +using Microsoft.Extensions.Logging; namespace Confuser.Renamer.Analyzers { internal class WPFAnalyzer : IRenamer { @@ -47,7 +48,7 @@ public void PreRename(ConfuserContext context, INameService service, ProtectionP var renameMode = parameters.GetParameter(context, def, "renXamlMode", RenameMode.Letters); if (renameMode < RenameMode.Letters) { var illegalValues = Enum.GetValues(typeof(RenameMode)).Cast().Where(m => m < RenameMode.Letters); - context.Logger.Warn("The renaming modes " + String.Join(", ", illegalValues) + " are not allowed for XAML resources. Letters mode will be used."); + context.Logger.LogWarning("The renaming modes " + String.Join(", ", illegalValues) + " are not allowed for XAML resources. Letters mode will be used."); renameMode = RenameMode.Letters; } @@ -82,7 +83,7 @@ public void PreRename(ConfuserContext context, INameService service, ProtectionP string decodedNewName = decodedDirectory + fileName; string encodedNewName = encodedDirectory + fileName; - context.Logger.Debug(String.Format("Preserving virtual paths. Replaced {0} with {1}", decodedName, decodedNewName)); + context.Logger.LogDebug(String.Format("Preserving virtual paths. Replaced {0} with {1}", decodedName, decodedNewName)); bool renameOk = references.All(r => r.CanRename(module, decodedName, decodedNewName) || r.CanRename(module, encodedName, encodedNewName)); @@ -199,7 +200,7 @@ void AnalyzeMethod(ConfuserContext context, INameService service, MethodDef meth operand = match.Groups[2].Value; } else if (operand.Contains("/")) - context.Logger.WarnFormat("Fail to extract XAML name from '{0}'.", instr.Operand); + context.Logger.LogWarning("Fail to extract XAML name from '{0}'.", instr.Operand); var reference = new BAMLStringReference(refModule, instr); operand = WebUtility.UrlDecode(operand.TrimStart('/')); @@ -222,14 +223,14 @@ void AnalyzeMethod(ConfuserContext context, INameService service, MethodDef meth int[] args = trace.TraceArguments(instrInfo.Item2); if (args == null) { if (!erred) - context.Logger.WarnFormat("Failed to extract dependency property name in '{0}'.", method.FullName); + context.Logger.LogWarning("Failed to extract dependency property name in '{0}'.", method.FullName); erred = true; continue; } Instruction ldstr = method.Body.Instructions[args[0]]; if (ldstr.OpCode.Code != Code.Ldstr) { if (!erred) - context.Logger.WarnFormat("Failed to extract dependency property name in '{0}'.", method.FullName); + context.Logger.LogWarning("Failed to extract dependency property name in '{0}'.", method.FullName); erred = true; continue; } @@ -270,10 +271,10 @@ void AnalyzeMethod(ConfuserContext context, INameService service, MethodDef meth } if (!found) { if (instrInfo.Item1) - context.Logger.WarnFormat("Failed to find the accessors of attached dependency property '{0}' in type '{1}'.", + context.Logger.LogWarning("Failed to find the accessors of attached dependency property '{0}' in type '{1}'.", name, declType.FullName); else - context.Logger.WarnFormat("Failed to find the CLR property of normal dependency property '{0}' in type '{1}'.", + context.Logger.LogWarning("Failed to find the CLR property of normal dependency property '{0}' in type '{1}'.", name, declType.FullName); } } @@ -283,14 +284,14 @@ void AnalyzeMethod(ConfuserContext context, INameService service, MethodDef meth int[] args = trace.TraceArguments(instr); if (args == null) { if (!erred) - context.Logger.WarnFormat("Failed to extract routed event name in '{0}'.", method.FullName); + context.Logger.LogWarning("Failed to extract routed event name in '{0}'.", method.FullName); erred = true; continue; } Instruction ldstr = method.Body.Instructions[args[0]]; if (ldstr.OpCode.Code != Code.Ldstr) { if (!erred) - context.Logger.WarnFormat("Failed to extract routed event name in '{0}'.", method.FullName); + context.Logger.LogWarning("Failed to extract routed event name in '{0}'.", method.FullName); erred = true; continue; } @@ -300,7 +301,7 @@ void AnalyzeMethod(ConfuserContext context, INameService service, MethodDef meth EventDef eventDef = null; if ((eventDef = declType.FindEvent(name)) == null) { - context.Logger.WarnFormat("Failed to find the CLR event of routed event '{0}' in type '{1}'.", + context.Logger.LogWarning("Failed to find the CLR event of routed event '{0}' in type '{1}'.", name, declType.FullName); continue; } diff --git a/Confuser.Renamer/Analyzers/WinFormsAnalyzer.cs b/Confuser.Renamer/Analyzers/WinFormsAnalyzer.cs index c8f78e92c..9a33714c7 100644 --- a/Confuser.Renamer/Analyzers/WinFormsAnalyzer.cs +++ b/Confuser.Renamer/Analyzers/WinFormsAnalyzer.cs @@ -6,6 +6,7 @@ using Confuser.Core.Services; using dnlib.DotNet; using dnlib.DotNet.Emit; +using Microsoft.Extensions.Logging; namespace Confuser.Renamer.Analyzers { public class WinFormsAnalyzer : IRenamer { @@ -66,7 +67,7 @@ void AnalyzeMethod(ConfuserContext context, INameService service, MethodDef meth int[] args = trace.TraceArguments(instrInfo.Item2); if (args == null) { if (!erred) - context.Logger.WarnFormat("Failed to extract binding property name in '{0}'.", method.FullName); + context.Logger.LogWarning("Failed to extract binding property name in '{0}'.", method.FullName); erred = true; continue; } @@ -75,14 +76,14 @@ void AnalyzeMethod(ConfuserContext context, INameService service, MethodDef meth var propertyName = ResolveNameInstruction(method, args, ref argumentIndex); if (propertyName.OpCode.Code != Code.Ldstr) { if (!erred) - context.Logger.WarnFormat("Failed to extract binding property name in '{0}'.", method.FullName); + context.Logger.LogWarning("Failed to extract binding property name in '{0}'.", method.FullName); erred = true; } else { List props; if (!properties.TryGetValue((string)propertyName.Operand, out props)) { if (!erred) - context.Logger.WarnFormat("Failed to extract target property in '{0}'.", method.FullName); + context.Logger.LogWarning("Failed to extract target property in '{0}'.", method.FullName); erred = true; } else { @@ -95,14 +96,14 @@ void AnalyzeMethod(ConfuserContext context, INameService service, MethodDef meth var dataMember = ResolveNameInstruction(method, args, ref argumentIndex); if (dataMember.OpCode.Code != Code.Ldstr) { if (!erred) - context.Logger.WarnFormat("Failed to extract binding property name in '{0}'.", method.FullName); + context.Logger.LogWarning("Failed to extract binding property name in '{0}'.", method.FullName); erred = true; } else { List props; if (!properties.TryGetValue((string)dataMember.Operand, out props)) { if (!erred) - context.Logger.WarnFormat("Failed to extract target property in '{0}'.", method.FullName); + context.Logger.LogWarning("Failed to extract target property in '{0}'.", method.FullName); erred = true; } else { @@ -116,7 +117,7 @@ void AnalyzeMethod(ConfuserContext context, INameService service, MethodDef meth int[] args = trace.TraceArguments(instrInfo); if (args == null) { if (!erred) - context.Logger.WarnFormat("Failed to extract binding property name in '{0}'.", method.FullName); + context.Logger.LogWarning("Failed to extract binding property name in '{0}'.", method.FullName); erred = true; continue; } @@ -125,13 +126,13 @@ void AnalyzeMethod(ConfuserContext context, INameService service, MethodDef meth var propertyName = ResolveNameInstruction(method, args, ref argumentIndex); if (propertyName.OpCode.Code != Code.Ldstr) { if (!erred) - context.Logger.WarnFormat("Failed to extract binding property name in '{0}'.", method.FullName); + context.Logger.LogWarning("Failed to extract binding property name in '{0}'.", method.FullName); erred = true; } else { if (!properties.TryGetValue((string)propertyName.Operand, out var props)) { if (!erred) - context.Logger.WarnFormat("Failed to extract target property in '{0}'.", method.FullName); + context.Logger.LogWarning("Failed to extract target property in '{0}'.", method.FullName); erred = true; } else { diff --git a/Confuser.Renamer/BAML/BAMLAnalyzer.cs b/Confuser.Renamer/BAML/BAMLAnalyzer.cs index dec64a2e7..f4569a595 100644 --- a/Confuser.Renamer/BAML/BAMLAnalyzer.cs +++ b/Confuser.Renamer/BAML/BAMLAnalyzer.cs @@ -9,6 +9,7 @@ using Confuser.Renamer.Analyzers; using Confuser.Renamer.References; using dnlib.DotNet; +using Microsoft.Extensions.Logging; namespace Confuser.Renamer.BAML { internal class BAMLAnalyzer { @@ -338,7 +339,7 @@ void ProcessElementBody(BamlElement root, BamlElement elem) { if (attrInfo.Item1 is EventDef) { MethodDef method = root.Type.FindMethod(propRec.Value); if (method == null) - context.Logger.WarnFormat("Cannot resolve method '{0}' in '{1}'.", root.Type.FullName, propRec.Value); + context.Logger.LogWarning("Cannot resolve method '{0}' in '{1}'.", root.Type.FullName, propRec.Value); else { var reference = new BAMLAttributeReference(method, propRec); service.AddReference(method, reference); @@ -472,7 +473,7 @@ void ProcessConverter(PropertyWithConverterRecord rec, TypeDef type) { AddDefReference(field, reference); } if (property == null && field == null) - context.Logger.WarnFormat("Could not resolve command '{0}' in '{1}'.", cmd, CurrentBAMLName); + context.Logger.LogWarning("Could not resolve command '{0}' in '{1}'.", cmd, CurrentBAMLName); } } } @@ -540,7 +541,7 @@ void ProcessConverter(PropertyWithConverterRecord rec, TypeDef type) { src = match.Groups[2].Value; } else if (rec.Value.Contains("/")) - context.Logger.WarnFormat("Fail to extract XAML name from '{0}'.", rec.Value); + context.Logger.LogWarning("Fail to extract XAML name from '{0}'.", rec.Value); if (!src.StartsWith(packScheme, StringComparison.OrdinalIgnoreCase)) { var rel = new Uri(new Uri(packScheme + "application:,,,/" + CurrentBAMLName), src); diff --git a/Confuser.Renamer/RenamePhase.cs b/Confuser.Renamer/RenamePhase.cs index a73630deb..2302ddd74 100644 --- a/Confuser.Renamer/RenamePhase.cs +++ b/Confuser.Renamer/RenamePhase.cs @@ -5,6 +5,7 @@ using Confuser.Core; using dnlib.DotNet; using dnlib.DotNet.Pdb; +using Microsoft.Extensions.Logging; namespace Confuser.Renamer { class RenamePhase : ProtectionPhase { @@ -22,7 +23,7 @@ protected override void Execute(ConfuserContext context, ProtectionParameters pa var service = (NameService)context.Registry.GetService(); bool overloadConfusion = parameters.GetParameter(context, context.CurrentModule, "overload", false); - context.Logger.Debug("Renaming..."); + context.Logger.LogDebug("Renaming..."); foreach (var renamer in service.Renamers) { foreach (var def in parameters.Targets) renamer.PreRename(context, service, parameters, def); @@ -32,7 +33,7 @@ protected override void Execute(ConfuserContext context, ProtectionParameters pa var targets = parameters.Targets.ToList(); service.GetRandom().Shuffle(targets); var pdbDocs = new HashSet(); - foreach (var def in GetTargetsWithDelay(targets, context, service).WithProgress(targets.Count, context.Logger)) { + foreach (var def in GetTargetsWithDelay(targets, context, service).WithProgress(targets.Count, context.ProgressReporter)) { if (def is ModuleDef moduleDef && parameters.GetParameter(context, moduleDef, "rickroll", false)) RickRoller.CommenceRickroll(context, moduleDef); @@ -115,7 +116,7 @@ protected override void Execute(ConfuserContext context, ProtectionParameters pa foreach (var reference in updatedReferenceList) { errorBuilder.Append(" - ").AppendLine(reference.ToString(service)); } - context.Logger.Error(errorBuilder.ToString().Trim()); + context.Logger.LogError(errorBuilder.ToString().Trim()); throw new ConfuserException(); } context.CheckCancellation(); @@ -189,7 +190,7 @@ static IEnumerable GetTargetsWithDelay(IList definitions, foreach (var def in delayedItems) { errorBuilder.Append("• ").AppendDescription(def, service).AppendLine(); } - context.Logger.Warn(errorBuilder.ToString().Trim()); + context.Logger.LogWarning(errorBuilder.ToString().Trim()); yield break; } lastCount = delayedItems.Count; diff --git a/Confuser.Renamer/VTable.cs b/Confuser.Renamer/VTable.cs index cca4effa4..7a62802c7 100644 --- a/Confuser.Renamer/VTable.cs +++ b/Confuser.Renamer/VTable.cs @@ -4,7 +4,7 @@ using System.Linq; using Confuser.Core; using dnlib.DotNet; -using ILogger = Confuser.Core.ILogger; +using Microsoft.Extensions.Logging; namespace Confuser.Renamer { public class VTableSignature { @@ -314,31 +314,31 @@ static void Inherits(VTableConstruction vTbl, VTable baseVTbl) { [Conditional("DEBUG")] static void CheckKeyExist(VTableStorage storage, IDictionary dictionary, TKey key, string name) { if (!dictionary.ContainsKey(key)) { - storage.GetLogger().ErrorFormat("{0} not found: {1}", name, key); + storage.GetLogger().LogError("{0} not found: {1}", name, key); foreach (var k in dictionary.Values) - storage.GetLogger().ErrorFormat(" {0}", k); + storage.GetLogger().LogError(" {0}", k); } } [Conditional("DEBUG")] static void CheckKeyExist(VTableStorage storage, ILookup lookup, TKey key, string name) { if (!lookup.Contains(key)) { - storage.GetLogger().ErrorFormat("{0} not found: {1}", name, key); + storage.GetLogger().LogError("{0} not found: {1}", name, key); foreach (var k in lookup.Select(g => g.Key)) - storage.GetLogger().ErrorFormat(" {0}", k); + storage.GetLogger().LogError(" {0}", k); } } } public class VTableStorage { Dictionary storage = new Dictionary(); - ILogger logger; + Microsoft.Extensions.Logging.ILogger logger; - public VTableStorage(ILogger logger) { + public VTableStorage(Microsoft.Extensions.Logging.ILogger logger) { this.logger = logger; } - public ILogger GetLogger() { + public Microsoft.Extensions.Logging.ILogger GetLogger() { return logger; } diff --git a/ConfuserEx/ConfuserEx.csproj b/ConfuserEx/ConfuserEx.csproj index 5f11173c4..09231a04e 100644 --- a/ConfuserEx/ConfuserEx.csproj +++ b/ConfuserEx/ConfuserEx.csproj @@ -19,6 +19,8 @@ + + diff --git a/ConfuserEx/FlowDocumentSink.cs b/ConfuserEx/FlowDocumentSink.cs new file mode 100644 index 000000000..46a5dc70d --- /dev/null +++ b/ConfuserEx/FlowDocumentSink.cs @@ -0,0 +1,70 @@ +using System; +using System.Windows; +using System.Windows.Documents; +using System.Windows.Media; +using Serilog.Core; +using Serilog.Events; + +namespace ConfuserEx { + /// + /// A Serilog sink that writes log events to a WPF + /// with color-coded output matching the original ConfuserEx console style. + /// + internal sealed class FlowDocumentSink : ILogEventSink { + readonly Paragraph paragraph; + + public FlowDocumentSink(Paragraph paragraph) { + this.paragraph = paragraph ?? throw new ArgumentNullException(nameof(paragraph)); + } + + public void Emit(LogEvent logEvent) { + var brush = GetBrush(logEvent.Level); + var prefix = GetPrefix(logEvent.Level); + var message = logEvent.RenderMessage(); + + Application.Current.Dispatcher.BeginInvoke(new Action(() => { + paragraph.Inlines.Add(new Run(prefix + message) { Foreground = brush }); + paragraph.Inlines.Add(new LineBreak()); + + if (logEvent.Exception != null) { + paragraph.Inlines.Add(new Run("Exception: " + logEvent.Exception) { Foreground = brush }); + paragraph.Inlines.Add(new LineBreak()); + } + })); + } + + static Brush GetBrush(LogEventLevel level) { + switch (level) { + case LogEventLevel.Verbose: + case LogEventLevel.Debug: + return Brushes.Gray; + case LogEventLevel.Information: + return Brushes.White; + case LogEventLevel.Warning: + return Brushes.Yellow; + case LogEventLevel.Error: + case LogEventLevel.Fatal: + return Brushes.Red; + default: + return Brushes.White; + } + } + + static string GetPrefix(LogEventLevel level) { + switch (level) { + case LogEventLevel.Verbose: + case LogEventLevel.Debug: + return "[DEBUG] "; + case LogEventLevel.Information: + return " [INFO] "; + case LogEventLevel.Warning: + return " [WARN] "; + case LogEventLevel.Error: + case LogEventLevel.Fatal: + return "[ERROR] "; + default: + return ""; + } + } + } +} diff --git a/ConfuserEx/ViewModel/UI/ProtectTabVM.cs b/ConfuserEx/ViewModel/UI/ProtectTabVM.cs index a9f7088cc..b36356148 100644 --- a/ConfuserEx/ViewModel/UI/ProtectTabVM.cs +++ b/ConfuserEx/ViewModel/UI/ProtectTabVM.cs @@ -7,12 +7,16 @@ using System.Windows.Media; using CommunityToolkit.Mvvm.Input; using Confuser.Core; +using Confuser.Core.Diagnostics; using Confuser.Core.Project; +using Microsoft.Extensions.Logging; +using Serilog; namespace ConfuserEx.ViewModel { - internal class ProtectTabVM : TabViewModel, ILogger { + internal class ProtectTabVM : TabViewModel, IProgressReporter { readonly Paragraph documentContent; CancellationTokenSource cancelSrc; + DiagnosticCollector collector; double? progress = 0; bool? result; @@ -31,6 +35,10 @@ public ICommand CancelCmd { get { return new RelayCommand(DoCancel, () => App.NavigationDisabled); } } + public ICommand CopyReportCmd { + get { return new RelayCommand(DoCopyReport, () => Result != null && collector != null); } + } + public double? Progress { get { return progress; } set { SetProperty(ref progress, value, "Progress"); } @@ -48,9 +56,28 @@ void DoProtect() { parameters.Project = ((IViewModel)App.Project).Model; if (File.Exists(App.FileName)) Environment.CurrentDirectory = Path.GetDirectoryName(App.FileName); - parameters.Logger = this; documentContent.Inlines.Clear(); + + var serilogLogger = new LoggerConfiguration() + .MinimumLevel.Debug() + .WriteTo.Sink(new FlowDocumentSink(documentContent)) + .CreateLogger(); + + // The logger factory must outlive the async protection run — ConfuserEngine.Run + // executes on a background thread, so we dispose it in the continuation below + // rather than with a method-scoped 'using' (which would dispose it too early). + var loggerFactory = LoggerFactory.Create(builder => + builder.AddSerilog(serilogLogger, dispose: true)); + var melLogger = loggerFactory.CreateLogger("ConfuserEx"); + + // The collector wraps the logger and this progress reporter so a diagnostic report — + // covering both successful and failed runs — can be copied afterwards. It captures the + // full transcript regardless of the display level and forwards everything through. + collector = new DiagnosticCollector(melLogger, this) { Project = parameters.Project }; + parameters.Logger = collector; + parameters.ProgressReporter = collector; + cancelSrc = new CancellationTokenSource(); Result = null; Progress = null; @@ -58,90 +85,63 @@ void DoProtect() { App.NavigationDisabled = true; ConfuserEngine.Run(parameters, cancelSrc.Token) - .ContinueWith(_ => - Application.Current.Dispatcher.BeginInvoke(new Action(() => { - Progress = 0; - App.NavigationDisabled = false; - CommandManager.InvalidateRequerySuggested(); - }))); + .ContinueWith(_ => { + loggerFactory.Dispose(); + Application.Current.Dispatcher.BeginInvoke(new Action(() => { + Progress = 0; + App.NavigationDisabled = false; + CommandManager.InvalidateRequerySuggested(); + })); + }); } void DoCancel() { cancelSrc.Cancel(); } - void AppendLine(string format, Brush foreground, params object[] args) { - Application.Current.Dispatcher.BeginInvoke(new Action(() => { - documentContent.Inlines.Add(new Run(string.Format(format, args)) { Foreground = foreground }); - documentContent.Inlines.Add(new LineBreak()); - })); - } - - #region Logger Impl + void DoCopyReport() { + if (collector == null) + return; - DateTime begin; - - void ILogger.Debug(string msg) { - AppendLine("[DEBUG] {0}", Brushes.Gray, msg); - } - - void ILogger.DebugFormat(string format, params object[] args) { - AppendLine("[DEBUG] {0}", Brushes.Gray, string.Format(format, args)); + try { + Clipboard.SetText(collector.GenerateReport()); + } + catch { + // The clipboard can be transiently locked by another process; a failed copy + // should never crash the app. The user can simply retry. + } } - void ILogger.Info(string msg) { - AppendLine(" [INFO] {0}", Brushes.White, msg); - } - - void ILogger.InfoFormat(string format, params object[] args) { - AppendLine(" [INFO] {0}", Brushes.White, string.Format(format, args)); - } + #region IProgressReporter - void ILogger.Warn(string msg) { - AppendLine(" [WARN] {0}", Brushes.Yellow, msg); - } - - void ILogger.WarnFormat(string format, params object[] args) { - AppendLine(" [WARN] {0}", Brushes.Yellow, string.Format(format, args)); - } - - void ILogger.WarnException(string msg, Exception ex) { - AppendLine(" [WARN] {0}", Brushes.Yellow, msg); - AppendLine("Exception: {0}", Brushes.Yellow, ex); - } - - void ILogger.Error(string msg) { - AppendLine("[ERROR] {0}", Brushes.Red, msg); - } - - void ILogger.ErrorFormat(string format, params object[] args) { - AppendLine("[ERROR] {0}", Brushes.Red, string.Format(format, args)); - } - - void ILogger.ErrorException(string msg, Exception ex) { - AppendLine("[ERROR] {0}", Brushes.Red, msg); - AppendLine("Exception: {0}", Brushes.Red, ex); - } + DateTime begin; - void ILogger.Progress(int progress, int overall) { + void IProgressReporter.Progress(int progress, int overall) { Progress = (double)progress / overall; } - void ILogger.EndProgress() { + void IProgressReporter.EndProgress() { Progress = null; } - void ILogger.Finish(bool successful) { + void IProgressReporter.Finish(bool successful) { DateTime now = DateTime.Now; string timeString = string.Format( "at {0}, {1}:{2:d2} elapsed.", now.ToShortTimeString(), (int)now.Subtract(begin).TotalMinutes, now.Subtract(begin).Seconds); - if (successful) - AppendLine("Finished {0}", Brushes.Lime, timeString); - else - AppendLine("Failed {0}", Brushes.Red, timeString); + + Application.Current.Dispatcher.BeginInvoke(new Action(() => { + if (successful) { + documentContent.Inlines.Add(new Run("Finished " + timeString) { Foreground = Brushes.Lime }); + } + else { + documentContent.Inlines.Add(new Run("Failed " + timeString) { Foreground = Brushes.Red }); + } + documentContent.Inlines.Add(new LineBreak()); + })); + Result = successful; } diff --git a/ConfuserEx/Views/ProtectTabView.xaml b/ConfuserEx/Views/ProtectTabView.xaml index 34fd123be..fb45034a3 100644 --- a/ConfuserEx/Views/ProtectTabView.xaml +++ b/ConfuserEx/Views/ProtectTabView.xaml @@ -10,15 +10,19 @@ + -