पुनरावर्ती सभी संदर्भों के साथ AppDomain को असेंबली लोड करने के लिए कैसे?


113

मैं एक नई AppDomainकुछ असेंबली में लोड करना चाहता हूं जिसमें एक जटिल संदर्भ ट्री (MyDll.dll -> Microsoft.Office.Interop.Excel.dll -> Microsoft.Vbe.Interop.dll -> Office.dll -> stoleole.dll) है

जहां तक ​​मैंने समझा, जब एक विधानसभा को लोड किया जा रहा है AppDomain, तो इसके संदर्भ स्वचालित रूप से लोड नहीं होंगे, और मुझे उन्हें मैन्युअल रूप से लोड करना होगा। इसलिए जब मैं करता हूं:

string dir = @"SomePath"; // different from AppDomain.CurrentDomain.BaseDirectory
string path = System.IO.Path.Combine(dir, "MyDll.dll");

AppDomainSetup setup = AppDomain.CurrentDomain.SetupInformation;
setup.ApplicationBase = dir;
AppDomain domain = AppDomain.CreateDomain("SomeAppDomain", null, setup);

domain.Load(AssemblyName.GetAssemblyName(path));

और मिल गया FileNotFoundException:

फ़ाइल या असेंबली 'MyDll, संस्करण = 1.0.0.0, संस्कृति = तटस्थ, PublicKeyToken = null' या उसकी किसी एक निर्भरता को लोड नहीं किया जा सका। सिस्टम निर्दिष्ट फाइल का पता लगाने में नाकामयाब रहा।

मुझे लगता है कि प्रमुख हिस्सा इसकी निर्भरता में से एक है

ठीक है, मैं पहले करता हूँ domain.Load(AssemblyName.GetAssemblyName(path));

foreach (AssemblyName refAsmName in Assembly.ReflectionOnlyLoadFrom(path).GetReferencedAssemblies())
{
    domain.Load(refAsmName);
}

लेकिन FileNotFoundExceptionफिर से, दूसरे (संदर्भित) विधानसभा पर मिला ।

सभी संदर्भों को पुनरावर्ती रूप से कैसे लोड करें?

क्या मुझे रूट असेंबली लोड करने से पहले संदर्भ ट्री बनाना होगा? इसे लोड किए बिना विधानसभा के संदर्भ कैसे प्राप्त करें?


1
मैंने पहले भी कई बार इस तरह की असेंबली लोड की हैं, मैंने कभी भी इसे सभी संदर्भों को मैन्युअल रूप से लोड नहीं किया है। मुझे यकीन नहीं है कि इस सवाल का आधार सही है।
मिक

जवाबों:


68

CreateInstanceAndUnwrapविदेशी एप्लिकेशन डोमेन में आपकी प्रॉक्सी ऑब्जेक्ट निष्पादित होने से पहले आपको आह्वान करना होगा।

 class Program
{
    static void Main(string[] args)
    {
        AppDomainSetup domaininfo = new AppDomainSetup();
        domaininfo.ApplicationBase = System.Environment.CurrentDirectory;
        Evidence adevidence = AppDomain.CurrentDomain.Evidence;
        AppDomain domain = AppDomain.CreateDomain("MyDomain", adevidence, domaininfo);

        Type type = typeof(Proxy);
        var value = (Proxy)domain.CreateInstanceAndUnwrap(
            type.Assembly.FullName,
            type.FullName);

        var assembly = value.GetAssembly(args[0]);
        // AppDomain.Unload(domain);
    }
}

public class Proxy : MarshalByRefObject
{
    public Assembly GetAssembly(string assemblyPath)
    {
        try
        {
            return Assembly.LoadFile(assemblyPath);
        }
        catch (Exception)
        {
            return null;
            // throw new InvalidOperationException(ex);
        }
    }
}

इसके अलावा, ध्यान दें कि यदि आप उपयोग करते हैं LoadFromतो आपको एक FileNotFoundअपवाद मिलेगा क्योंकि विधानसभा रिज़ॉल्वर उस विधानसभा को खोजने का प्रयास करेगा जिसे आप GAC या वर्तमान एप्लिकेशन के बिन फ़ोल्डर में लोड कर रहे हैं। LoadFileइसके बजाय एक मनमाने ढंग से असेंबली फ़ाइल को लोड करने के लिए उपयोग करें - लेकिन ध्यान दें कि यदि आप ऐसा करते हैं तो आपको खुद पर निर्भरता को लोड करने की आवश्यकता होगी।


20
इस समस्या को हल करने के लिए मैंने जो कोड लिखा था, उसे देखें: github.com/jduv/AppDomainToolkit । विशेष रूप से, इस वर्ग में लोडअवेशनडविथराइटिंग्स विधि देखें: github.com/jduv/AppDomainToolkit/blob/master/AppDomainToolkit/…
जदयू

3
मैंने पाया है कि हालांकि यह ज्यादातर समय काम करता है , कुछ मामलों में आपको वास्तव AppDomain.CurrentDomain.AssemblyResolveमें इस हैंडबुक जवाब में वर्णित के रूप में घटना के लिए एक हैंडलर संलग्न करना होगा । मेरे मामले में, मैं MSTest के तहत चल रहे SpecRun परिनियोजन में हुक करने की कोशिश कर रहा था, लेकिन मुझे लगता है कि यह कई स्थितियों पर लागू होता है जिसमें आपका कोड "प्राइमरी" AppDomain - VS एक्सटेंशन, MSTest इत्यादि से नहीं चल सकता है
Aaronaught

आह दिलचस्प है। मैं उस पर गौर करूंगा और देखूंगा कि क्या मैं ADT के माध्यम से काम करना थोड़ा आसान बना सकता हूं। क्षमा करें कि कोड अब थोड़ी देर के लिए मृत हो गया है - हम सभी के पास दिन के काम हैं :)।
जदुव

अगर मैं कर सकता तो @Jduv आपकी टिप्पणी को 100 गुना बढ़ा देता। आपके पुस्तकालय ने मुझे MSBuild के तहत डायनेमिक असेंबली लोडिंग के साथ होने वाली एक प्रतीत होने वाली असाध्य समस्या को हल करने में मदद की। आपको इसे एक उत्तर में बढ़ावा देना चाहिए!
फिलिप डेनियल

2
@ जूदेव क्या आप सुनिश्चित हैं कि assemblyचर "मायडोमैन" से विधानसभा का संदर्भ लेंगे? मुझे लगता है कि var assembly = value.GetAssembly(args[0]);आपके द्वारा args[0]दोनों डोमेन में लोड किया assemblyजाएगा और चर मुख्य एप्लिकेशन डोमेन से कॉपी का संदर्भ देगा
इगोर बेंड्रुप

14

http://support.microsoft.com/kb/837908/en-us

C # संस्करण:

एक मॉडरेटर वर्ग बनाएं और इसे इनहेरिट करें MarshalByRefObject:

class ProxyDomain : MarshalByRefObject
{
    public Assembly GetAssembly(string assemblyPath)
    {
        try
        {
            return Assembly.LoadFrom(assemblyPath);
        }
        catch (Exception ex)
        {
            throw new InvalidOperationException(ex.Message);
        }
    }
}

ग्राहक साइट से कॉल करें

ProxyDomain pd = new ProxyDomain();
Assembly assembly = pd.GetAssembly(assemblyFilePath);

6
एक नया AppDomain बनाने के संदर्भ में इस समाधान को कैसे रखा गया है, क्या कोई समझा सकता है?
ट्राई क्यू ट्रान

2
A MarshalByRefObjectको अपॉइंटमेंट के आसपास से गुजारा जा सकता है। इसलिए मुझे लगता है कि Assembly.LoadFromएक नए एपडोमेन में असेंबली को लोड करने की कोशिश की जाती है, केवल तभी संभव है, अगर कॉलिंग ऑब्जेक्ट को उन एपडोमेंस के बीच पारित किया जा सकता है। इसे यहाँ वर्णित के रूप में रीमोटिंग
क्रिस्टोफ

32
यह काम नहीं करता है। यदि आप कोड निष्पादित करते हैं और AppDomain.CurrentDomain.GetAssemblies () की जाँच करते हैं, तो आप देखेंगे कि जिस लक्ष्य विधानसभा को आप लोड करने का प्रयास कर रहे हैं, वह वर्तमान एप्लिकेशन डोमेन में लोड है प्रॉक्सी नहीं है।
जुडव

41
यह पूरी बकवास है। MarshalByRefObjectजादुई रूप से इनहेरिट करने से यह हर दूसरे में लोड नहीं होता है AppDomain, यह सिर्फ .NET फ्रेमवर्क को सीरियलाइजेशन का उपयोग करने के बजाय एक पारदर्शी रीमोटिंग प्रॉक्सी बनाने के लिए कहता है जब आप एक से दूसरे AppDomainमें संदर्भ को अनप्लग करते हैं AppDomain(विशिष्ट तरीका CreateInstanceAndUnwrapविधि)। विश्वास नहीं कर सकता कि इस उत्तर में 30 से अधिक upvotes हैं; यहां कोडिंग कॉलिंग का एक बिल्कुल गोल चक्कर तरीका है Assembly.LoadFrom
हारून

1
हाँ, यह पूरी तरह बकवास लगता है, फिर भी इसमें 28 वोट हैं और यह उत्तर के रूप में चिह्नित है। लिंक किए गए लिंक में MarshalByRefObject का उल्लेख नहीं है। काफी विचित्र। अगर यह वास्तव में कुछ भी करता है तो मैं किसी को समझाने के लिए प्यार करूंगा
मिक

12

एक बार जब आप असेंबली इंस्टेंस को कॉलर डोमेन पर वापस भेज देते हैं, तो कॉलर डोमेन इसे लोड करने का प्रयास करेगा! यही कारण है कि आपको अपवाद मिलता है। आपके कोड की अंतिम पंक्ति में ऐसा होता है:

domain.Load(AssemblyName.GetAssemblyName(path));

इस प्रकार, जो कुछ भी आप विधानसभा के साथ करना चाहते हैं, वह एक प्रॉक्सी क्लास में किया जाना चाहिए - एक वर्ग जो कि मार्शलबायफ्रॉएड के उत्तराधिकारी है ।

ध्यान रखें कि कॉलर डोमेन और नए बनाए गए डोमेन दोनों को प्रॉक्सी क्लास असेंबली तक पहुंच होनी चाहिए। यदि आपका मुद्दा बहुत जटिल नहीं है, तो एप्लिकेशनबेस फ़ोल्डर को अपरिवर्तित छोड़ने पर विचार करें, इसलिए यह कॉलर डोमेन फ़ोल्डर के समान होगा (नया डोमेन केवल असेंबलियों को लोड करेगा जो इसकी आवश्यकता है)।

सरल कोड में:

public void DoStuffInOtherDomain()
{
    const string assemblyPath = @"[AsmPath]";
    var newDomain = AppDomain.CreateDomain("newDomain");
    var asmLoaderProxy = (ProxyDomain)newDomain.CreateInstanceAndUnwrap(Assembly.GetExecutingAssembly().FullName, typeof(ProxyDomain).FullName);

    asmLoaderProxy.GetAssembly(assemblyPath);
}

class ProxyDomain : MarshalByRefObject
{
    public void GetAssembly(string AssemblyPath)
    {
        try
        {
            Assembly.LoadFrom(AssemblyPath);
            //If you want to do anything further to that assembly, you need to do it here.
        }
        catch (Exception ex)
        {
            throw new InvalidOperationException(ex.Message, ex);
        }
    }
}

यदि आपको असेंबली को एक फ़ोल्डर से लोड करने की आवश्यकता है जो आपके वर्तमान ऐप डोमेन फ़ोल्डर से अलग है, तो विशिष्ट डीएलएस खोज पथ फ़ोल्डर के साथ नया ऐप डोमेन बनाएं।

उदाहरण के लिए, उपरोक्त कोड से एप्लिकेशन डोमेन निर्माण लाइन को इसके साथ प्रतिस्थापित किया जाना चाहिए:

var dllsSearchPath = @"[dlls search path for new app domain]";
AppDomain newDomain = AppDomain.CreateDomain("newDomain", new Evidence(), dllsSearchPath, "", true);

इस तरह, सभी dlls स्वचालित रूप से dllsSearchPath से हल हो जाएंगे।


मुझे प्रॉक्सी क्लास का उपयोग करके विधानसभा को लोड क्यों करना है? असेंबली का उपयोग करके इसे लोड करने की तुलना में क्या अंतर है। लॉडफ्रॉम (स्ट्रिंग)। मुझे सीएलआर के दृष्टिकोण से तकनीकी विवरण में दिलचस्पी है। यदि आप जवाब दे सकते हैं तो मैं बहुत आभारी रहूंगा।
डेनिस कसेल

नई असेंबली को अपने कॉलर डोमेन पर लोड होने से बचाने के लिए आप प्रॉक्सी क्लास का उपयोग करते हैं। यदि आप असेंबली का उपयोग करते हैं। LadFrom (स्ट्रिंग), कॉलर डोमेन नए असेंबली संदर्भों को लोड करने का प्रयास करेगा और उन्हें नहीं ढूंढेगा क्योंकि यह "[अस्मापथ]" में असेंबली की खोज नहीं करता है। ( msdn.microsoft.com/en-us/library/yx7xezcf%28v=vs.110%29.aspx )
Nir

11

अपने नए AppDomain पर, एक असेंबली बनाने का प्रयास करें । इवेंट हैंडलर को हल करें । उस घटना को कहा जाता है जब एक निर्भरता गायब होती है।


यह नहीं है वास्तव में, आप उस पंक्ति पर एक अपवाद प्राप्त करते हैं जिसे आप इस घटना को नए AppDomain पर पंजीकृत कर रहे हैं। आप इस घटना को वर्तमान AppDomain पर पंजीकृत कर सकते हैं।
user1004959

ऐसा लगता है कि वर्ग MarshalByRefObject से विरासत में मिला है। ऐसा नहीं है कि वर्ग को केवल [सीरियल] विशेषता के साथ चिह्नित किया गया है।
user2126375

5

आपको AppDomain.AssemblyResolve या AppDomain.ReflectionOnlyAssemblyResolve इवेंट्स (इस पर निर्भर करता है कि कौन सा लोड आप कर रहे हैं) के मामले में संदर्भित असेंबली GAC या CLR की जांच पथ पर नहीं है।

AppDomain.AssemblyResolve

AppDomain.ReflectionOnlyAssemblyResolve


इसलिए मुझे अनुरोधित विधानसभा को मैन्युअल रूप से इंगित करना होगा? यहां तक ​​कि यह नए AppDomain के AppBase में है? क्या ऐसा करने का कोई तरीका नहीं है?
abatishchev

5

मुझे @ user1996230 के उत्तर को समझने में थोड़ा समय लगा इसलिए मैंने और अधिक स्पष्ट उदाहरण प्रदान करने का निर्णय लिया। नीचे दिए गए उदाहरण में मैं किसी अन्य AppDomain में लोड की गई वस्तु के लिए एक प्रॉक्सी बनाता हूं और किसी अन्य डोमेन से उस ऑब्जेक्ट पर एक विधि कहता हूं।

class ProxyObject : MarshalByRefObject
{
    private Type _type;
    private Object _object;

    public void InstantiateObject(string AssemblyPath, string typeName, object[] args)
    {
        assembly = Assembly.LoadFrom(AppDomain.CurrentDomain.BaseDirectory + AssemblyPath); //LoadFrom loads dependent DLLs (assuming they are in the app domain's base directory
        _type = assembly.GetType(typeName);
        _object = Activator.CreateInstance(_type, args); ;
    }

    public void InvokeMethod(string methodName, object[] args)
    {
        var methodinfo = _type.GetMethod(methodName);
        methodinfo.Invoke(_object, args);
    }
}

static void Main(string[] args)
{
    AppDomainSetup setup = new AppDomainSetup();
    setup.ApplicationBase = @"SomePathWithDLLs";
    AppDomain domain = AppDomain.CreateDomain("MyDomain", null, setup);
    ProxyObject proxyObject = (ProxyObject)domain.CreateInstanceFromAndUnwrap(typeof(ProxyObject).Assembly.Location,"ProxyObject");
    proxyObject.InstantiateObject("SomeDLL","SomeType", new object[] { "someArgs});
    proxyObject.InvokeMethod("foo",new object[] { "bar"});
}

कोड में कुछ छोटे टाइपो, और मुझे मानना ​​होगा कि मुझे विश्वास नहीं था कि यह काम करेगा, लेकिन यह मेरे लिए एक जीवन रक्षक था। अनेक अनेक धन्यवाद।
ओवेन आइवरी

4

कुंजी AppDomain द्वारा उठाए गए असेंबलीResolve इवेंट है।

[STAThread]
static void Main(string[] args)
{
    fileDialog.ShowDialog();
    string fileName = fileDialog.FileName;
    if (string.IsNullOrEmpty(fileName) == false)
    {
        AppDomain.CurrentDomain.AssemblyResolve += CurrentDomain_AssemblyResolve;
        if (Directory.Exists(@"c:\Provisioning\") == false)
            Directory.CreateDirectory(@"c:\Provisioning\");

        assemblyDirectory = Path.GetDirectoryName(fileName);
        Assembly loadedAssembly = Assembly.LoadFile(fileName);

        List<Type> assemblyTypes = loadedAssembly.GetTypes().ToList<Type>();

        foreach (var type in assemblyTypes)
        {
            if (type.IsInterface == false)
            {
                StreamWriter jsonFile = File.CreateText(string.Format(@"c:\Provisioning\{0}.json", type.Name));
                JavaScriptSerializer serializer = new JavaScriptSerializer();
                jsonFile.WriteLine(serializer.Serialize(Activator.CreateInstance(type)));
                jsonFile.Close();
            }
        }
    }
}

static Assembly CurrentDomain_AssemblyResolve(object sender, ResolveEventArgs args)
{
    string[] tokens = args.Name.Split(",".ToCharArray());
    System.Diagnostics.Debug.WriteLine("Resolving : " + args.Name);
    return Assembly.LoadFile(Path.Combine(new string[]{assemblyDirectory,tokens[0]+ ".dll"}));
}

0

मुझे कई बार ऐसा करना पड़ा है और कई अलग-अलग समाधानों पर शोध किया है।

समाधान जो मुझे सबसे सुरुचिपूर्ण और आसानी से पूरा होता है उसे इस तरह से लागू किया जा सकता है।

1. एक प्रोजेक्ट बनाएं जिसे आप एक सरल इंटरफ़ेस बना सकते हैं

इंटरफ़ेस में आपके द्वारा कॉल किए जाने वाले किसी भी सदस्य के हस्ताक्षर होंगे।

public interface IExampleProxy
{
    string HelloWorld( string name );
}

इस परियोजना को साफ और लाइट रखना महत्वपूर्ण है। यह एक ऐसी परियोजना है, जो दोनों AppDomainको संदर्भित कर सकती है और हमें Assemblyहमारे ग्राहक सभा से अलग डोमेन में लोड करने की इच्छा का संदर्भ नहीं देने की अनुमति देगी ।

2. अब वह प्रोजेक्ट बनाएं जिसमें वह कोड हो जिसे आप अलग करना चाहते हैं AppDomain

क्लाइंट प्रेज के साथ यह परियोजना प्रॉक्सी प्रोज को संदर्भित करेगी और आप इंटरफ़ेस को लागू करेंगे।

public interface Example : MarshalByRefObject, IExampleProxy
{
    public string HelloWorld( string name )
    {
        return $"Hello '{ name }'";
    }
}

3. अगला, क्लाइंट प्रोजेक्ट में, कोड को दूसरे में लोड करें AppDomain

तो, अब हम एक नया बनाते हैं AppDomain। विधानसभा संदर्भों के लिए आधार स्थान निर्दिष्ट कर सकते हैं। जांच GAC में और वर्तमान निर्देशिका और AppDomainबेस लोके में निर्भर असेंबलियों के लिए जाँच करेगा ।

// set up domain and create
AppDomainSetup domaininfo = new AppDomainSetup
{
    ApplicationBase = System.Environment.CurrentDirectory
};

Evidence adevidence = AppDomain.CurrentDomain.Evidence;

AppDomain exampleDomain = AppDomain.CreateDomain("Example", adevidence, domaininfo);

// assembly ant data names
var assemblyName = "<AssemblyName>, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null|<keyIfSigned>";
var exampleTypeName = "Example";

// Optional - get a reflection only assembly type reference
var @type = Assembly.ReflectionOnlyLoad( assemblyName ).GetType( exampleTypeName ); 

// create a instance of the `Example` and assign to proxy type variable
IExampleProxy proxy= ( IExampleProxy )exampleDomain.CreateInstanceAndUnwrap( assemblyName, exampleTypeName );

// Optional - if you got a type ref
IExampleProxy proxy= ( IExampleProxy )exampleDomain.CreateInstanceAndUnwrap( @type.Assembly.Name, @type.Name );    

// call any members you wish
var stringFromOtherAd = proxy.HelloWorld( "Tommy" );

// unload the `AppDomain`
AppDomain.Unload( exampleDomain );

यदि आप की जरूरत है, वहाँ एक विधानसभा लोड करने के लिए विभिन्न तरीकों की एक टन कर रहे हैं। आप इस समाधान के साथ एक अलग तरीके का उपयोग कर सकते हैं। यदि आपके पास असेंबली योग्य नाम है, तो मैं इसका उपयोग करना पसंद करता हूं CreateInstanceAndUnwrapक्योंकि यह असेंबली बाइट्स को लोड करता है और फिर आपके लिए अपने प्रकार को इंस्टेंट करता है और रिटर्न करता है objectकि आप अपने प्रॉक्सी प्रकार को सरल कास्ट कर सकते हैं या यदि आप दृढ़ता से टाइप किए गए कोड में नहीं हैं तो आप कर सकते हैं डायनेमिक लैंग्वेज रनटाइम का उपयोग करें और dynamicटाइप किए गए वेरिएबल को दिए गए ऑब्जेक्ट को असाइन करें और फिर सीधे उस पर सदस्यों को कॉल करें।

ये लो।

यह एक असेंबली को लोड करने की अनुमति देता है जो आपके क्लाइंट प्रॉजे नॉट के पास एक अलग में संदर्भ है AppDomain और क्लाइंट से उस पर कॉल सदस्य हैं।

परीक्षण करने के लिए, मुझे Visual Studio में मॉड्यूल विंडो का उपयोग करना पसंद है। यह आपको अपना क्लाइंट असेंबली डोमेन दिखाएगा और उस डोमेन में सभी मॉड्यूल लोड किए गए हैं और साथ ही आपके नए ऐप डोमेन और उस डोमेन में क्या असेंबली या मॉड्यूल लोड किए गए हैं।

कुंजी या तो यह सुनिश्चित करने के लिए है कि आप कोड या तो व्युत्पन्न करते हैं MarshalByRefObjectया क्रमबद्ध हैं।

`MarshalByRefObject आपको डोमेन के जीवनकाल को इसके अंदर कॉन्फ़िगर करने की अनुमति देगा। उदाहरण के लिए, यदि आप चाहते हैं कि डोमेन को नष्ट कर दिया जाए तो प्रॉक्सी को 20 मिनट में कॉल नहीं किया जाएगा।

आशा है कि ये आपकी मदद करेगा।


नमस्ते, अगर मुझे सही ढंग से याद है, तो मुख्य मुद्दा यह था कि सभी निर्भरता को कैसे पुन: लोड किया जाए, इसलिए प्रश्न। कृपया एक प्रकार की श्रेणी, Foo, FooAssemblyजिसमें Bar, BarAssemblyकुल 3 विधानसभाओं की संपत्ति है , को वापस करने के लिए HelloWorld को बदलकर अपने कोड का परीक्षण करें । क्या यह काम करना जारी रखेगा?
abatishchev

हां, विधानसभा परिवीक्षा चरण में उचित निर्देशिका की आवश्यकता है। AppDomain में एक ApplicationBase है, हालांकि, मैंने इसका परीक्षण नहीं किया। फ़ाइलों को कॉन्फ़िगर भी कर सकते हैं आप विधानसभा जांच निर्देशिकाओं को निर्दिष्ट कर सकते हैं जैसे कि app.config जो एक dll का उपयोग कर सकता है और साथ ही गुणों में कॉपी करने के लिए सेट किया गया है। इसके अलावा, यदि आपके पास अलग-अलग ऐप डोमेन में लोड होने के इच्छुक विधानसभा की इमारत पर नियंत्रण है, तो संदर्भ एक हिंटपाथ प्राप्त कर सकते हैं जो इसे देखने के लिए निर्दिष्ट थे। अगर वह सब विफल हो जाता है तो मैं नए AppDomains असेंबली की सदस्यता लेने का नतीजा लूंगा। घटना को मैन्युअल रूप से और विधानसभाओं को लोड करना होगा। उदाहरण के लिए।
सिम्पर टीटी
हमारी साइट का प्रयोग करके, आप स्वीकार करते हैं कि आपने हमारी Cookie Policy और निजता नीति को पढ़ और समझा लिया है।
Licensed under cc by-sa 3.0 with attribution required.