multithreaded-task-migration

📁 dotnet/msbuild 📅 9 days ago
1
总安装量
1
周安装量
#52499
全站排名
安装命令
npx skills add https://github.com/dotnet/msbuild --skill multithreaded-task-migration

Agent 安装分布

cursor 1
github-copilot 1
antigravity 1

Skill 文档

Migrating MSBuild Tasks to Multithreaded API

This skill guides you through migrating MSBuild tasks to support multithreaded execution by implementing IMultiThreadableTask and using TaskEnvironment.

Overview

MSBuild’s multithreaded execution model requires tasks to avoid global process state (working directory, environment variables). Thread-safe tasks declare this capability by annotating with MSBuildMultiThreadableTask and use TaskEnvironment provided by IMultiThreadableTask for safe alternatives.

Migration Steps

Step 1: Update Task Class Declaration

a. add the attribute b. AND implement the interface if it’s necessary to use TaskEnvironment APIs.

[MSBuildMultiThreadableTask]
public class MyTask : Task, IMultiThreadableTask
{
    public TaskEnvironment TaskEnvironment { get; set; }
    ...
}

Step 2: Absolutize Paths Before File Operations

Critical: All path strings must be absolutized with TaskEnvironment.GetAbsolutePath() before use in file system APIs. This ensures paths resolve relative to the project directory, not the process working directory.

// BEFORE - File.Exists uses process working directory for relative paths (UNSAFE)
if (File.Exists(inputPath))
{
    string content = File.ReadAllText(inputPath);
}

// AFTER - Absolutize first, then use in file operations (SAFE)
AbsolutePath absolutePath = TaskEnvironment.GetAbsolutePath(inputPath);
if (File.Exists(absolutePath))
{
    string content = File.ReadAllText(absolutePath);
}

GetAbsolutePath() throws for null/empty inputs. See Exception Handling in Batch Operations for handling strategies.

The AbsolutePath struct:

  • Has Value property returning the absolute path string
  • Has OriginalValue property preserving the input path
  • Is implicitly convertible to string for File/Directory API compatibility

CAUTION: FileInfo can be created from relative paths – only use FileInfo.FullName if constructed with an absolute path.

Note:

If code previously used Path.GetFullPath() for canonicalization (resolving .. segments, normalizing separators), call AbsolutePath.GetCanonicalForm() after absolutization to preserve that behavior. Do not simply replace Path.GetFullPath with GetAbsolutePath if canonicalization was the intent. You can replace Path.GetFullPath behavior by combining both:

AbsolutePath absolutePath = TaskEnvironment.GetAbsolutePath(inputPath).GetCanonicalForm();

The goal is MAXIMUM compatibility so think about these edge cases so it behaves the same as before.

Step 3: Replace Environment Variable APIs

// BEFORE (UNSAFE)
string value = Environment.GetEnvironmentVariable("VAR");
Environment.SetEnvironmentVariable("VAR", "value");

// AFTER (SAFE)
string value = TaskEnvironment.GetEnvironmentVariable("VAR");
TaskEnvironment.SetEnvironmentVariable("VAR", "value");

Step 4: Replace Process Start APIs

// BEFORE (UNSAFE - inherits process state)
var psi = new ProcessStartInfo("tool.exe");

// AFTER (SAFE - uses task's isolated environment)
var psi = TaskEnvironment.GetProcessStartInfo();
psi.FileName = "tool.exe";

Updating Unit Tests

Every test creating a task instance must set TaskEnvironment. Use TaskEnvironmentHelper.CreateForTest():

// BEFORE
var task = new Copy
{
    BuildEngine = new MockEngine(true),
    SourceFiles = sourceFiles,
    DestinationFolder = new TaskItem(destFolder),
};

// AFTER
var task = new Copy
{
    TaskEnvironment = TaskEnvironmentHelper.CreateForTest(),
    BuildEngine = new MockEngine(true),
    SourceFiles = sourceFiles,
    DestinationFolder = new TaskItem(destFolder),
};

Testing Exception Cases

Tasks must handle null/empty path inputs properly.

[Fact]
public void Task_WithNullPath_Throws()
{
    var task = CreateTask();
    
    Should.Throw<ArgumentNullException>(() => task.ProcessPath(null!));
}

APIs to Avoid

Critical Errors (No Alternative)

  • Environment.Exit(), Environment.FailFast() – Return false or throw instead
  • Process.GetCurrentProcess().Kill() – Never terminate process
  • ThreadPool.SetMinThreads/MaxThreads – Process-wide settings
  • CultureInfo.DefaultThreadCurrentCulture (setter) – Affects all threads
  • Console.* – Interferes with logging

Requires TaskEnvironment

  • Environment.CurrentDirectory → TaskEnvironment.ProjectDirectory
  • Environment.GetEnvironmentVariable → TaskEnvironment.GetEnvironmentVariable
  • Environment.SetEnvironmentVariable → TaskEnvironment.SetEnvironmentVariable
  • Path.GetFullPath → TaskEnvironment.GetAbsolutePath
  • Process.Start, ProcessStartInfo → TaskEnvironment.GetProcessStartInfo

File APIs Need Absolute Paths

  • File.*, Directory.*, FileInfo, DirectoryInfo, FileStream, StreamReader, StreamWriter
  • All path parameters must be absolute

Potential Issues (Review Required)

  • Assembly.Load*, LoadFrom, LoadFile – Version conflicts
  • Activator.CreateInstance* – Version conflicts

Practical Notes

CRITICAL: Trace All Path String Usage

You MUST trace every path string variable through the entire codebase to find all places where it flows into file system operations – including helper methods, utility classes, and third-party code that may internally use File APIs.

Steps:

  1. Find every path string (e.g., item.ItemSpec, function parameters)
  2. Trace downstream: Follow the variable through all method calls and assignments
  3. Absolutize BEFORE any code path that touches the file system
  4. Use OriginalValue for user-facing output (logs, errors)
// WRONG - LockCheck internally uses File APIs with non-absolutized path
string sourceSpec = item.ItemSpec;  // sourceSpec is string
string lockedMsg = LockCheck.GetLockedFileMessage(sourceSpec);  // BUG! Trace the call!

// CORRECT - absolutized path passed to helper
AbsolutePath sourceFile = TaskEnvironment.GetAbsolutePath(item.ItemSpec);
string lockedMsg = LockCheck.GetLockedFileMessage(sourceFile);

// For error messages, preserve original user input
Log.LogError("...", sourceFile.OriginalValue, ...);

Exception Handling in Batch Operations

Important: GetAbsolutePath() throws on null/empty inputs. In batch processing scenarios (e.g., iterating over multiple files), an unhandled exception will abort the entire batch. Tasks must catch and handle these exceptions appropriately to avoid cutting short processing of valid items:

// WRONG - one bad path aborts entire batch
foreach (ITaskItem item in SourceFiles)
{
    AbsolutePath path = TaskEnvironment.GetAbsolutePath(item.ItemSpec); // throws, batch stops!
    ProcessFile(path);
}

// CORRECT - handle exceptions, continue processing valid items
bool success = true;
foreach (ITaskItem item in SourceFiles)
{
    try
    {
        AbsolutePath path = TaskEnvironment.GetAbsolutePath(item.ItemSpec);
        ProcessFile(path);
    }
    catch (ArgumentException ex)
    {
        Log.LogError($"Invalid path '{item.ItemSpec}': {ex.Message}");
        success = false;
        // Continue processing remaining items
    }
}
return success;

Consider the task’s error semantics: should one invalid path fail the entire task immediately, or should all items be processed with errors collected? Match the original task’s behavior.

Prefer AbsolutePath Over String

When working with paths, stay in the AbsolutePath world as much as possible rather than converting back and forth to string. This reduces unnecessary conversions and maintains type safety:

// AVOID - unnecessary conversions
string path = TaskEnvironment.GetAbsolutePath(input).Value;
AbsolutePath again = TaskEnvironment.GetAbsolutePath(path); // redundant!

// PREFER - stay in AbsolutePath
AbsolutePath path = TaskEnvironment.GetAbsolutePath(input);
// Use path directly - it's implicitly convertible to string where needed
File.ReadAllText(path);

TaskEnvironment is Not Thread-Safe

If your task spawns multiple threads internally, you must synchronize access to TaskEnvironment. However, each task instance gets its own environment, so no synchronization with other tasks is needed.

Checklist

  • Task is annotated with MSBuildMultiThreadableTask attribute and implements IMultiThreadableTask if TaskEnvironment APIs are required
  • All environment variable access uses TaskEnvironment APIs
  • All process spawning uses TaskEnvironment.GetProcessStartInfo()
  • All file system APIs receive absolute paths
  • All helper methods receiving path strings are traced to verify they don’t internally use File APIs with non-absolutized paths
  • No use of Environment.CurrentDirectory
  • All tests set TaskEnvironment = TaskEnvironmentHelper.CreateForTest()
  • Tests verify exception behavior for null/empty paths
  • No use of forbidden APIs (Environment.Exit, etc.)

References