-
Notifications
You must be signed in to change notification settings - Fork 0
Isolation
this is a 1.2 feature and is not implemented yet
In shell-based scenarios, everything is easy. A process is loaded, executes a bunch of code, and shuts down, leaving nothing behind.
One of the challenges of supporting long-running clients (such as Visual Studio) comes from how .net assembly loading itself works, combined with the by-project installation of packages.
The typical scenario is the following. I load Visual Studio, open a solution containing OpenWrap 1.0. As part of a first build, openwrap.dll is loaded in the default appdomain for .net code (where the Visual Studio MSBuild engines are executed, the same appdomain in which .net add-ins are loaded).
When I close the solution, Visual Studio doesn't release the lock on the msbuild files, or the assemblies that get loaded. If I open a new solution that depends on OpenWrap 1.1 in the same instance, the new assembly cannot be loaded (OpenWrap.dll has already been loaded).
Each integration point needs a different solution to the various problems caused by this.
If Visual Studio is running on a project, and OpenWrap is updated, because VS keeps a lock on the various files, an update to the anchored folder for the package would be impossible.
In order to stop that locking from happening in anchored content (which needs to be updated whenever a new package is updated), the simplest solution is to stop relying on an anchored directory alltogether.
As a post-install hook, the installation of an OpenWrap package could create a proxy assembly to the /wraps folder, containing a shim of the msbuild tasks with the same name and namespace as the actual package, that would redirect the calls to the correct package automatically. Project files would then reference that shim dll instead of the main build one.
For example, on a fresh project, 'add-wrap openwrap' would copy openwrap.msbuild-1.0.0.123456.dll and openwrap.*.targets to /wraps, a shim generated at install-time. The shim itself would not redirect the call directly, but would load a separate appdomain for that specific project, load the openwrap dlls in there, and communicate inputs and outputs of msbuild tasks through a simple key/value pairs dictionary serialized between the two appdomains.
Whenever an update occurs, or a new solution file is loaded, the target files importing the types would be updated, and each of the csproj files would be touched to force a reload. Upon unloading, the versioned appdomain is shut down and a new one is created for the version under use.
Some solution hooks present an extra challenge, because they need to live in the same appdomain as the plugin they're patching. For example, the resharper integration requires calling Resharper within the default AppDomain. Shimming and redirecting would not be possible because those add-ins pull-in object instances from the add-in itself, which cannot be shimed, as we don't control both sides. As we cannot cross an AppDomain boundary for this, we need another way to enable a different version loading.
In such a scenario, the assembly containing the solution hooks ought to be copied to a temporary location (shadow copied), and strong-named on the fly with a version that matches the package version it comes from. When the solution unloads or the package gets updated, the old version unregisters itself from anything it has registered to, the new version is copied and strong-named, and loaded in the AppDomain.
This does mean that the whole assembly dependency tree for that specific assembly needs to be strong-named as well. Indeed, there is the possibility that both a project package and a system package rely on different versions of the same package, and need to run side-by-side to function properly.
The caveat here of course is that the assembly itself will never get unloaded, but as long as the code is not in use anymore, all ought to be fine.
In the case of DTE and Visual Studio extensions consumers, we don't have a problem as much, as the object model being pulled in is COM, and happily supports multiple consumers living in separate AppDomains.
The solution is then to keep one AppDomain for DTE consumers, per OpenWrap project. Whenever an update occurs (or a new solution is opened), the previous AppDomain gets shut down and a new AppDomain is loaded with the new dependency tree.
For each of the system packages that require loading, we do the same, except we create one AppDomain per system package requiring consumption. Whenever a system package gets updated, the appdomain gets shut down and a new one gets laoded
UI commands are commands that are registered in an IDE for execution from within the IDE shell. In the case of VS, this is done by registering VS commands for each of the UI commands, and executing the command upon invocation.
In this case, the same solution as msbuild tasks can be applied. One appdomain is created for the current solution, as well as one appdomain per system package containing ui commands, with the former being reloaded when project dependencies change.
This leaves the case of the loader that manages all these types. Upon loading a solution, the current version of openwrap contains that loader, and will be loaded in its own versioned AppDomain. Through the use of mscoree, the loader will be able to manage the various appdomains in existence, as well as the default appdomain code.
In the case where the current openwrap version changes, the loader will have to be unlaoded and reloaded. In order for the list of managed appdomains to be managed by the new loader, data about each will be kept in the default AppDomain data.