Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,28 @@ is not part of this repository.
doing nothing.
- After a reading of the list fails once and the next one works, the window says so straight away.
It used to go on saying it could not read the list until something on the machine changed.
- A force stop no longer ends a process under services that still need it. While services depending
on the entry are running outside the plan, `bws kill` and the window's Force stop refuse and name
them. The same goes for running services that depend on anything sharing the entry's process.
`bws kill NAME --dependents` still stops the dependents as part of the plan, and if one of them
will not stop, the process is not ended.
- `bws kill NAME --force --dependents` is refused. Its preview listed the dependents as stopping,
while the run skipped their stops along with the polite one and ended the process under them.
- A force stop asks the entry itself to stop before the other services sharing its process, and asks
those only if the entry did not stop. A service sharing the process that refused to stop used to
make the plan skip the entry's own polite stop and end the process at once, and when the entry
would have stopped on its own, the others had been stopped for nothing. In the JSON of a run, the
steps not needed are reported with `"skippedBecause": "processStays"`, and the run still counts as
completed.
- The preview of a force stop names a critical service arriving with `--dependents`, a service
sharing the process that does not accept a stop, and one that is disabled and could not be
started again by `--restart`.
- The way back after a force stop includes the services that ended with the process without stopping
on their own. After `bws kill NAME --restart --force` it used to tell you to stop a service that was
running before and after.
- After a stop refused because other running services depend on the entry, or because of missing
rights, the window no longer offers Force stop, which could not help with either.
- The equivalent command of a force stop planned with its dependents includes `--dependents`.

## [0.3.0] - 2026-09-25

Expand Down
12 changes: 9 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -265,9 +265,15 @@ line that asks for the same thing, and the way back afterwards.
The window greys the button and says why beside it.
- **Force stop is two steps, and the second is conditional.** `bws kill` asks the service to stop and
ends its process only if that does not work. The preview names the process and every other service
living in it, because ending a process takes all of them. `--restart` brings them back, `--force`
skips the polite step - and the preview shows one step instead of two, so the difference is visible
before anything happens.
living in it, because ending a process takes all of them - and those are asked to stop only when the
service itself did not, so a polite stop that works leaves them running. `--restart` brings them
back, `--force` skips the polite step - and the preview shows one step instead of two, so the
difference is visible before anything happens.
- **Force stop never pulls a process out from under a running service.** While services that depend
on it, or on anything sharing its process, are running outside the plan, `bws kill` refuses and
names them. `--dependents` stops the first kind as part of the plan, and a dependant that will not
stop holds the process ending back. `--force` does not go with `--dependents`, because skipping the
polite step would skip theirs too.
- **Afterwards, the way back.** A report ends with what did not work and the commands that put things
back - and the window offers *Copy all* over them.
- **A start type change moves nothing on its own.** It changes what happens at the next boot, leaves
Expand Down
21 changes: 21 additions & 0 deletions src/Bws.Cli/PlanText.cs
Original file line number Diff line number Diff line change
Expand Up @@ -323,10 +323,31 @@ private static string Took(long milliseconds) => milliseconds < 1000
PlanProblemKind.CannotStartLate => Texts.Of(
"cli.plan.problem.cannotStartLate", problem.ServiceName, Join(problem.Related)),

PlanProblemKind.DependentsInTheWay or PlanProblemKind.NeighbourNeeded => StillRunning(problem),

_ => throw new ArgumentOutOfRangeException(
nameof(problem), problem.Kind, EquivalentCommand.Unhandled)
};

/// <summary>
/// The two refusals of a forced stop that name entries still running, since 2026-09-29.
///
/// <b>Both name them, because they are what somebody has to stop first. Only the first offers
/// --dependents</b>, which reaches what depends on the entry itself and nothing else. Out of
/// Describe because that switch stands close to the complexity ceiling.
/// </summary>
private static string StillRunning(PlanProblem problem) => problem.Kind == PlanProblemKind.DependentsInTheWay
? Texts.Of(
problem.Related.Count == 1
? "cli.plan.problem.dependentsInTheWay.one"
: "cli.plan.problem.dependentsInTheWay.many",
problem.ServiceName, Join(problem.Related))
: Texts.Of(
problem.Related.Count == 1
? "cli.plan.problem.neighbourNeeded.one"
: "cli.plan.problem.neighbourNeeded.many",
problem.ServiceName, Join(problem.Related));

private static bool IsTheTarget(PlanProblem problem) =>
problem.Related.Count == 1
&& string.Equals(problem.Related[0], problem.ServiceName, StringComparison.OrdinalIgnoreCase);
Expand Down
26 changes: 26 additions & 0 deletions src/Bws.Cli/Refusals.cs
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,11 @@ internal static class Refusals
}
}

if (AboutTheForcing(options) is { } forced)
{
return forced;
}

if (WriteCommands.NeedsAStartType(options.Kind) && AboutTheSetting(options.ServiceName, options.Setting) is { } code)
{
return code;
Expand All @@ -229,6 +234,27 @@ internal static class Refusals
return null;
}

/// <summary>
/// What is wrong with a kill ask, or nothing.
///
/// <b>--force skips every stop in front of the ending, the cascade's too</b>, so beside --dependents
/// it promised dependants stopped and then ended the process under them - the preview and the run
/// disagreeing, which is rule 5 of the untouchable list (stability report W-4, 2026-09-29). Refused
/// here with both ways out, before anything is read. The core refuses the same shape loudly behind
/// this. Its own method because AboutTheAsk stands close to the complexity ceiling.
/// </summary>
private static int? AboutTheForcing(CommandLine options)
{
if (options.Kind != CommandKind.Kill || !options.Force || !options.Dependents)
{
return null;
}

Console.Error.WriteLine(Texts.Of("cli.forceWithDependents", options.ServiceName));

return ExitCode.Usage;
}

/// <summary>
/// What is wrong with a start-type ask, or nothing.
///
Expand Down
8 changes: 7 additions & 1 deletion src/Bws.Cli/Resources/cli.en.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
"cli.missingStartType": "start-type needs a start type after the name. Example: bws start-type {0} manual --dry-run\nThe start types are: {1}",
"cli.badStartType": "{0} is not a start type. The start types are: {1}",
"cli.stopNeedsDisabled": "--stop goes only with disabled. To stop an entry you set to {0}, run bws stop with its name after this command.",
"cli.forceWithDependents": "--force and --dependents cannot go together. --force skips every stop before the process is ended, so the entries depending on {0} would still be running when it goes. Drop --force to have them stopped first, or stop them yourself and then use --force.",
"cli.badTimeout": "--timeout takes a whole number of seconds, at least 1. It got: {0}",

"cli.plan.heading.one": "Plan: {0} {1} (1 step)",
Expand Down Expand Up @@ -103,6 +104,7 @@
"cli.run.outcome.alreadyThere": "already there, nothing to do",
"cli.run.outcome.earlierStepFailed": "not tried, an earlier step did not work",
"cli.run.outcome.cancelled": "not tried, the run was interrupted",
"cli.run.outcome.processStays": "not needed, the process it lives in is not being ended",

"cli.run.took.milliseconds": "{0} ms",
"cli.run.took.seconds": "{0} s",
Expand All @@ -118,7 +120,11 @@
"cli.plan.problem.processCannotBeEnded": "Windows will not let this tool end process {1}, which is the process {0} runs in. It said: {2} (error {3}). Nothing was changed. A process the system protects is the usual reason, and no tool running as you gets past it.",
"cli.plan.problem.processCannotBeEnded.plain": "Windows will not let this tool end process {1}, which is the process {0} runs in. Nothing was changed. A process the system protects is the usual reason, and no tool running as you gets past it.",
"cli.plan.problem.cannotStartLate": "{0} belongs to the load order group {1}, and Windows does not let such an entry start late. Nothing was changed. The word automatic starts it at boot with the others.",
"cli.plan.problem.cascadeUnreadable":"Could not read everything that depends on {0}, so the list of what ending its process would take down would be shorter than the truth. There is no preview this tool can honestly offer for that.",
"cli.plan.problem.dependentsInTheWay.one": "{1} depends on {0} and is still running. Ending the process under it is exactly what Windows refuses an ordinary stop for, so there is no plan. Stop it first, or add --dependents to stop it as part of this one.",
"cli.plan.problem.dependentsInTheWay.many": "These depend on {0} and are still running: {1}. Ending the process under them is exactly what Windows refuses an ordinary stop for, so there is no plan. Stop them first, or add --dependents to stop them as part of this one.",
"cli.plan.problem.neighbourNeeded.one": "{1} is running and depends on an entry that shares the process of {0}, which ending that process would take down. There is no plan. Stop {1} first.",
"cli.plan.problem.neighbourNeeded.many": "These are running and depend on an entry that shares the process of {0}, which ending that process would take down: {1}. There is no plan. Stop them first.",
"cli.plan.problem.cascadeUnreadable":"Could not read everything that depends on {0} or on what shares its process, so the list of what ending its process would take down would be shorter than the truth. There is no preview this tool can honestly offer for that.",

"cli.column.name": "NAME",
"cli.column.displayName": "DISPLAY NAME",
Expand Down
8 changes: 7 additions & 1 deletion src/Bws.Core/Planning/EquivalentCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -112,9 +112,15 @@ public static string For(ServiceAction action)
// nothing, carried in from a selection whose tick box belongs to a different question. That
// is the same shape as the nine two-way branches this file's Unhandled constant describes:
// a condition phrased as everything-except answers for kinds nobody has written yet.
//
// KILL TAKES IT TOO AND WAS MISSING HERE UNTIL 2026-09-29 (stability report W-12). The command
// line has accepted it on kill from the day the verb arrived, so a forced stop planned with
// its dependents came back as a line that, pasted, planned the stop without them - which since
// that day is a refusal rather than a quieter plan.
var switches = new List<string>();

if (action.IncludeDependents && action.Kind is ActionKind.Stop or ActionKind.Restart)
if (action.IncludeDependents
&& action.Kind is ActionKind.Stop or ActionKind.Restart or ActionKind.ForceStop or ActionKind.ForceRestart)
{
switches.Add(Dependents);
}
Expand Down
Loading
Loading