Skip to content
Back to skills

Powershell Integration

ASecurity

PowerShell SDK integration in Photino.NET: runspace architecture, SessionManager, streaming execution, TabExpansion2, function injection, PSObject conversion, disposal patterns. PowerShell SDK嵌入Photino.NET。 Use when: embedding PowerShell in .NET app, managing runspace lifecycle, implementing tab completion, injecting functions, serializing PSObjects

  • 4 stars
  • 0 votes
  • 0 copies
  • 4 views
  • Added September 12, 2026
toolsgoc#shellfrontend

Works with

  • cursor

Security analysis

A100/100

Pro scans all 2 files and shows the line behind each finding

Scanned September 12, 2026

npx -y skills add aibot88/sec_skill_store --skill powershell-integration --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Powershell Integration?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Powershell Integration
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/aibot88-powershell-integration/badge)](https://www.skillsdirectory.com/skills/aibot88-powershell-integration)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: photino-powershell-integration
description: "PowerShell SDK integration in Photino.NET: runspace architecture, SessionManager, streaming execution, TabExpansion2, function injection, PSObject conversion, disposal patterns. PowerShell SDK嵌入Photino.NET。 Use when: embedding PowerShell in .NET app, managing runspace lifecycle, implementing tab completion, injecting functions, serializing PSObjects"
disable-model-invocation: true
---

# PowerShell SDK Integration

Photino.NET應用嵌入PowerShell SDK之法:runspace生命周期、會話管理、流式輸出、Tab補全、函數注入及對象序列化。

## Runspace Architecture

PowerShell SDK提供進程內PowerShell引擎。每會話獲獨立runspace及隔離狀態:

```
┌──────────────────────────────────────────┐
│              SessionManager              │
│  ┌────────┐  ┌────────┐  ┌────────┐    │
│  │Session 1│  │Session 2│  │Session 3│   │
│  │Runspace │  │Runspace │  │Runspace │   │
│  │Pipeline │  │Pipeline │  │Pipeline │   │
│  └────────┘  └────────┘  └────────┘    │
│  Each session has independent:           │
│  - Variables ($PWD, $LASTEXITCODE)       │
│  - Functions (custom + injected)         │
│  - History                               │
│  - Working directory                     │
└──────────────────────────────────────────┘
```

## SessionManager

SessionManager創建、追蹤並釋放PowerShell會話:

```csharp
public sealed class SessionManager : IDisposable
{
    private readonly ConcurrentDictionary<string, PowerShellSession> _sessions = new();

    public PowerShellSession CreateSession(string sessionId)
    {
        var session = new PowerShellSession(sessionId);
        if (!_sessions.TryAdd(sessionId, session))
            throw new InvalidOperationException($"Session '{sessionId}' already exists");
        return session;
    }

    public PowerShellSession? GetSession(string sessionId)
    {
        _sessions.TryGetValue(sessionId, out var session);
        return session;
    }

    public void CloseSession(string sessionId)
    {
        if (_sessions.TryRemove(sessionId, out var session))
            session.Dispose();
    }

    public void Dispose()
    {
        foreach (var session in _sessions.Values)
            session.Dispose();
        _sessions.Clear();
    }
}
```

## PowerShellSession

每會話封裝runspace,提供執行、補全及取消功能:

```csharp
public sealed class PowerShellSession : IDisposable
{
    private readonly Runspace _runspace;
    private System.Management.Automation.PowerShell? _currentPipeline;
    private readonly string _sessionId;

    public event Action<string>? OutputReceived;
    public event Action<string>? ErrorReceived;
    public event Action<int>? ExecutionCompleted;

    public PowerShellSession(string sessionId)
    {
        _sessionId = sessionId;
        var iss = InitialSessionState.CreateDefault2();
        _runspace = RunspaceFactory.CreateRunspace(iss);
        _runspace.Open();
        InjectFunctions();
    }

    public async Task ExecuteAsync(string command, CancellationToken ct = default)
    {
        using var ps = System.Management.Automation.PowerShell.Create();
        ps.Runspace = _runspace;
        _currentPipeline = ps;

        ps.AddScript(command);

        // Stream output as it arrives
        var output = new PSDataCollection<PSObject>();
        output.DataAdded += (sender, e) =>
        {
            var data = ((PSDataCollection<PSObject>)sender!)[e.Index];
            OutputReceived?.Invoke(FormatPSObject(data));
        };

        ps.Streams.Error.DataAdded += (sender, e) =>
        {
            var err = ((PSDataCollection<ErrorRecord>)sender!)[e.Index];
            ErrorReceived?.Invoke(err.ToString());
        };

        try
        {
            await Task.Factory.FromAsync(
                ps.BeginInvoke<PSObject, PSObject>(null, output),
                ps.EndInvoke);
        }
        finally
        {
            _currentPipeline = null;
        }

        ExecutionCompleted?.Invoke(ps.HadErrors ? 1 : 0);
    }

    public void Cancel()
    {
        _currentPipeline?.Stop();
    }

    public void Dispose()
    {
        _currentPipeline?.Dispose();
        _runspace.Close();
        _runspace.Dispose();
    }
}
```

## Streaming Execution

PowerShell對象產生時即流式輸出,非命令完成後。此使實時顯示成為可能:

```csharp
// The PSDataCollection fires DataAdded for each object
var output = new PSDataCollection<PSObject>();
output.DataAdded += (sender, e) =>
{
    var data = ((PSDataCollection<PSObject>)sender!)[e.Index];
    // This fires immediately as each object is produced
    OutputReceived?.Invoke(FormatPSObject(data));
};
```

### Output Streams

PowerShell具多輸出流:

| Stream | Property | Use |
|--------|----------|-----|
| Output (1) | `PSDataCollection<PSObject>` | Normal output objects |
| Error (2) | `ps.Streams.Error` | Error records |
| Warning (3) | `ps.Streams.Warning` | Warning messages |
| Verbose (4) | `ps.Streams.Verbose` | Verbose messages |
| Debug (5) | `ps.Streams.Debug` | Debug messages |
| Information (6) | `ps.Streams.Information` | Information records |
| Progress | `ps.Streams.Progress` | Progress bars |

訂閱各流以獲全量輸出:

```csharp
ps.Streams.Warning.DataAdded += (s, e) =>
{
    var warn = ((PSDataCollection<WarningRecord>)s!)[e.Index];
    OutputReceived?.Invoke($"WARNING: {warn.Message}");
};

ps.Streams.Progress.DataAdded += (s, e) =>
{
    var progress = ((PSDataCollection<ProgressRecord>)s!)[e.Index];
    // Forward progress percentage to frontend
};
```

## TabExpansion2

PowerShell SDK含`TabExpansion2`以供命令補全:

```csharp
public CompletionResult[] GetCompletions(string input, int cursorPosition)
{
    using var ps = System.Management.Automation.PowerShell.Create();
    ps.Runspace = _runspace;

    ps.AddCommand("TabExpansion2")
      .AddParameter("inputScript", input)
      .AddParameter("cursorColumn", cursorPosition);

    var results = ps.Invoke();
    if (results.Count == 0) return Array.Empty<CompletionResult>();

    var completions = results[0].Properties["CompletionMatches"].Value;
    if (completions is not IList<CompletionResult> matches)
        return Array.Empty<CompletionResult>();

    return matches.ToArray();
}
```

### Completion Result Structure

```csharp
// Each CompletionResult has:
// - CompletionText: What to insert
// - ListItemText: What to show in the dropdown
// - ResultType: Command, ProviderItem, ParameterName, etc.
// - ToolTip: Additional description
```

### Gotcha: TabExpansion2 Runs Synchronously

`TabExpansion2`可能緩慢(100-500ms),因其審視runspace。務必:
1. 在後台線程運行,絕不阻塞UI
2. 用戶再次輸入時取消進行中補全
3. 對補全請求進行防抖(150-200ms為典型值)

```csharp
private CancellationTokenSource? _completionCts;

public async Task<CompletionResult[]> GetCompletionsAsync(string input, int cursor)
{
    // Cancel previous completion request
    _completionCts?.Cancel();
    _completionCts = new CancellationTokenSource();
    var ct = _completionCts.Token;

    return await Task.Run(() =>
    {
        ct.ThrowIfCancellationRequested();
        return GetCompletions(input, cursor);
    }, ct);
}
```

## Function Injection

向每會話runspace注入自定義PowerShell函數,以供應用特定功能:

```csharp
private void InjectFunctions()
{
    using var ps = System.Management.Automation.PowerShell.Create();
    ps.Runspace = _runspace;

    // Inject a function that the frontend can call
    ps.AddScript(@"
        function global:__beagle_notify {
            param([string]$Message, [string]$Type = 'info')
            $global:__service.Notify($Message, $Type)
        }

        function global:__beagle_setvar {
            param([string]$Name, [object]$Value)
            $global:__service.SetVariable($Name, $Value)
        }
    ");

    ps.Invoke();
}
```

### The `$global:__service` Pattern

通過全局變量向PowerShell暴露C#對象:

```csharp
// Set a C# service object accessible from PowerShell
_runspace.SessionStateProxy.SetVariable("__service", new PowerShellServiceBridge(this));

// The bridge exposes methods that PowerShell can call
public class PowerShellServiceBridge
{
    private readonly PowerShellSession _session;

    public PowerShellServiceBridge(PowerShellSession session)
        => _session = session;

    public void Notify(string message, string type)
    {
        _session.NotificationReceived?.Invoke(message, type);
    }

    public void SetVariable(string name, object value)
    {
        _session.VariableChanged?.Invoke(name, value);
    }
}
```

## PSObject Conversion

PowerShell返回`PSObject`包裝器。將其轉換為前端JSON友好結構:

```csharp
private static string FormatPSObject(PSObject obj)
{
    if (obj.BaseObject is string s)
        return s;

    // For complex objects, extract properties into a dictionary
    var dict = new Dictionary<string, object?>();
    foreach (var prop in obj.Properties)
    {
        try
        {
            dict[prop.Name] = prop.Value?.ToString();
        }
        catch
        {
            dict[prop.Name] = null;
        }
    }

    return JsonSerializer.Serialize(dict);
}
```

### Rich Object Rendering

已知類型提供結構化輸出:

```csharp
private static object ConvertPSObject(PSObject obj)
{
    return obj.BaseObject switch
    {
        string s => new { type = "text", value = s },
        System.Diagnostics.Process p => new
        {
            type = "process",
            pid = p.Id,
            name = p.ProcessName,
            cpu = p.TotalProcessorTime,
            memory = p.WorkingSet64
        },
        System.IO.FileInfo f => new
        {
            type = "file",
            name = f.Name,
            size = f.Length,
            modified = f.LastWriteTime
        },
        _ => new { type = "object", properties = ExtractProperties(obj) }
    };
}
```

## Disposal Pattern

PowerShell runspace持有原生資源,正確釋放至關重要:

```csharp
public sealed class PowerShellSession : IDisposable
{
    private bool _disposed;

    public void Dispose()
    {
        if (_disposed) return;
        _disposed = true;

        // Cancel any running pipeline first
        try { _currentPipeline?.Stop(); } catch { }
        try { _currentPipeline?.Dispose(); } catch { }

        // Close runspace (blocks until pipeline finishes)
        try
        {
            if (_runspace.RunspaceStateInfo.State == RunspaceState.Opened)
                _runspace.Close();
        }
        catch { }

        _runspace.Dispose();
    }
}
```

### Disposal Order

1. 停止運行中管道
2. 關閉runspace(優雅停機)
3. 釋放runspace(釋放原生資源)
4. 從SessionManager移除

管道執行中絕不釋放runspace——先調用`Stop()`。

## Common Pitfalls

| Issue | Cause | Fix |
|-------|-------|-----|
| `PSInvalidOperationException: Runspace is not opened` | Using disposed session | Check `_disposed` before operations |
| Memory leak in long sessions | Unreleased pipelines | Always dispose `PowerShell` objects |
| Deadlock in event handlers | Blocking STA thread | Use `_ = HandleAsync()` pattern |
| Slow first command | Runspace initialization | Pre-warm with dummy command on creation |
| `ApartmentState` error | Wrong thread model | Ensure `[STAThread]` on Main |

Files in this skill

  • SKILL.md11.6 KB
  • source.json902 B

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.