Skip to content

Jakub-Syrek/WindowsWebServiceManager

Repository files navigation

Windows Web Service Manager (wwsm)

CI Release Latest release .NET Platform IIS Kestrel Windows Services Docker Tests Last commit Code size

A single CLI to start, stop, restart, health-check, and bust the cache of web apps regardless of how they're hosted on Windows — IIS sites/pools, self-hosted Kestrel processes, Windows services, or Docker containers.

.NET 8 · Windows-only · zero external services · configured from a single appsettings.json


Why

On a typical Windows host you end up juggling iisreset, sc.exe, Get-Process, docker stop, and a pile of admin endpoints just to bounce one app and clear its cache. wwsm reads one config file, figures out the hosting model per app, and exposes a uniform verb set on top.

flowchart LR
    User([User]) -->|wwsm start orders-api| CLI[wwsm CLI]
    CLI --> Dispatcher{Verb<br/>Dispatcher}
    Dispatcher --> Repo[(WebAppRepository<br/>appsettings + IIS discovery)]
    Dispatcher --> Factory[WebAppManager<br/>Factory]
    Factory -->|Iis| IIS[IIS<br/>Manager]
    Factory -->|Kestrel| Kestrel[Kestrel<br/>Process Mgr]
    Factory -->|WindowsService| WinSvc[Windows Service<br/>Mgr]
    Factory -->|Docker| Docker[Docker<br/>Mgr]
    IIS --> IISHost[(IIS<br/>ServerManager)]
    Kestrel --> Proc[(OS Process)]
    WinSvc --> SCM[(Service<br/>Control Mgr)]
    Docker --> Dockerd[(docker CLI)]
    IIS & Kestrel & WinSvc & Docker --> Health[HealthCheck<br/>Service]
    Health --> HTTP[(HTTP<br/>health URL)]
Loading

Quick start

# Build
dotnet build

# Run from the build output
.\bin\Debug\net8.0-windows\wwsm.exe list

# Or via dotnet run
dotnet run -- status orders-api
dotnet run -- restart billing-svc
dotnet run -- clear-cache orders-api --method http

CLI verbs

Verb Aliases Purpose
list ls Show all managed apps with status + health
status <id> Detailed status for one app
start <id> Start an app
stop <id> Stop an app
restart <id> Stop then start
clear-cache <id> clearcache, reset-cache Run a cache reset strategy. --method recycle|http|purge-files; omit to be prompted.
help --help, -h, /? Print usage

How it's wired

Two strategy-pattern families do all the real work. Both register multiple implementations of the same interface in DI and resolve to one by an enum key — adding a new hosting model or cache method is "implement the interface and register it" in ServiceCollectionExtensions.

Hosting model strategies

classDiagram
    class IWebAppManager {
        <<interface>>
        +WebAppType SupportedType
        +GetStateAsync(app, ct) Result~WebAppState~
        +StartAsync(app, ct) Result
        +StopAsync(app, ct) Result
        +RestartAsync(app, ct) Result
    }
    class WebAppManagerFactory {
        -Dictionary~WebAppType,IWebAppManager~ _byType
        +Resolve(WebAppType) IWebAppManager
    }
    class IisWebAppManager
    class KestrelProcessManager
    class WindowsServiceWebAppManager
    class DockerWebAppManager

    IWebAppManager <|.. IisWebAppManager
    IWebAppManager <|.. KestrelProcessManager
    IWebAppManager <|.. WindowsServiceWebAppManager
    IWebAppManager <|.. DockerWebAppManager
    WebAppManagerFactory o-- IWebAppManager : has many
Loading

Cache reset strategies

classDiagram
    class ICacheResetStrategy {
        <<interface>>
        +CacheResetMethod Method
        +IsApplicable(app) bool
        +ExecuteAsync(app, ct) Result
    }
    class CacheResetCoordinator {
        +AvailableFor(app) CacheResetMethod[]
        +ExecuteAsync(app, method, ct) Result
    }
    class RecycleCacheReset {
        Restarts host;
        recycles IIS pool in-place
    }
    class HttpCacheReset {
        POSTs configured admin endpoint
    }
    class FileCacheReset {
        Purges configured directory
    }

    ICacheResetStrategy <|.. RecycleCacheReset
    ICacheResetStrategy <|.. HttpCacheReset
    ICacheResetStrategy <|.. FileCacheReset
    CacheResetCoordinator o-- ICacheResetStrategy : has many
Loading

Recycle is always applicable (it falls back to RestartAsync); HttpEndpoint requires Cache.HttpEndpoint.Url; PurgeFiles requires Cache.PurgePath.

What happens on wwsm restart <id>

