I have a C# console program that uses the nuget plugin SocketIO4Net
When I build the exe and move it to my Windows 2008 server, it doesn't work, whereas on my local machine, it works.
Unhandled Exception: System.IO.FileNotFoundException: Could not load file or assembly 'SocketIOClient, Version=0.6.26.0, Culture=neutral, PublicKeyToken=null' or one of its dependencies. The system cannot find the file specified.
Is there any way I can bake all my dependencies into the exe?
I tried doing:
AppDomain.CurrentDomain.AssemblyResolve += (sender, args) =>
{
var resName = "converter.SocketIOClient.dll";
var thisAssembly = Assembly.GetExecutingAssembly();
using (var input = thisAssembly.GetManifestResourceStream(resName))
{
return input != null
? Assembly.Load(StreamToBytes(input))
: null;
}
};
But that didn't work. Perhaps I'm getting the resourceName wrong?
Here is my example which is based off of Embedding one dll inside another as an embedded resource and then calling it from my code but has some helpful screenshots.
using System;
using System.IO;
using System.Reflection;
using System.Windows.Forms;
using MyEmbbedFile;
namespace ProjectNameSpace
{
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
AppDomain.CurrentDomain.AssemblyResolve += (sender, args) =>
{
var resName = "ProjectNameSpace.MyEmbbedFile.dll";
var thisAssembly = Assembly.GetExecutingAssembly();
using (var input = thisAssembly.GetManifestResourceStream(resName))
{
return input != null
? Assembly.Load(StreamToBytes(input))
: null;
}
};
}
private void button1_Click(object sender, EventArgs e)
{
MyEmbbedFileApp app = new MyEmbbedFileApp();
app.DoStuff();
}
private static byte[] StreamToBytes(Stream input)
{
var capacity = input.CanSeek ? (int)input.Length : 0;
using (var output = new MemoryStream(capacity))
{
int readLength;
var buffer = new byte[4096];
do
{
readLength = input.Read(buffer, 0, buffer.Length);
output.Write(buffer, 0, readLength);
}
while (readLength != 0);
return output.ToArray();
}
}
}
}
There are 2 other things you will need to do:
You will still need to make sure you add your assembly as a reference so your code compiles. Just make sure it does not copy to the output directory.
The second thing you need to do is add your reference to the project as a normal file. Then set it's build action to Embedded Resource under properties.
Yes.
Use AppDomain.AssemblyResolve to 'hydrate' embedded assemblies at runtime.
This project SQLDiagCmd at Github contains an example of doing this. It is based on Jeffrey Ricther's method:
AppDomain.CurrentDomain.AssemblyResolve += (sender, args) =>
{
String resourceName = "AssemblyLoadingAndReflection." +
new AssemblyName(args.Name).Name + ".dll";
using (var stream = Assembly.GetExecutingAssembly()
.GetManifestResourceStream(resourceName))
{
Byte[] assemblyData = new Byte[stream.Length];
stream.Read(assemblyData, 0, assemblyData.Length);
return Assembly.Load(assemblyData);
}
};
The 'trick' is where the embedded assembly is located and (as you have found), the string used to refer to it in the AssemblyResolve handler. [I don't have time right now but will look again later...]
Related
I'm currently working on an editor script that will ease my transition between freemium and paid versions of my game.I would like to manually import the .unitypackage file that gets imported when I click the import button under Services -> In-App Purchasing.
I am aware of the function AssetDatabase.ImportAsset(path) but I need the path of the package first.
Thanks in advance!
When you enable IAP and click Import, the following will happen:
1.Unity will generate a random file name with
FileUtil.GetUniqueTempPathInProject()
2.A full path will be constructed like this:
<ProjectName>Temp\FileUtil.GetUniqueTempPathInProject()
3.Unity will then add .unitypackage to the end of that random file name.
Now, you have something like:
<ProjectName>Temp\FileUtil.GetUniqueTempPathInProject()+".unitypackage";
4.IAP package will then be downloaded and stored to path from #3.
Illustration of what it looks like:
5.The file is then copied to
<ProjectName>Temp\TarGZ
It is saved with the file name generated from #2. No .unitypackage at the end like #3.
UnityEngine.Cloud.Purchasing
6.After that, Unity imports it with AssetDatabase.ImportPackage not AssetDatabase.ImportAsset(path,false) as mentioned in your question.
Now you can see where Unity IAP package is located but there are just few problems in what you are trying to do:
1.For the IAP package to be present, the import button from the Services Tab must be clicked. I am sure you want this to be automated.
2.The file name is not static therefore making the path hard to retrieve. As you can see from the image above, there are other files in this folder too. We don't know which one is the AIP Package.
3.When you re-start Unity, the temp folder will be deleted. So the downloaded IAP package will be lost.
The best way to get the Unity IAP package is to download it directly from Unity's server then save it to your preferred location. The code below will download the IAP package and store it at: <ProjectName>/UnityEngine.Cloud.Purchasing.unitypackage
It will also import it and then, enable it. For AIP to work, Analytics must be enabled too. This code will also do that.
I tried to hide the AIP url so that it won't be abused and Unity won't have change it.
If you want to remake this into something else, the two most important functions to look at are downloadAndInstallAIP() and deleteAndDisableAIP().
AIPDownloader Code:
using System;
using System.ComponentModel;
using System.IO;
using System.Net;
using System.Net.Security;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using UnityEditor;
using UnityEditor.Analytics;
using UnityEditor.Purchasing;
using UnityEngine;
[ExecuteInEditMode]
public class AIPDownloader : MonoBehaviour
{
static string projectDirectory = Environment.CurrentDirectory;
static string aipFileName = "UnityEngine.Cloud.Purchasing.unitypackage";
static string etagName = "UnityEngine.Cloud.PurchasingETAG.text";
static string aipfullPath = "";
static string eTagfullPath = "";
static EditorApplication.CallbackFunction doneEvent;
[MenuItem("AIP/Enable AIP")]
public static void downloadAndInstallAIP()
{
//Make AIP fullpath
aipfullPath = null;
aipfullPath = Path.Combine(projectDirectory, aipFileName);
//Make AIP Etag fullpath
eTagfullPath = null;
eTagfullPath = Path.Combine(projectDirectory, etagName);
/*If the AIP File already exist at <ProjectName>/UnityEngine.Cloud.Purchasing.unitypackage,
* there is no need to re-download it.
Re-import the package
*/
if (File.Exists(aipfullPath))
{
Debug.Log("AIP Package already exist. There is no need to re-download it");
if (saveETag(null, true))
{
importAIP(aipfullPath);
return;
}
}
string[] uLink = {
"aHR0cHM=",
"Oi8vcHVibGljLWNkbg==",
"LmNsb3Vk",
"LnVuaXR5M2Q=",
"LmNvbQ==",
"L1VuaXR5RW5naW5l",
"LkNsb3Vk",
"LlB1cmNoYXNpbmc=",
"LnVuaXR5cGFja2FnZQ=="
};
prepare(uLink);
try
{
ServicePointManager.ServerCertificateValidationCallback += new RemoteCertificateValidationCallback(ValidateRemoteCertificate);
WebClient client = new WebClient();
client.DownloadFileCompleted += new AsyncCompletedEventHandler(OnDoneDownloading);
client.DownloadProgressChanged += new DownloadProgressChangedEventHandler(OnDownloadProgressChanged);
client.DownloadFileAsync(new Uri(calc(uLink)), aipfullPath);
}
catch (Exception e)
{
Debug.LogError("Error: " + e.Message);
}
}
[MenuItem("AIP/Disable AIP")]
public static void deleteAndDisableAIP()
{
FileUtil.DeleteFileOrDirectory("Assets/Plugins/UnityPurchasing");
//Disable AIP
PurchasingSettings.enabled = false;
//Disable Analytics
AnalyticsSettings.enabled = false;
}
private static bool ValidateRemoteCertificate(object sender, X509Certificate cert, X509Chain chain, SslPolicyErrors error)
{
return true;
}
public bool isAIPEnabled()
{
return PurchasingSettings.enabled;
}
private static bool saveETag(WebClient client, bool alreadyDownloadedAIP = false)
{
string contents = "";
if (alreadyDownloadedAIP)
{
//Load Etag from file
try
{
contents = File.ReadAllText(eTagfullPath);
return _saveEtag(contents, alreadyDownloadedAIP);
}
catch (Exception e)
{
Debug.LogWarning("File does not exist!: " + e.Message);
}
return false; //Failed
}
else
{
//Load Etag from downloaded WebClient
contents = client.ResponseHeaders.Get("ETag");
return _saveEtag(contents, alreadyDownloadedAIP);
}
}
static bool _saveEtag(string contents, bool alreadyDownloadedAIP = false)
{
if (contents != null)
{
try
{
//Save if not downloaded
if (!alreadyDownloadedAIP)
{
Directory.CreateDirectory(Path.GetDirectoryName(eTagfullPath));
File.WriteAllText(eTagfullPath, contents);
}
//Save to the etag to AIP directory
Directory.CreateDirectory(Path.GetDirectoryName("Assets/Plugins/UnityPurchasing/ETag"));
File.WriteAllText("Assets/Plugins/UnityPurchasing/ETag", contents);
return true;//Success
}
catch (Exception e)
{
Debug.LogWarning("Failed to write to file: " + e.Message);
return false; //Failed
}
}
else
{
return false; //Failed
}
}
public static void OnDownloadProgressChanged(object sender, DownloadProgressChangedEventArgs e)
{
Debug.Log("Downloading: " + e.ProgressPercentage);
}
public static void OnDoneDownloading(object sender, AsyncCompletedEventArgs args)
{
WebClient wc = (WebClient)sender;
if (wc == null || args.Error != null)
{
Debug.Log("Failed to Download AIP!");
return;
}
Debug.Log("In Download Thread. Done Downloading");
saveETag(wc, false);
doneEvent = null;
doneEvent = new EditorApplication.CallbackFunction(AfterDownLoading);
//Add doneEvent function to call back!
EditorApplication.update = (EditorApplication.CallbackFunction)Delegate.Combine(EditorApplication.update, doneEvent);
}
static void AfterDownLoading()
{
//Remove doneEvent function from call back!
EditorApplication.update = (EditorApplication.CallbackFunction)Delegate.Remove(EditorApplication.update, doneEvent);
Debug.Log("Back to Main Thread. Done Downloading!");
importAIP(aipfullPath);
}
//Import or Install AIP
public static void importAIP(string path)
{
AssetDatabase.ImportPackage(path, false);
Debug.Log("Done Importing AIP package");
//Enable Analytics
AnalyticsSettings.enabled = true;
//Enable AIP
PurchasingSettings.enabled = true;
}
private static void prepare(string[] uLink)
{
for (int i = 5; i < uLink.Length; i++)
{
byte[] textAsBytes = System.Convert.FromBase64String(uLink[i]);
uLink[i] = Encoding.UTF8.GetString(textAsBytes);
}
for (int i = 0; i < uLink.Length; i++)
{
byte[] textAsBytes = System.Convert.FromBase64String(uLink[i]);
uLink[i] = Encoding.UTF8.GetString(textAsBytes);
if (i == 4)
{
break;
}
}
}
private static string calc(string[] uLink)
{
return string.Join("", uLink);
}
}
I'm trying to develop an application that supports dynamic loading of external modules. I have read several articles from loading external assemblies using C# (.NET v4.5) and got the code below. However, it is not working, not detecting my subclass on the external module.
Here is the code for loading external assembly:
byte[] array = <HERE I LOAD THE DLL>
Assembly asb = Assembly.Load(array);
Type[] types = GetAssemblyTypes(asb);
for( int i = 0; i < types.Length; i++ )
{
Type t = types[i];
if( t != null && typeof(App).IsAssignableFrom(t) /*t.IsSubclassOf(typeof(App))*/ )
{
app.AppClass = (App)Activator.CreateInstance(t);
return true;
}
}
Here is the GetAssemblyTypes()
private Type[] GetAssemblyTypes(Assembly asb)
{
Type[] types;
try
{
types = asb.GetTypes();
}
catch( ReflectionTypeLoadException ex )
{
types = ex.Types;
}
return types;
}
Here is the class on the MAIN APPLICATION (This class will be used by the modules)
namespace MyApplication.API
{
public class App
{
// CODE
}
}
Here is the example of my module:
using MyApplication.API;
namespace HelloWorld
{
class HelloWorld : App
{
}
}
Important points are:
1 - I don't know the class name of the module, I just know that it will be a subclass of the App class.
The issue is that although the types.Length gives me 1, when I tries to access by types[i] it gives a null pointer. Am I missing something here?
I have done something similar to find out if the assembly is derived from my base assembly
var assembly = System.Reflection.Assembly.LoadFrom(file);
AppDomain.CurrentDomain.ReflectionOnlyAssemblyResolve += CurrentDomain_ReflectionOnlyAssemblyResolve;
var derivedAssemblies = assembly.GetExportedTypes().Where(w => w.IsSubclassOf(typeof(AddressManager.Base.Connector.ConnectorBase))).Count();
AppDomain.CurrentDomain.ReflectionOnlyAssemblyResolve -= CurrentDomain_ReflectionOnlyAssemblyResolve;
assembly = null;
if (derivedAssemblies > 0)
{
Manager.LoadAssembly(file, "Connectors");
Trace.TraceInformation(" Success! Library loaded.");
}
else
Trace.TraceInformation(" Skipped! Not a subclass of '" + typeof(AddressManager.Base.Connector.ConnectorBase).Name + "'.");
And handle the ReflectionOnlyAssemblyResolve Event:
private System.Reflection.Assembly CurrentDomain_ReflectionOnlyAssemblyResolve(object sender, ResolveEventArgs args)
{
var assembly = AppDomain.CurrentDomain.GetAssemblies().Where(w => w.FullName == args.Name).FirstOrDefault();
return assembly;
}
I got a semi working version based on #CadBurry code.
byte[] bytes = <HERE I LOAD THE DLL>
Assembly asb = Assembly.Load(bytes);
AppDomain.CurrentDomain.AssemblyResolve += CurrentDomain_AssemblyResolve;
AppDomain.CurrentDomain.ReflectionOnlyAssemblyResolve += CurrentDomain_ReflectionOnlyAssemblyResolve;
IEnumerable<Type> types = asb.GetExportedTypes().Where(w => w.IsSubclassOf(typeof(App)));
AppDomain.CurrentDomain.ReflectionOnlyAssemblyResolve -= CurrentDomain_ReflectionOnlyAssemblyResolve;
AppDomain.CurrentDomain.AssemblyResolve -= CurrentDomain_AssemblyResolve;
if(types.Count() > 0)
{
Type type = types.FirstOrDefault();
if( type == null )
return false;
app.AppClass = (App)Activator.CreateInstance(type);
return true;
}
with the methods:
private Assembly CurrentDomain_AssemblyResolve(object sender, ResolveEventArgs args)
{
if( args.Name.Contains(typeof(MyApplication).Assembly.GetName().Name) )
{
return Assembly.GetExecutingAssembly();
}
return null;
}
private Assembly CurrentDomain_ReflectionOnlyAssemblyResolve(object sender, ResolveEventArgs args)
{
Assembly asb = AppDomain.CurrentDomain.GetAssemblies().Where(w => w.FullName == args.Name).FirstOrDefault();
return asb;
}
Using the above code I can load the assembly even when it is not on the current directory of the main application (eg. Plugins folders).
I would like to add an assembly with ResourceManager I have this code but it obviously doesn't work. Please help!
Loading the resource and trying to use it as an assembly:
static ResourceManager resourceManager = new ResourceManager("res", Assembly.GetExecutingAssembly());
static void Main()
{
AppDomain.CurrentDomain.AssemblyResolve += new ResolveEventHandler(CurrentDomain_AssemblyResolve);
}
static Assembly CurrentDomain_AssemblyResolve(object sender, ResolveEventArgs args)
{
AppDomain domain = (AppDomain)sender;
if(args.Name.Contains("System.Data.SQLite"))
{
return domain.Load(resourceManager.GetObject("System.Data.SQLite"));
}
return null;
}
Putting the resource in a ResourceManager:
using (ResourceWriter w = new ResourceWriter("res.resources"))
{
w.AddResource("System.Data.SQLite", File.ReadAllText("System.Data.SQLite.dll"));
}
if (CodeDom.Compile(outputValueTb.Text, Properties.Resources.src, iconValueTb.Text, "res.resources"))
{
//File.Copy("System.Data.SQLite.dll", System.IO.Path.GetDirectoryName(outputValueTb.Text) + "/System.Data.SQLite.dll");
File.Delete("res.resources");
success("Built");
}
Edit
So I changed my code to this
w.AddResource("System.Data.SQLite", File.ReadAllBytes("System.Data.SQLite.dll"));
But I still don't know how to use the resource into an assembly with this code:
AppDomain domain = (AppDomain)sender;
if(args.Name.Contains("System.Data.SQLite"))
{
return domain.Load(resourceManager.GetObject("System.Data.SQLite")); //should be a resource not bytes[]
}
return null;
If you have added the dll as a project item file and marked it "Embedded Resource" on the Build Action type (under properties), then you can use the following. Resolver is the event handler that you already have. Note: I haven't checked syntax, just typed it here; so fix syntax if needed.
internal static Assembly CurrentDomain_AssemblyResolve(object sender, ResolveEventArgs args)
{
var name = Assembly.GetExecutingAssembly()
.GetManifestResourceNames()
.FirstOrDefault(f => f.Contains("System.Data.SQLite"));
if (!string.IsNullOrEmpty(name) && args.Name.Contains("SQLite"))
{
using(var strm = Assembly.GetExecutingAssembly().GetManifestResourceStream(name))
{
var bytes = new byte[strm.Length];
stream.Read(bytes, 0, strm.Length);
return Assembly.Load(bytes);
}
}
return null;
}
I have two dlls (xNet.dll and ag.dll), which I want to use in my project.
I add them to resourses, stated that build action is Embedded Resource.
Next I have such code to load the first dll:
public Form1()
{
AppDomain.CurrentDomain.AssemblyResolve += AssemblyResolve;
InitializeComponent();
}
private static Assembly AssemblyResolve(object sender, ResolveEventArgs args)
{
Assembly assembly = Assembly.GetExecutingAssembly();
string assemblyName = args.Name.Split(',')[0];
using (Stream stream = assembly.GetManifestResourceStream("Yandex.dll.xNet.dll"))
{
if (stream == null)
return null;
byte[] rawAssembly = new byte[stream.Length];
stream.Read(rawAssembly, 0, (int)stream.Length);
return Assembly.Load(rawAssembly);
}
}
How to load the second dll?
You should match on the requested assembly name and return the correct assembly.
I don't know your assembly names so i'm doing only a very simple matching but it should look something like :
private static Assembly AssemblyResolve(object sender, ResolveEventArgs args)
{
if (args.Name.Contains("xNet"))
{
return LoadAssemblyFromResource("Yandex.dll.xNet.dll");
}
if (args.Name.Contains("ag"))
{
return LoadAssemblyFromResource("ag.dll");
}
return null;
}
private static Assembly LoadAssemblyFromResource(string resourceName)
{
Assembly assembly = Assembly.GetExecutingAssembly();
using (Stream stream = assembly.GetManifestResourceStream(resourceName))
{
if (stream == null)
return null;
byte[] rawAssembly = new byte[stream.Length];
stream.Read(rawAssembly, 0, (int)stream.Length);
return Assembly.Load(rawAssembly);
}
}
Why not extract the dll's to a temporary path and then load them.Assume you've two dll's,namely firstDll and secondDll with both having build action set to Resource.
Then extract those dll's to a temporary path,like this;
byte[] firstAssembly=Properties.Resources.firstDll;
File.WriteAllBytes(#"C:\Temp\firstDll.dll",firstAssembly);
byte[] secondAssembly=Properties.Resources.secondDll;
File.WriteAllBytes(#"C:\Temp\secondDll.dll",secondAssembly);
After this use Reflection to load those assembles and work with them.
I have A C# Visual Studio 2012 Solution that relies on a native dll that I use PInvoke to access. When I deploy the app I will have to ensure that this Dll is in the app folder.
Is there anyway I can merge this Dll into the executable?
perhaps as a resource?
I have heard of ILMerge but I am told it cant cope with native code.
Any help would be appreciated.
You can create a Setup package project with Visual Studio that deploys all your files to the correct location or use other third party packaging software (like full InstallShield or alternatives)
However, your question reminds me on the Open Hardware Monitor project where they include drivers as embedded resource and extract them when the user starts the application. It works like this: they've added WinRing0.sys and WinRing0x64.sys to the project and set their Build Action to Embedded Resource, then they have a method that extracts the driver from the resource:
private static bool ExtractDriver(string fileName) {
string resourceName = "OpenHardwareMonitor.Hardware." +
(OperatingSystem.Is64BitOperatingSystem() ? "WinRing0x64.sys" :
"WinRing0.sys");
string[] names =
Assembly.GetExecutingAssembly().GetManifestResourceNames();
byte[] buffer = null;
for (int i = 0; i < names.Length; i++) {
if (names[i].Replace('\\', '.') == resourceName) {
using (Stream stream = Assembly.GetExecutingAssembly().
GetManifestResourceStream(names[i]))
{
buffer = new byte[stream.Length];
stream.Read(buffer, 0, buffer.Length);
}
}
}
if (buffer == null)
return false;
try {
using (FileStream target = new FileStream(fileName, FileMode.Create)) {
target.Write(buffer, 0, buffer.Length);
target.Flush();
}
} catch (IOException) {
// for example there is not enough space on the disk
return false;
}
// make sure the file is actually writen to the file system
for (int i = 0; i < 20; i++) {
try {
if (File.Exists(fileName) &&
new FileInfo(fileName).Length == buffer.Length)
{
return true;
}
Thread.Sleep(100);
} catch (IOException) {
Thread.Sleep(10);
}
}
// file still has not the right size, something is wrong
return false;
}
They're reading the resource into a buffer, write that buffer to disk and wait until the file has been flushed to disk.
My solution is conceptually similar to the one presented by Wouter.
It's what we use in our own app, and we can use native/mixed-mode and c# dlls all embedded in the same .exe.
It extracts the dlls into a temp dir everytime the application is run. Obviously you might not want to do this in the production version, where the dlls will be stable; you might choose a different directory there (probably somewhere in %AppData%). It will use an existing dll with the same version number, though (e.g. it's only done the first time when opening the app multiple times between booting the computer).
Since we're doing
AppDomain.CurrentDomain.AssemblyResolve += (sender, args)
this function is getting called wherever the system tries to resolve a dll. And since it's initalised in the static Program class, it all works automagically.
Program.cs:
namespace MyApp
{
internal class Program
{
static Program()
{
LoadAssemblyResource.Initialize("MyApp");
}
//....
}
}
LoadAssemblyResource.cs
namespace MyAppStartup
{
public static class LoadAssemblyResource
{
private readonly static String _version_string =
Assembly.GetExecutingAssembly().GetName().Version.ToString();
private readonly static String _dll_path = Path.GetTempPath()
+ "\\MyApp\\" + _version_string;
static public String last_error_msg = null;
public static bool WriteBytesToFile(string filename, byte[] bytes)
{
try
{
var fs = new FileStream(filename, FileMode.Create, FileAccess.Write);
fs.Write(bytes, 0, bytes.Length);
fs.Close();
return true;
}
catch (Exception e)
{
Console.WriteLine("Writing file failed. Exception: {0}", e.ToString());
}
return false;
}
public static Assembly LoadUnsafe(String assembly_name, Byte[] assembly)
{
if (!Directory.Exists(_dll_path))
{
Directory.CreateDirectory(_dll_path);
Console.WriteLine("Created tmp path '" + _dll_path + "'.");
}
String fullpath = _dll_path + "\\" + assembly_name;
if (!File.Exists(fullpath))
{
Console.WriteLine("Assembly location: " + fullpath + ".");
if (!WriteBytesToFile(fullpath, assembly))
return null;
}
return Assembly.UnsafeLoadFrom(fullpath);
}
public static void Initialize(String exe_name)
{
AppDomain.CurrentDomain.AssemblyResolve += (sender, args) =>
{
String assembly_name = new AssemblyName(args.Name).Name + ".dll";
String resource_name = exe_name + "." + assembly_name;
using (var stream =
Assembly.GetExecutingAssembly().GetManifestResourceStream(resource_name))
{
if (stream == null)
return null;
Byte[] assembly_data = new Byte[stream.Length];
stream.Read(assembly_data, 0, assembly_data.Length);
try
{
Assembly il_assembly = Assembly.Load(assembly_data);
return il_assembly;
}
catch (System.IO.FileLoadException ex)
{
// might have failed because it's an mixed-mode dll.
last_error_msg = ex.Message;
}
Assembly mixed_mode_assembly = LoadUnsafe(assembly_name, assembly_data);
return mixed_mode_assembly;
}
};
}
}
}