Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: revise vscode extension directories #13178

Merged
merged 3 commits into from
Dec 21, 2023

Conversation

xai
Copy link
Contributor

@xai xai commented Dec 15, 2023

What it does

This patch eliminates the use of the temporary directories vscode-unpacked and vscode-copied for installing and deploying vscode extensions. The file-handler and directory handler for vscode extensions have been adjusted accordingly:

  1. A temporary directory is now only used for downloading extensions from the registry. This temporary directory is now user-specific and resides within the configdir (i.e., per default in the user's home: /.theia/tmp/) to avoid clashes and permission issues on multi-user operating systems that share temporary directories, such as Linux or BSDs. Having this temporary directory in a location that is configurable by the user also seems the more sensible approach when extensions are considered confidential data.
  2. $configDir/deployedPlugins replaces our volatile /tmp/vscode-copied deployment directory. Having a more permanent way of handling installed extensions should improve startup time and reduce issues with multiple instances running in parallel.
  3. The local file resolver unpacks the vsix file from the temp dir into $configDir/deployedPlugins/<extension-id>.
  4. The simplified directory handler loads unpacked extensions directly from $configDir/deployedPlugins/<extension-id>.
  5. We use $configDir/extensions as a location for the user to drop vsix files that will be installed to the deployment location automatically on startup. We do not manage or remove files within $configDir/extensions.

Overall, this should improve the stability on systems with shared temp dir locations and reduce the startup of the first application start after a reboot.

Fixes #12757

Contributed on behalf of STMicroelectronics

How to test

Installing from registry

  1. Start Theia
  2. Use the vsix registry to install an extension
  3. Verify that the extension is loaded and its functionality is enabled.
  4. Verify that the extension is permanently stored unpacked in $configDir/deployedPlugins/.
  5. Try starting Theia again to verify the extension gets correctly loaded
  6. Remove the extension (this should indicate that a reload of the window is required), and verify that the extension is no longer active
  7. Verify that $configDir/extensions/ has been removed

Installing a local vsix extensions

  1. Start Theia
  2. Select Install from VSIX in the triple-dot menu of the extensions view
  3. Browse to a local .vsix file and install ut
  4. Verify that the extension is permanently stored unpacked in $configDir/deployedPlugins/.
  5. Remove the extension (this should indicate that a reload of the window is required), and verify that the extension is no longer active
  6. Verify that $configDir/extensions/ has been removed

Store a vsix file in the drop-in location

  1. Put a .vsix file into $configDir/extensions
  2. Start Theia
  3. Verify that the extension is permanently stored unpacked in $configDir/deployedPlugins/.
  4. Removing the extension will cause it to be reinstalled until you remove it from the drop-in location

Follow-ups

Review checklist

Reminder for reviewers

@xai xai force-pushed the issues/12757 branch 2 times, most recently from 84ecd86 to 2850ea3 Compare December 15, 2023 13:37
@tsmaeder
Copy link
Contributor

I have a couple of general comments:

  1. We should not download extensions to a shared directory. I have heard of use cases where even the existence of certain extensions may be a trade secret. Isn't there a user-specific tmp on linux?
  2. We should never move or delete files that are managed by the user. So if the user puts a *.vsix into .theia/extensions, we should not move nor delete it from there.
  3. If we download *.vsix files as a side-effect of an installation, we should clean up those files once we no longer need them. Relying on the OS or user to clean up files created by software is bad practice, IMO.
  4. I think it would be a good idea to separate "drop in locations" and "deployment locations". Any given directory should be one or the other. If an application does not have the ability to install additional plugins, we should never deal with drop-in locations. Concretely, this would mean that we have .theia/extensions as a drop-in location where the user can put extensions and .theia/deployedPlugins where stuff is extracted to.

@xai
Copy link
Contributor Author

xai commented Dec 15, 2023

Thanks, @tsmaeder, for the quick response!

1. We should not download extensions to a shared directory. I have heard of use cases where even the existence of certain extensions may be a trade secret. Isn't there a user-specific tmp on linux?

I don't think there is a consistent user-specific temp dir in Linux. According to man 3 tmpfile:

The  standard  does  not  specify  the  directory  that  tmpfile()  will  use.  Glibc will try the path prefix P_tmpdir defined in <stdio.h>, and if that fails the directory /tmp.

The shared /tmp is often also on a different filesystem/mountpoint than the user home. In several scenarios, the user home will be better secured (e.g., encrypted with user-specific credentials, mounted after kerberos auth, ...) than the shared temp dir location.

In our use case, the download directory is meant to be user-specific (that's why I encoded the username into it), but sure, the default permissions will most likely be world-readable (755) if we do not change them. So I see two options:

  1. Make sure the permission of /tmp/vscode-download-$username is 700
  2. Download to a path within the user home, such as .theia/tmp/download

I think if we want to treat extensions as potential secrets, then the second approach is more sensible, considering the user home to be a much safer place than /tmp. Of course, this not being a temp dir that the OS handles, we have to clean it up ourselves.
So what do you think about <userhome>/.theia/tmp/download-<randomuuid/processid>?

2. We should never move or delete files that are managed by the user. So if the user puts a *.vsix into `.theia/extensions`, we should not move nor delete it from there.

ACK.

3. If we download *.vsix files as a side-effect of an installation, we should clean up those files once we no longer need them. Relying on the OS or user to clean up files created by software is bad practice, IMO.

That makes sense, especially if we decide to use a location within the user's home. We have to be careful here with regard to robustness in case of multiple Theia processes (run by the same user). So, cleaning up after successful unpacking is easy enough. What do we do if the application gets killed hard by the user or the OOM killer and our cleanup is not finished? Should we try to clean at the application start? Then we have to make sure not to remove stuff that another running Theia process is currently using.

4. I think it would be a good idea to separate "drop in locations" and "deployment locations". Any given directory should be one or the other. If an application does not have the ability to install additional plugins, we should never deal with drop-in locations. Concretely, this would mean that we have `.theia/extensions` as a drop-in location where the user can put extensions and `.theia/deployedPlugins` where stuff is extracted to.

Makes sense; this is backward-compatible and avoids unwanted interactions between our unpacking/deployment and the user. I'll revise the code accordingly.

@xai
Copy link
Contributor Author

xai commented Dec 15, 2023

I revised my PR to use

  • ~/.theia/deployedPlugins/ as a permanent location for installed extensions. This replaces the volatile /tmp/vscode-copied
  • ~/.theia/extensions/ as a drop-in location that the user manages (we do not touch anything there) and that only serves as a source of extensions that will be installed on startup
  • ~/.theia/tmp/ as a replacement for the remaining shared temp directories. It can be used to download .vsix files from the registry (they do not belong in ~/.theia/extensions`, as we no longer manage this directory).

Copy link
Contributor

@tsmaeder tsmaeder left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mostly comments, but I think we should fix the "move" logic.

if (!this._tmpDirUri) {
const configDir: URI = new URI(await this.environments.getConfigDirUri());
const processId: string = process.pid.toString();
this._tmpDirUri = configDir.resolve('tmp').resolve(`${prefix ?? 'tmp-'}-${processId}`);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll try.

console.log(`[${pluginResolverContext.getOriginId()}]: trying to decompress "${localPath}" into "${tempUnpackDir}"...`);
if (await fs.pathExists(tempUnpackDir)) {
console.log(`[${pluginResolverContext.getOriginId()}]: Removing existing target dir "${tempUnpackDir}"...`);
fs.removeSync(tempUnpackDir);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this sync?

} catch (e) {
console.warn(`Problem copying plugin at ${localPath}:`, e);
await fs.ensureDir(tempUnpackDir);
if (!await decompressExtension(localPath, tempUnpackDir)) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The path of the vsix and the destination dir are even more important in the case of failure.

}
} catch (e) {
console.error(`[${pluginResolverContext.getOriginId()}]: error while decompressing`, e);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again: log the relevant paths.

return;
}
console.log(`[${pluginResolverContext.getOriginId()}]: moving to extension dir "${extensionDeploymentDir}"...`);
await fs.move(tempUnpackDir, extensionDeploymentDir, { overwrite: true });
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we should use fs.move here: we're trying to do this replacement atomically (either completely succeed or fail). The safe way to do this is by using rename. But that only works when the source and destination are on the same device. So IMO, we should to this:

  1. Unpack to a tmp-subdir inside extensionDeploymentDir.
  2. If the destination folder already exists, rename it to something else
  3. If that works, rename the tmp folder to the destination folder, otherwise rename the original folder back and fail, cleaning up any new files.
  4. Remove the pre-existing folder, if it exists

}
return true;
} catch (error) {
console.error(`Failed to decompress ${sourcePath} to ${destPath}: ${error}`);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are we not just letting the exception propagate?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, stupid mistake.


console.log(`[${id}]: decompressed`);
} catch (error) {
console.error(`[${id}]: error while decompressing`, error);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not let the error propagate? How is the invoker going to tell sometihing failed?

const extensionDeploymentDirPath = FileUri.fsPath(deployedPluginsDirUri.resolve(pluginId));
return extensionDeploymentDirPath;
} catch (error) {
console.error('Error resolving extension directory:', error);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the error is unhandled, we'll get a stack trace. If it isn't shouldn't the handler print the error?

@tsmaeder
Copy link
Contributor

Some notes from testing:

  1. Dropping the "vscode-drawio" plugin into the "extensions" folder inside the ~/.theia folder blocks the UI at startup for 20 seconds with a white screen. When we install extensions via the UI, the extensions can startup after being installed, so there's no reason to block while we wait for the plugins to install.
    This happens on every startup, not just the first.
  2. After startup, we end up with an "uninstall" button for the extesnsion in the plugins view. When you click it, the extension is uninstalled. However, the extension is reinstalled the next time we start Theia. Maybe we should consider extensions installed via "drop-in" folder as "built-in" and the way to remove them would be to remove the *.vsix from the drop-in folder?

I can kind of live with the second behavior, but Nr. 1 is a must-fix, IMO.

@xai xai force-pushed the issues/12757 branch 2 times, most recently from 9cef267 to 87ee02b Compare December 18, 2023 21:14
@xai
Copy link
Contributor Author

xai commented Dec 18, 2023

1. Dropping the "vscode-drawio" plugin into the "extensions" folder inside the `~/.theia` folder blocks the UI at startup for 20 seconds with a white screen. When we install extensions via the UI, the extensions can startup after being installed, so there's no reason to block while we wait for the plugins to install.
   This happens on **every** startup, not just the first.

It should be fixed now. I reverted back to using the filename of a dropped vsix file as extension-id instead of always unpacking and parsing package.json. Would be nicer to have the correct extension id, but we can deal with this in a future PR, I'd be happy to look into this.

2. After startup, we end up with an "uninstall" button for the extesnsion in the plugins view. When you click it, the extension is uninstalled. However, the extension is reinstalled the next time we start Theia. Maybe we should consider extensions installed via "drop-in" folder as "built-in" and the way to remove them would be to remove the *.vsix from the drop-in folder?

Treating them as "built-in" would be in line with the user being the only one who manages them. This would probably also fit well with having a lookup mechanism for extensions because then we could track the source of deployed extensions (e.g., downloaded, dropped-in, installed-from-file) and treat them accordingly.

I can kind of live with the second behavior, but Nr. 1 is a must-fix, IMO.

Nr. 1 should be done now. Nr. 2 is definitely a good candidate for a follow-up ticket.

@xai xai requested a review from tsmaeder December 18, 2023 21:21
@xai xai changed the title fix: use path in user home as extension dir fix: revise vscode extension directories Dec 18, 2023
@xai xai marked this pull request as ready for review December 18, 2023 21:26
Copy link
Contributor

@tsmaeder tsmaeder left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code looks good to me now, but I still get a 20s pause with a blank window the first time I drop the draw.io vsix into the .theia/extensions folder.

@tsmaeder
Copy link
Contributor

@xai we have a conflict.

xai and others added 3 commits December 21, 2023 10:33
This patch eliminates the use of the temporary directories
vscode-unpacked and vscode-copied for installing and deploying
vscode extensions.

The file-handler and directory handler for vscode extensions
have been adjusted accordingly:

* A temporary directory is now only used for downloading extensions from
  the registry. This temporary directory is now user-specific and
  resides within the configdir (i.e., per default in the user's home:
  /.theia/tmp/) to avoid clashes and permission issues on multi-user
  operating systems that share temporary directories, such as Linux or
  BSDs. Having this temporary directory in a location that is
  configurable by the user also seems the more sensible approach when
  extensions are considered confidential data.

* $configDir/deployedPlugins replaces our volatile /tmp/vscode-copied
  deployment directory. Having a more permanent way of handling
  installed extensions should improve startup time and reduce issues
  with multiple instances running in parallel.

* The local file resolver unpacks the vsix file from the temp dir into
  $configDir/deployedPlugins/<extension-id>.

* The simplified directory handler loads unpacked extensions directly
  from $configDir/deployedPlugins/<extension-id>.

* We use $configDir/extensions as a location for the user to drop vsix
  files that will be installed to the deployment location automatically
  on startup. We do not manage or remove files within
  $configDir/extensions.

Overall, this should improve the stability on systems with shared temp
dir locations and reduce the startup of the first application start
after a reboot.

Contributed on behalf of STMicroelectronics

Signed-off-by: Olaf Lessenich <[email protected]>
To avoid delaying the application startup while loading plugins,
the initialize function of the PluginDeployerContribution class
explicitly returns a resolved Promise.

Signed-off-by: Olaf Lessenich <[email protected]>
Co-authored-by: Thomas Mäder <[email protected]>
Contributed on behalf of STMicroelectronics

Signed-off-by: Olaf Lessenich <[email protected]>
@xai
Copy link
Contributor Author

xai commented Dec 21, 2023

@xai we have a conflict.

Resolved.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
Archived in project
Development

Successfully merging this pull request may close these issues.

Plugin cache prevents vscode extension install
3 participants