Skip to content

Commit c82dbb8

Browse files
daxian-dbwSteveL-MSFTjoeyaiello
authored
Merge RFC0052 as Final - Support notification on pwsh startup when a new update is available (#162)
* Support notification on pwsh startup for new updates * Fix typo -- Non-Goals Co-Authored-By: daxian-dbw <[email protected]> * Fix typo -- per day Co-Authored-By: daxian-dbw <[email protected]> * Update to be more accurate on the implementation * Address comments, update according to the implementation * Update RFC with the new environment variable settings * Update the RFC based on the implementation * Minor update * Mention the message is printed with foreground and background colors inverted * Address comment * Prepare RFC0052 (update notifications) for merging as Final Co-authored-by: Steve Lee <[email protected]> Co-authored-by: Joey Aiello <[email protected]>
1 parent 3caa52a commit c82dbb8

File tree

1 file changed

+304
-0
lines changed

1 file changed

+304
-0
lines changed
Lines changed: 304 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,304 @@
1+
---
2+
RFC: 0052
3+
Author: Dongbo Wang
4+
Status: Final
5+
SupercededBy: N/A
6+
Version: 1.0
7+
Area: Console
8+
Comments Due: 4/30/2019
9+
Plan to implement: Yes
10+
---
11+
12+
# Notification on PowerShell version updates
13+
14+
Today, to find out whether a new version of PowerShell is available,
15+
one has to check the release page of the `PowerShell\PowerShell` repository,
16+
or depend on communication channels like `twitter` or `GitHub Notifications`.
17+
It would be convenient if `pwsh` itself can notify the user of a new update on startup.
18+
19+
## Motivation
20+
21+
As a PowerShell user, I get notified when a new version of `pwsh` becomes available.
22+
23+
## Specification
24+
25+
This feature is in the PowerShell console host, not in the PowerShell engine.
26+
27+
### Target Goals
28+
29+
1. No notification or update check when the running `pwsh` is a self-built version.
30+
No notification or update check for non-interactive sessions.
31+
Also, no notification when the PowerShell banner message is suppressed.
32+
33+
2. When there is a new update, assuming you use `pwsh` every day
34+
and at least one interactive `pwsh` session lasts long enough for the update check,
35+
then you should be able to see an update notification during the `pwsh` startup on the same day of a release or the next day at the latest.
36+
37+
3. This feature must have very minimal impact on the startup time of `pwsh`.
38+
This means the check for update must not happen during `pwsh` startup.
39+
The only acceptable extra overhead to the `pwsh` startup should just be the work related to printing the notification.
40+
41+
4. Check for updates should not blindly run for every interactive `pwsh` session.
42+
For a particular version of `pwsh`, only one check at most can run to complete per day
43+
no matter how many interactive session of the `pwsh` are started/opened in that day.
44+
45+
5. After a new update is detected during a successful check,
46+
all subsequent interactive sessions of that version of `pwsh` should show the notification at startup time.
47+
And subsequent checks can be avoided for a reasonable period of time, such as a week.
48+
49+
6. `pwsh` of preview versions should check for the new preview version as well as the new GA version.
50+
`pwsh` of GA versions should check for the new GA version only.
51+
52+
7. The notification and update check are not needed in some scenarios,
53+
such as when `pwsh` is in a container image.
54+
Hence, you should be able to suppress them altogether by setting an environment variable.
55+
56+
### Non-Goals
57+
58+
1. Notification shows up right after a new version of `pwsh` is released.
59+
60+
_This is not a goal._
61+
Assuming you use `pwsh` interactively every day,
62+
then a notification about the new release may show up on the same day,
63+
but is guaranteed no later than the next day.
64+
65+
2. If an update check detects a new release, the notification should show up in the same session.
66+
67+
_This is not a goal._
68+
An update check should happen way after the startup of an interactive session,
69+
and thus it has no impact on whether or not a notification will be shown at the startup of that session.
70+
If new release is detected,
71+
the subsequent interactive sessions will show a notification about that new release.
72+
73+
3. If a new release is available, `pwsh` is able to automatically upgrade.
74+
75+
_This is not a goal._
76+
A notification message is printed, but `pwsh` will not auto-upgrade.
77+
78+
### Implementation
79+
80+
This section talks about
81+
82+
- how to control the update notification behavior
83+
- when to do the update check
84+
- how to persist the detected new release for subsequent `pwsh` sessions to use
85+
- how to synchronize update checks from different processes of the same version `pwsh` so that at most only one can run to complete during a day
86+
- how to do the update check
87+
- how to display the notification
88+
89+
#### How to control the update notification behavior
90+
91+
The environment variable `POWERSHELL_UPDATECHECK` will be introduced to control the behavior of the update notification feature.
92+
The environment variable supports 3 values:
93+
94+
- `Off`. This turns off the update notification feature.
95+
96+
- `Default`. This gives you the default behaviors:
97+
- `pwsh` of preview versions check for the new preview version as well as the new GA version.
98+
- `pwsh` of GA versions check for the new GA version only.
99+
100+
- `LTS`. `pwsh` of both preview and stable versions check for the new LTS GA version only.
101+
102+
The notification behavior is mapped to the following enum:
103+
104+
```c#
105+
private enum NotificationType
106+
{
107+
/// <summary>
108+
/// Turn off the udpate notification.
109+
/// </summary>
110+
Off = 0,
111+
112+
/// <summary>
113+
/// Give you the default behaviors:
114+
/// - the preview version 'pwsh' checks for the new preview version and the new GA version.
115+
/// - the GA version 'pwsh' checks for the new GA version only.
116+
/// </summary>
117+
Default = 1,
118+
119+
/// <summary>
120+
/// Both preview and GA version 'pwsh' checks for the new LTS version only.
121+
/// </summary>
122+
LTS = 2
123+
}
124+
```
125+
126+
#### When to do the update check
127+
128+
During the startup, `pwsh` creates a `Task` of the update check work,
129+
but delays the task run for 3 seconds by using `Task.Delay(3000)`.
130+
The typical startup time for `pwsh` with a moderate size profile should be less than 1 second.
131+
Given that, I think it's reasonable to delay the update check work for 3 seconds,
132+
so that it has close-to-zero impact on the startup performance.
133+
134+
#### How to persist information about a new version
135+
136+
The version of new release is persisted using a file,
137+
not as the file content, but instead baked in the file name in the following template,
138+
so that we can avoid extra file loading at the startup.
139+
140+
```none
141+
update<notification-type>_<version>_<publish-date>
142+
```
143+
144+
A separate file is used for each supported notification type,
145+
indicated by the integer value of the corresponding `NotificationType` member.
146+
For example,
147+
- when the notification type is `NotificationType.Default`,
148+
the file name would be like `update1_<version>_<publish-date>`.
149+
- when the notification type is `NotificationType.LTS`,
150+
the file name would be like `update2_<version>_<publish-date>`.
151+
152+
The file should be in a folder that is unique to the specific version of `pwsh`.
153+
For example, for the `v6.2.0 pwsh`, the folder `6.2.0` will be created in the `pwsh` cache folder (shown below),
154+
and the update check related files for that version of `pwsh` are put there exclusively.
155+
In this way, the update information for different versions of `pwsh` doesn't interfere with each other.
156+
157+
- Windows: `$env:LOCALAPPDATA\Microsoft\PowerShell\6.2.0`
158+
- Unix: `$env:HOME/.cache/powershell/6.2.0`
159+
160+
#### How to synchronize update checks
161+
162+
The most challenging part is to properly synchronize the update checks started from different `pwsh` processes,
163+
**so that for a specific version of `pwsh` and a specific notification type,
164+
only one update check task, at most, will run to complete per a day.**
165+
Other tasks should be able to detect "a check is in progress" or "the check has been done for today" and bail out early,
166+
to avoid any unnecessary network IO or CPU cycles.
167+
168+
For each notification type, we need two more files to achieve the synchronization,
169+
`"_sentinel<notification-type>_"` and `"sentinel<notification-type>-{year}-{month}-{day}.done"`.
170+
The `<notification-type>` part will be the integer value of the corresponding `NotificationType` member.
171+
The `{year}-{month}-{day}` part will be filled with the date of current day when the update check task starts to run,
172+
and they will be in the version folder too.
173+
174+
The file `"_sentinel<notification-type>_"` serves as a file lock among `pwsh` processes.
175+
The file `"sentinel<notification-type>-{year}-{month}-{day}.done"` serves as a flag that indicates a successful update check as been done for the day.
176+
Here are the sample code for doing this synchronization:
177+
178+
```c#
179+
const string TestDir = @"C:\arena\tmp\updatetest";
180+
const string SentinelFileName = "_sentinel1_";
181+
const string DoneFileNameTemplate = "sentinel1-{0}-{1}-{2}.done";
182+
183+
static void CheckForUpdate()
184+
{
185+
// Some pre-validation needs to happen to see if we need to do anything at all.
186+
// - If the current running `pwsh` is a self-built version, let's bail out early.
187+
// - Check if a file like `_update_<version>_<publish-date>` already exists.
188+
// If so, check the `publish-date` to see if it's still relatively new, say within a week.
189+
// If so, let's bail out early.
190+
191+
DateTime today = DateTime.UtcNow;
192+
string todayDoneFileName = string.Format(
193+
CultureInfo.InvariantCulture,
194+
DoneFileNameTemplate,
195+
today.Year.ToString(),
196+
today.Month.ToString(),
197+
today.Day.ToString());
198+
199+
string sentinelFilePath = Path.Combine(TestDir, SentinelFileName);
200+
string todayDoneFilePath = Path.Combine(TestDir, todayDoneFileName);
201+
202+
if (File.Exists(todayDoneFilePath))
203+
{
204+
// A successful update check has been done today,
205+
// so we can bail out early.
206+
return;
207+
}
208+
209+
try
210+
{
211+
// Use 'sentinelFilePath' as the file lock.
212+
// The update check tasks started by every 'pwsh' process will compete on holding this file.
213+
using (FileStream s = new FileStream(sentinelFilePath, FileMode.OpenOrCreate, FileAccess.Write, FileShare.None, bufferSize: 1, FileOptions.DeleteOnClose))
214+
{
215+
if (File.Exists(todayDoneFilePath))
216+
{
217+
// After grab the file lock, it turns out a successful check has finished.
218+
// Then let's bail out early.
219+
return;
220+
}
221+
222+
// Now it's guaranteed that I'm the only process that reaches here.
223+
foreach (string oldFile in Directory.EnumerateFiles(TestDir, "sentinel-*-*-*.done"))
224+
{
225+
// Clean up the old '.done' file, there should be only one.
226+
File.Delete(oldFile);
227+
}
228+
229+
// Do the real update check
230+
// - Send HTTP request to query for the new release/pre-release;
231+
// - If there is a valid new release that should be reported to the user, create the file `_update_<new-version>`,
232+
// or rename the existing `_update_<old-version>` to `_update_<new-version>`.
233+
// ... more ...
234+
235+
// Finally, create the `todayDoneFilePath` file as an indicator that a successful update check has finished today.
236+
new FileStream(todayDoneFilePath, FileMode.CreateNew, FileAccess.Write, FileShare.None).Close();
237+
}
238+
}
239+
catch (Exception e)
240+
{
241+
// An update check is in progress from another `pwsh` process. So it's OK to just return.
242+
}
243+
}
244+
```
245+
246+
> Note: `FileOptions.DeleteOnClose` is used when opening the sentinel file,
247+
so the sentinel file will be removed after being used as a lock.
248+
249+
With the file lock, only one process can get in the guarded `using` block at a given time.
250+
So only one process will be creating the file `_update_<version>_<publish-date>`, or renaming an old such file to reflect the new version.
251+
Yes, other processes could be looking at the old file name (when a `pwsh` session tries to print a notification),
252+
or working with an outdated file name (another update check tries to do a pre-validation).
253+
But it's fine for that to happen:
254+
255+
- In the former case, that particular `pwsh` session will show a notification about an outdated version,
256+
but the next `pwsh` session will show the right notification.
257+
- In the latter case, the other update check will continue and find the `.done` file already exists for today.
258+
259+
It's possible that a `pwsh` session terminates while the update check task is still running,
260+
in the middle of the `using` block for example.
261+
Creating the `.done` file is the very last step in the `using` block.
262+
So if the session ends before the `.done` file is created,
263+
another update check will happen when the next `pwsh` session starts and finish the work.
264+
265+
#### How to do the update check
266+
267+
This is comparatively the easy part.
268+
269+
- Determine the URL to use depending on the notification type.
270+
- For latest preview release-info: `https://<buildinfo-blob>/preview.json`
271+
- For latest stable release-info: `https://<buildinfo-blob>/stable.json`
272+
- For latest LTS release-info: `https://<buildinfo-blob>/lts.json`
273+
- Send HTTP query request and parse the response.
274+
- If there is a new update, create the file `update<notification-type>_<version>_<publish-date>` if one doesn't exists yet;
275+
or rename the existing file with the new version.
276+
277+
#### How to display the notification
278+
279+
`pwsh` checks to see if notification should be printed only if it's allowed to print the banner message and feature is not turned off.
280+
281+
- Run `Directory.EnumerateFiles` with the the version directory and the pattern `update<notification-type>_v*.*.*_????-??-??` to find such a file.
282+
- If a file path is returned, then get the version information from the file name.
283+
- Use that version to construct the notification message, including the URL to that GitHub release page.
284+
- The notification message is printed with the foreground and background colors inverted.
285+
286+
## Alternate Proposals and Considerations
287+
288+
When thinking about how to reduce unnecessary update checks,
289+
the first design I had was to depend on the `Day` of the month.
290+
So for instance, we can check for updates every 3 days by checking `DateTime.UtcNow.Day % 3 == 0`.
291+
But that means in the worst case, a user won't be notified of a new release until 3 days after the release.
292+
That makes this feature somewhat broken from the UX perspective.
293+
294+
Another design is to let all `pwsh`, including different versions, share the same update file,
295+
whose name contains both the latest stable release tag and latest preview release tag.
296+
When `pwsh` starts, it parses the file name, compare the latest stable/preview release version with its current version,
297+
and decides if a notification should be printed.
298+
This would reduce the number of helper files in the cache folder,
299+
however, with the cost of additional work at startup time for all versions of `pwsh`.
300+
Especially, for the latest stable or preview `pwsh` in use, it also needs to spend those extra cycles when it should not.
301+
Besides, I think having the helper files isolated in a version folder makes it flexible in case we need to make change to the design at a later time.
302+
303+
We may consider to add an API in PowerShell engine to check for updates in future,
304+
so that other hosts can offer similar features using the API.

0 commit comments

Comments
 (0)