sequenceDiagram
    autonumber
    actor U as User
    participant D as CommandDispatcher
    participant R as WebAppRepository
    participant F as WebAppManagerFactory
    participant M as IWebAppManager (resolved)
    participant H as HealthCheckService

    U->>D: wwsm restart orders-api
    D->>R: FindAsync("orders-api")
    R-->>D: WebAppDescriptor (Type=Kestrel)
    D->>F: Resolve(Kestrel)
    F-->>D: KestrelProcessManager
    D->>M: StopAsync(app)
    M-->>D: Result.Ok
    D->>M: StartAsync(app)
    M-->>D: Result.Ok
    D->>M: GetStateAsync(app)
    M->>H: CheckAsync(HealthUrl)
    H-->>M: 200 OK
    M-->>D: WebAppState(Running)
    D-->>U: ✓ printed via ConsoleRenderer
Loading

Status state machine

WebAppStatus folds OS-level state and HTTP health into one value the CLI renders. The interesting transition is Running + bad health → Unhealthy — a process that's up but failing health is not reported as Running.

stateDiagram-v2
    [*] --> Unknown
    Unknown --> Stopped
    Stopped --> Starting : start
    Starting --> Running : process up
    Running --> Unhealthy : health URL fails
    Unhealthy --> Running : health URL recovers
    Running --> Stopping : stop
    Unhealthy --> Stopping : stop
    Stopping --> Stopped
    Stopped --> NotFound : descriptor gone
    Running --> NotFound : descriptor gone
Loading

Configuration

Apps are read from appsettings.json under WebServiceManager:Applications, merged with IIS auto-discovery (toggle via WebServiceManager:IisAutoDiscovery). Static config wins on Id collisions; discovered IIS apps get synthetic Ids like iis-site:Default Web Site / iis-pool:DefaultAppPool.

flowchart LR
    A[appsettings.json] -->|bind| Opts[WebServiceManagerOptions]
    Opts --> Apps[Static<br/>Applications]
    IIS[IIS ServerManager] -->|sites + pools| Disc[IisDiscoveryService]
    Apps --> Merge{{Merge by Id<br/>static wins}}
    Disc --> Merge
    Merge --> Cache[(Cached for<br/>process lifetime)]
    Cache --> CLI[CLI commands]
Loading

Example descriptor shapes

{
  "WebServiceManager": {
    "IisAutoDiscovery": true,
    "HealthCheck":  { "TimeoutSeconds": 5 },
    "Operation":    { "StateTransitionTimeoutSeconds": 30, "MaxRetries": 3 },
    "Applications": [
      {
        "Id": "orders-api",
        "Type": "Kestrel",
        "HealthUrl": "http://localhost:5010/health",
        "Process": { "ExecutablePath": "C:\\apps\\orders\\Orders.Api.exe" },
        "Cache":   {
          "HttpEndpoint": { "Url": "http://localhost:5010/admin/cache/clear", "Method": "POST" },
          "PurgePath":    "C:\\apps\\orders\\cache"
        }
      },
      { "Id": "billing-svc",     "Type": "WindowsService", "ServiceName":   "BillingWebApi" },
      { "Id": "frontend-docker", "Type": "Docker",         "ContainerName": "frontend-web"  }
    ]
  }
}

Requirements

  • Windows 10/11 or Windows Server with .NET 8 SDK
  • Microsoft.Web.Administration (bundled via NuGet) — IIS must be installed if you use the Iis type or auto-discovery
  • docker on PATH if you use the Docker type
  • Sufficient privileges for the operation: stopping/starting Windows services and IIS sites usually requires an elevated shell

CI / CD

Two GitHub Actions workflows under .github/workflows/:

  • ci.yml — runs on every push to main and PR → main. Restores → builds the solution (Release) → runs the xunit suite (99 tests) → uploads .trx as artifact. Pinned to windows-latest because the manager talks to IIS / Windows Services APIs.
  • release.yml — runs when a v* tag is pushed. Publishes two flavors of the CLI and attaches them to an auto-created GitHub Release with auto-generated notes from the commit history:
    • wwsm-<ver>-win-x64-selfcontained.zip — single-file wwsm.exe with the .NET 8 runtime bundled (~70 MB, no install needed)
    • wwsm-<ver>-win-x64-framework-dependent.zip — smaller (~5 MB) but requires .NET 8 Runtime on the target machine

To cut a release:

git tag v1.0.0
git push origin v1.0.0

Dependabot (.github/dependabot.yml) opens weekly PRs every Monday for outdated NuGet packages (main and Tests projects, separately) and for the GitHub Actions versions themselves. Labels: deps / deps tests / ci, capped at 5/3/unlimited PRs respectively.

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages