|
| 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