პლაგინები C#-ში
როგორც წინა სტატიაში დაგპირდით ახლა გავარჩევთ პლაგინების სისტემის პატარა მაგალითს.
თავდაპირველად შევქმნათ პროექტი(class library) და დავარქვათ PluginInterface და მასში შევქმნათ ინტერფეისი PluginInterface :
using System;
namespace PluginInterface
{
public interface IMyPlugin
{
string Execute(Func<string> f);
}
}
სწორედ ამ ინტერფეისს მივცემთ ჩვენ დეველოპერებს, რომლებიც ჩვენი აპლიკაციისთვის დაწერენ პლაგინებს. მოცემულ ინტერფეისში არის მხოლოდ ერთი მეთოდის აღწერა, რომელიც იღებს დელეგატს და აბრუნებს სტრინგს.
ახლა დავწეროთ ორი პლაგინი. შევქმნათ ორი პროექტი(class library) Plugin1 და Plugin2, ორივეს დავუკავშიროთ(add reference) პროექტი PluginInterface და თითოეულში განვათავსოთ შემდეგი კოდი(პროექტის შესაბამისად შევცვალოთ სახელსივრცე და კლასი):
using System;
using PluginInterface;
namespace Plugin2
{
public class Plugin2 : MarshalByRefObject, IMyPlugin
{
public string Execute(Func<string> f)
{
return f();
}
}
}
როგორც ვხედავთ თითოელ პლაგინში ვაკეთებთ კლასს რომელიც არეალიზებს(implements) IMyPlugin-ს. შექმნილი ფუნქცია უბრალოდ ასრულებს პარამეტრად გადაცემულ დელეგატს და აბრუნებს მნიშვნელობას.
ახლა გადავიდეთ მთავარ ნაწილზე. შევქმნათ პროექტი(console app) CSharpPluginsExample.
ჩავთვალოთ რომ ჩვენ მიერ შექმნილი პლაგინები (dll - ები) მდებარეობს debug\plugins
საქაღალდეში.
ამოვიღოთ ამ საქაღალდიდან ყველა assembly-ის მისამართი:
var assemblyPaths = Directory.GetFiles(pluginsFolder, "*.dll").ToList();
ახლა თითოეული assembly ჩავტვირთოთ readonly რეჟიმში, შევამოწმოთ მისი თითოეული ტიპი, აკმაყოფილებს თუ არა ჩვენს მოთხოვნებს და თუ აკმაყოფილებს, appdomain-ში შევმქნათ მისი ეგზემპლარი და შევინახოთ სიაში.
foreach (var assembly in assemblyPaths.Select(Assembly.ReflectionOnlyLoadFrom))
{
foreach (var type in assembly.DefinedTypes)
{
if (IsValidType(type))
{
var newPlugin = appdomain.CreateInstanceFromAndUnwrap(assembly.Location, type.FullName) as IMyPlugin;
myPlugins.Add(newPlugin);
}
}
}
მეთოდი IsValidType
გამოიყურება ასე :
private static bool IsValidType(TypeInfo type)
{
return type.IsClass &&
!type.IsAbstract &&
type.ImplementedInterfaces.Any(i => i.GUID == typeof (IMyPlugin).GUID) &&
type.BaseType == typeof (MarshalByRefObject);
}
ეს მეთოდი არა იდეალური, თუმცა მაგალითისთვის გამოგვადგება. ამ მომენტისთვის ყველა პლაგინი უკვე ჩატვირთული გვაქვს აპლიკაციაში და შეგვიძლია გამოვიყენოთ :
myPlugins.ForEach(plugin =>
Console.WriteLine("is transparent proxy : " + RemotingServices.IsTransparentProxy(plugin) + ", assembly name : " +
plugin.Execute(() => Assembly.GetCallingAssembly().GetName().Name)));
ამ მონაკვეთში თითოეულ პლაგინს გადავცემთ დელეგატს, რომელსაც გამოიძახებენ პლაგინები და მივიღებთ თითოეული პლაგინის assembly-ის სახელს.
დავაკომპილიროთ კოდი, პლაგინების dll-ები გადავაკოპიროთ მთავარი პროექტის plugins
საქაღალდეში და გავუშვათ კოდი. აპლიკაცია გაეშვება, მაგრამ მივიღებთ System.Reflection.ReflectionTypeLoadException
-ს.
მარტივად რომ ავხსნათ, ეს იქსეფშენი ვარდება მაშინ როდესაც CLR ტვირთავს assembly-ის,
მაგრამ ვერ პოულობს ამ assembly-სთან დაკავშირებულ სხვა assembly-ს,
რომელშიც განსაზღვრულია ის ტიპი რომელსაც იყენებს უკვე ჩატვირთული assembly, ჩვენ კი გვჭირდება ამ ტიპის გამოყენება.
ჩვენს შემთხვევაში plugins საქაღალდეში არსებული პლაგინები
დაკავშირებულნი არიან PluginInterface-თან, მაგრამ CLR ვერ პოულობს ამ assembly-ის.
ამიტომ ასეთ შემთხვევვებში ცოტა “ხელი უნდა წავახმაროთ”” CLR-ს და
Appdomain.CurrentDomain.AssemblyResolve
- ივენთს (ჩვენს კონკრეტულ შემთხვევაში
AppDomain.CurrentDomain.ReflectionOnlyAssemblyResolve
-ს, რადგან assembly-ის ჩასატვირთად ვიყენებთ ReflectionOnlyLoadFrom
მეთოდს)
უნდა მივაბათ ჩვენი ფუნქცია(event handler) :
private static Assembly ReflectionOnlyAssemblyResolve(object sender, ResolveEventArgs args)
{
var strTempAssmbPath = "";
var objExecutingAssemblies = Assembly.GetExecutingAssembly();
var arrReferencedAssmbNames = objExecutingAssemblies.GetReferencedAssemblies();
if (arrReferencedAssmbNames.Any(
strAssmbName =>
strAssmbName.FullName.Substring(0, strAssmbName.FullName.IndexOf(",", StringComparison.Ordinal)) ==
args.Name.Substring(0, args.Name.IndexOf(",", StringComparison.Ordinal))))
{
strTempAssmbPath = Path.GetDirectoryName(objExecutingAssemblies.Location) + "\\" +
args.Name.Substring(0, args.Name.IndexOf(",", StringComparison.Ordinal)) + ".dll";
}
var myAssembly = Assembly.ReflectionOnlyLoadFrom(strTempAssmbPath);
return myAssembly;
}
მოცემული მეთოდი აპლიკაციის მეხსიერებაში არსებული assembly-ებში მოძებნის და დააბრუნებს მოთხოვნილ assembly-ის.
ახლა აპლიკაცია უკვე მზადაა. გავუშვათ და დავინახავთ შემდეგს :
is transparent proxy : True, assembly name : Plugin1
is transparent proxy : True, assembly name : Plugin2
მაგალითის მთლიანი კოდი შეგიძლიათ იხილოთ ლინკზე : https://github.com/tamazbagdavadze/CSharp-Plugins-example
ამ მარტივი აპლიკაციის განხილვის შემდეგ ალბათ ხვდებით რომ არაა რთული wpf-ში ან asp.net-ში, ან თუ თუნდაც სხვა რომელიმე ტექნოლოგიაში მარტივი პლაგინების სისტემის დაწერა, რომელიც დინამიურად ჩატვირთავს მესამე მხარის(3rd party) მიერ დამზადებულ პლაგინებს. თუმცა უნდა უნდა ავღნიშნოთ რომ ჩვენს მიერ განხილული მაგალითი არის ძალიან პრიმიტიული და აქვს ნაკლები:
-
ყველა პლაგინს ვტვირთავთ ერთ appdomain-ში, რაც გვაიძულებს საჭიროების შემთხვევაში ყველა პლაგინი ერთდროულად წავშალოთ, რადგანაც .net-ში შეუძლებელია ცალკეული assembly-ის წაშლა მეხსიერებიდან(unload).
-
ჩვენი აპლიკაცია გარკვეულწილად დაცულია, რადგან პლაგინები ჩატვირთულია სხვა appdomain-ში, მაგრამ ამ appdomain-ს არ აქვს შეზღუდული უფლებები, შეუძლია ფაილებთან, ქსელთან წვდომა და ა.შ.
უფლებების შესასღუდად საჭიროა შევმქნათ ერთგვარი Sandbox. ამისათვის უნდა გვქონდეს Strong-Named Assembly, ასევე შევქმნათ უფლებების სია და გადავცეთ appdomain-ს:
var setup = new AppDomainSetup { ApplicationBase = Path.GetFullPath(PathToUntrusted) };
var permSet = new PermissionSet(PermissionState.None);
permSet.AddPermission(new SecurityPermission(SecurityPermissionFlag.Execution));
permSet.AddPermission(new FileIOPermission(FileIOPermissionAccess.AllAccess,"D:\\temp\\"));
permSet.AddPermission(new ReflectionPermission(ReflectionPermissionFlag.MemberAccess));
var fullTrustAssembly = typeof(Sandboxer).Assembly.Evidence.GetHostEvidence<StrongName>();
var newDomain = AppDomain.CreateDomain("Sandbox", null, setup, permSet, fullTrustAssembly);
var handle = Activator.CreateInstanceFrom(
newDomain,
typeof(Sandboxer).Assembly.ManifestModule.FullyQualifiedName,
typeof(Sandboxer).FullName
);
var newDomainInstance = (Sandboxer)handle.Unwrap();
ამჯერად თემას მეტად აღარ ჩავუღრმავდებით, თუმცა თუ გაინტერესებთ დაწვრილებითი ინფორმაცია Sandbox-ზე, შეგიძლიათ გადახვიდეთ შემდედ ლინკზე How to: Run Partially Trusted Code in a Sandbox