mirror of
https://github.com/OrchardCMS/Orchard.git
synced 2025-10-15 11:44:58 +08:00
Add support for response file to "orchard.exe"
Syntax is: orchar.exe @response-file-name-1 @response-file-name-2 ... --HG-- branch : dev
This commit is contained in:
@@ -16,21 +16,67 @@ namespace Orchard.Commands {
|
|||||||
/// executing a single command.
|
/// executing a single command.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class CommandHostAgent {
|
public class CommandHostAgent {
|
||||||
|
private IContainer _hostContainer;
|
||||||
|
|
||||||
public int RunSingleCommand(TextReader input, TextWriter output, string tenant, string[] args, Dictionary<string,string> switches) {
|
public int RunSingleCommand(TextReader input, TextWriter output, string tenant, string[] args, Dictionary<string,string> switches) {
|
||||||
|
int result = StartHost(input, output);
|
||||||
|
if (result != 0)
|
||||||
|
return result;
|
||||||
|
|
||||||
|
result = RunCommand(input, output, tenant, args, switches);
|
||||||
|
if (result != 0)
|
||||||
|
return result;
|
||||||
|
|
||||||
|
return StopHost(input, output);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int StartHost(TextReader input, TextWriter output) {
|
||||||
try {
|
try {
|
||||||
var hostContainer = OrchardStarter.CreateHostContainer(MvcSingletons);
|
_hostContainer = OrchardStarter.CreateHostContainer(MvcSingletons);
|
||||||
var host = hostContainer.Resolve<IOrchardHost>();
|
|
||||||
|
var host = _hostContainer.Resolve<IOrchardHost>();
|
||||||
host.Initialize();
|
host.Initialize();
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
catch (Exception e) {
|
||||||
|
for (; e != null; e = e.InnerException) {
|
||||||
|
output.WriteLine("Error: {0}", e.Message);
|
||||||
|
output.WriteLine("{0}", e.StackTrace);
|
||||||
|
}
|
||||||
|
return 5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public int StopHost(TextReader input, TextWriter output) {
|
||||||
|
try {
|
||||||
|
//
|
||||||
|
_hostContainer.Dispose();
|
||||||
|
_hostContainer = null;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
catch (Exception e) {
|
||||||
|
for (; e != null; e = e.InnerException) {
|
||||||
|
output.WriteLine("Error: {0}", e.Message);
|
||||||
|
output.WriteLine("{0}", e.StackTrace);
|
||||||
|
}
|
||||||
|
return 5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public int RunCommand(TextReader input, TextWriter output, string tenant, string[] args, Dictionary<string, string> switches) {
|
||||||
|
try {
|
||||||
|
var host = _hostContainer.Resolve<IOrchardHost>();
|
||||||
|
|
||||||
// Find tenant (or default)
|
// Find tenant (or default)
|
||||||
tenant = tenant ?? "default";
|
tenant = tenant ?? "default";
|
||||||
var tenantManager = hostContainer.Resolve<IShellSettingsManager>();
|
var tenantManager = _hostContainer.Resolve<IShellSettingsManager>();
|
||||||
var tenantSettings = tenantManager.LoadSettings().Single(s => String.Equals(s.Name, tenant, StringComparison.OrdinalIgnoreCase));
|
var tenantSettings = tenantManager.LoadSettings().Single(s => String.Equals(s.Name, tenant, StringComparison.OrdinalIgnoreCase));
|
||||||
|
|
||||||
// Disptach command execution to ICommandManager
|
// Disptach command execution to ICommandManager
|
||||||
using (var env = host.CreateStandaloneEnvironment(tenantSettings)) {
|
using (var env = host.CreateStandaloneEnvironment(tenantSettings)) {
|
||||||
var parameters = new CommandParameters {
|
var parameters = new CommandParameters {
|
||||||
Arguments = args,
|
Arguments = args,
|
||||||
Switches = switches,
|
Switches = switches,
|
||||||
Input = input,
|
Input = input,
|
||||||
Output = output
|
Output = output
|
||||||
@@ -41,7 +87,7 @@ namespace Orchard.Commands {
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
catch (Exception e) {
|
catch (Exception e) {
|
||||||
for(; e != null; e = e.InnerException) {
|
for (; e != null; e = e.InnerException) {
|
||||||
output.WriteLine("Error: {0}", e.Message);
|
output.WriteLine("Error: {0}", e.Message);
|
||||||
output.WriteLine("{0}", e.StackTrace);
|
output.WriteLine("{0}", e.StackTrace);
|
||||||
}
|
}
|
||||||
|
@@ -1,7 +1,10 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Web.Hosting;
|
using System.Web.Hosting;
|
||||||
|
using Orchard.Parameters;
|
||||||
|
using Orchard.ResponseFiles;
|
||||||
|
|
||||||
namespace Orchard.Host {
|
namespace Orchard.Host {
|
||||||
class CommandHost : MarshalByRefObject, IRegisteredObject {
|
class CommandHost : MarshalByRefObject, IRegisteredObject {
|
||||||
@@ -18,7 +21,7 @@ namespace Orchard.Host {
|
|||||||
//TODO
|
//TODO
|
||||||
}
|
}
|
||||||
|
|
||||||
public int RunCommand(TextReader input, TextWriter output, OrchardParameters args) {
|
public int RunCommand(TextReader input, TextWriter output, Logger logger, OrchardParameters args) {
|
||||||
var agent = Activator.CreateInstance("Orchard.Framework", "Orchard.Commands.CommandHostAgent").Unwrap();
|
var agent = Activator.CreateInstance("Orchard.Framework", "Orchard.Commands.CommandHostAgent").Unwrap();
|
||||||
int result = (int)agent.GetType().GetMethod("RunSingleCommand").Invoke(agent, new object[] {
|
int result = (int)agent.GetType().GetMethod("RunSingleCommand").Invoke(agent, new object[] {
|
||||||
input,
|
input,
|
||||||
@@ -29,5 +32,34 @@ namespace Orchard.Host {
|
|||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public int RunCommands(TextReader input, TextWriter output, Logger logger, IEnumerable<ResponseLine> responseLines) {
|
||||||
|
var agent = Activator.CreateInstance("Orchard.Framework", "Orchard.Commands.CommandHostAgent").Unwrap();
|
||||||
|
|
||||||
|
int result = (int)agent.GetType().GetMethod("StartHost").Invoke(agent, new object[] { input, output });
|
||||||
|
if (result != 0)
|
||||||
|
return result;
|
||||||
|
|
||||||
|
foreach (var line in responseLines) {
|
||||||
|
logger.LogInfo("{0} ({1}): Running command: {2}", line.Filename, line.LineNumber, line.LineText);
|
||||||
|
|
||||||
|
var args = new OrchardParametersParser().Parse(new CommandParametersParser().Parse(line.Args));
|
||||||
|
|
||||||
|
result = (int)agent.GetType().GetMethod("RunCommand").Invoke(agent, new object[] {
|
||||||
|
input,
|
||||||
|
output,
|
||||||
|
args.Tenant,
|
||||||
|
args.Arguments.ToArray(),
|
||||||
|
args.Switches});
|
||||||
|
|
||||||
|
if (result != 0) {
|
||||||
|
output.WriteLine("{0} ({1}): Command returned error ({2})", line.Filename, line.LineNumber, result);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result = (int)agent.GetType().GetMethod("StopHost").Invoke(agent, new object[] { input, output });
|
||||||
|
return result;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
21
src/Tools/Orchard/Logger.cs
Normal file
21
src/Tools/Orchard/Logger.cs
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
|
||||||
|
namespace Orchard {
|
||||||
|
public class Logger : MarshalByRefObject {
|
||||||
|
private readonly bool _verbose;
|
||||||
|
private readonly TextWriter _output;
|
||||||
|
|
||||||
|
public Logger(bool verbose, TextWriter output) {
|
||||||
|
_verbose = verbose;
|
||||||
|
_output = output;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void LogInfo(string format, params object[] args) {
|
||||||
|
if (_verbose) {
|
||||||
|
_output.Write("{0}: ", DateTime.Now);
|
||||||
|
_output.WriteLine(format, args);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -48,6 +48,7 @@
|
|||||||
<Reference Include="System.Xml" />
|
<Reference Include="System.Xml" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<Compile Include="Logger.cs" />
|
||||||
<Compile Include="OrchardHost.cs" />
|
<Compile Include="OrchardHost.cs" />
|
||||||
<Compile Include="Parameters\ICommandParametersParser.cs" />
|
<Compile Include="Parameters\ICommandParametersParser.cs" />
|
||||||
<Compile Include="IOrchardParametersParser.cs" />
|
<Compile Include="IOrchardParametersParser.cs" />
|
||||||
@@ -59,6 +60,8 @@
|
|||||||
<Compile Include="Program.cs" />
|
<Compile Include="Program.cs" />
|
||||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||||
<Compile Include="Parameters\CommandSwitch.cs" />
|
<Compile Include="Parameters\CommandSwitch.cs" />
|
||||||
|
<Compile Include="ResponseFiles\ResponseFiles.cs" />
|
||||||
|
<Compile Include="ResponseFiles\ResponseFileReader.cs" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
||||||
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
|
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
|
||||||
|
@@ -14,6 +14,7 @@ namespace Orchard {
|
|||||||
private readonly TextWriter _output;
|
private readonly TextWriter _output;
|
||||||
private readonly string[] _args;
|
private readonly string[] _args;
|
||||||
private OrchardParameters _arguments;
|
private OrchardParameters _arguments;
|
||||||
|
private Logger _logger;
|
||||||
|
|
||||||
public OrchardHost(TextReader input, TextWriter output, string[] args) {
|
public OrchardHost(TextReader input, TextWriter output, string[] args) {
|
||||||
_input = input;
|
_input = input;
|
||||||
@@ -33,7 +34,7 @@ namespace Orchard {
|
|||||||
_output.WriteLine("Error:");
|
_output.WriteLine("Error:");
|
||||||
for (; e != null; e = e.InnerException) {
|
for (; e != null; e = e.InnerException) {
|
||||||
_output.WriteLine(" {0}", e.Message);
|
_output.WriteLine(" {0}", e.Message);
|
||||||
if (Verbose) {
|
if (_logger != null) {
|
||||||
_output.WriteLine(" Stacktrace:");
|
_output.WriteLine(" Stacktrace:");
|
||||||
_output.WriteLine("{0}", e.StackTrace);
|
_output.WriteLine("{0}", e.StackTrace);
|
||||||
_output.WriteLine();
|
_output.WriteLine();
|
||||||
@@ -45,7 +46,25 @@ namespace Orchard {
|
|||||||
|
|
||||||
private int DoRun() {
|
private int DoRun() {
|
||||||
_arguments = new OrchardParametersParser().Parse(new CommandParametersParser().Parse(_args));
|
_arguments = new OrchardParametersParser().Parse(new CommandParametersParser().Parse(_args));
|
||||||
if (!_arguments.Arguments.Any() || _arguments.Switches.ContainsKey("?")) {
|
_logger = new Logger(_arguments.Verbose, _output);
|
||||||
|
|
||||||
|
// Perform some argument validation and display usage if something is incorrect
|
||||||
|
bool showHelp = _arguments.Switches.ContainsKey("?");
|
||||||
|
if (!showHelp) {
|
||||||
|
showHelp = (!_arguments.Arguments.Any() && !_arguments.ResponseFiles.Any());
|
||||||
|
if (showHelp) {
|
||||||
|
_output.WriteLine("Incorrect syntax: A command or a response file must be specified");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!showHelp) {
|
||||||
|
showHelp = (_arguments.Arguments.Any() && _arguments.ResponseFiles.Any());
|
||||||
|
if (showHelp) {
|
||||||
|
_output.WriteLine("Incorrect syntax: Response files cannot be used in conjunction with commands");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showHelp) {
|
||||||
return GeneralHelp();
|
return GeneralHelp();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,17 +84,28 @@ namespace Orchard {
|
|||||||
var host = (CommandHost)CreateWorkerAppDomainWithHost(_arguments.VirtualPath, orchardDirectory.FullName, typeof(CommandHost));
|
var host = (CommandHost)CreateWorkerAppDomainWithHost(_arguments.VirtualPath, orchardDirectory.FullName, typeof(CommandHost));
|
||||||
|
|
||||||
LogInfo("Executing command in ASP.NET AppDomain...");
|
LogInfo("Executing command in ASP.NET AppDomain...");
|
||||||
var result = host.RunCommand(_input, _output, _arguments);
|
var result = Execute(host);
|
||||||
LogInfo("Return code for command: {0}", result);
|
LogInfo("Return code for command: {0}", result);
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private int Execute(CommandHost host) {
|
||||||
|
if (_arguments.ResponseFiles.Any()) {
|
||||||
|
var responseLines = new ResponseFiles.ResponseFiles().ReadFiles(_arguments.ResponseFiles);
|
||||||
|
return host.RunCommands(_input, _output, _logger, responseLines.ToArray());
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return host.RunCommand(_input, _output, _logger, _arguments);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private int GeneralHelp() {
|
private int GeneralHelp() {
|
||||||
_output.WriteLine("Executes Orchard commands from a Orchard installation directory.");
|
_output.WriteLine("Executes Orchard commands from a Orchard installation directory.");
|
||||||
_output.WriteLine("");
|
_output.WriteLine("");
|
||||||
_output.WriteLine("Usage:");
|
_output.WriteLine("Usage:");
|
||||||
_output.WriteLine(" orchard.exe command [arg1] ... [argn] [/switch1[:value1]] ... [/switchn[:valuen]]");
|
_output.WriteLine(" orchard.exe command [arg1] ... [argn] [/switch1[:value1]] ... [/switchn[:valuen]]");
|
||||||
|
_output.WriteLine(" orchard.exe @response-file1 ... [@response-filen] [/switch1[:value1]] ... [/switchn[:valuen]]");
|
||||||
_output.WriteLine("");
|
_output.WriteLine("");
|
||||||
_output.WriteLine(" command");
|
_output.WriteLine(" command");
|
||||||
_output.WriteLine(" Specify the command to execute");
|
_output.WriteLine(" Specify the command to execute");
|
||||||
@@ -87,6 +117,10 @@ namespace Orchard {
|
|||||||
_output.WriteLine(" Specify switches to apply to the command. Available switches generally ");
|
_output.WriteLine(" Specify switches to apply to the command. Available switches generally ");
|
||||||
_output.WriteLine(" depend on the command executed, with the exception of a few built-in ones.");
|
_output.WriteLine(" depend on the command executed, with the exception of a few built-in ones.");
|
||||||
_output.WriteLine("");
|
_output.WriteLine("");
|
||||||
|
_output.WriteLine(" [@response-file1] ... [@response-filen]");
|
||||||
|
_output.WriteLine(" Specify one or more response files to be used for reading commands and switches.");
|
||||||
|
_output.WriteLine(" A response file is a text file that contains one line per command to execute.");
|
||||||
|
_output.WriteLine("");
|
||||||
_output.WriteLine(" Built-in commands");
|
_output.WriteLine(" Built-in commands");
|
||||||
_output.WriteLine(" =================");
|
_output.WriteLine(" =================");
|
||||||
_output.WriteLine("");
|
_output.WriteLine("");
|
||||||
@@ -119,10 +153,8 @@ namespace Orchard {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void LogInfo(string format, params object[] args) {
|
private void LogInfo(string format, params object[] args) {
|
||||||
if (Verbose) {
|
if (_logger != null)
|
||||||
_output.Write("{0}: ", DateTime.Now);
|
_logger.LogInfo(format, args);
|
||||||
_output.WriteLine(format, args);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private DirectoryInfo GetOrchardDirectory(string directory) {
|
private DirectoryInfo GetOrchardDirectory(string directory) {
|
||||||
|
@@ -7,7 +7,8 @@ namespace Orchard {
|
|||||||
public string VirtualPath { get; set; }
|
public string VirtualPath { get; set; }
|
||||||
public string WorkingDirectory { get; set; }
|
public string WorkingDirectory { get; set; }
|
||||||
public string Tenant { get; set; }
|
public string Tenant { get; set; }
|
||||||
public IEnumerable<string> Arguments { get; set; }
|
public IList<string> Arguments { get; set; }
|
||||||
|
public IList<string> ResponseFiles { get; set; }
|
||||||
public IDictionary<string, string> Switches { get; set; }
|
public IDictionary<string, string> Switches { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -1,4 +1,5 @@
|
|||||||
using System.Collections.Generic;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using Orchard.Parameters;
|
using Orchard.Parameters;
|
||||||
|
|
||||||
namespace Orchard {
|
namespace Orchard {
|
||||||
@@ -7,11 +8,28 @@ namespace Orchard {
|
|||||||
public OrchardParameters Parse(CommandParameters parameters) {
|
public OrchardParameters Parse(CommandParameters parameters) {
|
||||||
|
|
||||||
var result = new OrchardParameters {
|
var result = new OrchardParameters {
|
||||||
Arguments = parameters.Arguments,
|
Arguments = new List<string>(),
|
||||||
|
ResponseFiles = new List<string>(),
|
||||||
Switches = new Dictionary<string, string>()
|
Switches = new Dictionary<string, string>()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
foreach (var arg in parameters.Arguments) {
|
||||||
|
// @response-file
|
||||||
|
if (arg[0] == '@') {
|
||||||
|
var filename = arg.Substring(1);
|
||||||
|
if (string.IsNullOrEmpty(filename)) {
|
||||||
|
throw new ArgumentException("Incorrect syntax: response file name can not be empty");
|
||||||
|
}
|
||||||
|
result.ResponseFiles.Add(filename);
|
||||||
|
}
|
||||||
|
// regular argument
|
||||||
|
else {
|
||||||
|
result.Arguments.Add(arg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
foreach (var sw in parameters.Switches) {
|
foreach (var sw in parameters.Switches) {
|
||||||
|
// Built-in switches
|
||||||
switch (sw.Key.ToLowerInvariant()) {
|
switch (sw.Key.ToLowerInvariant()) {
|
||||||
case "wd":
|
case "wd":
|
||||||
case "workingdirectory":
|
case "workingdirectory":
|
||||||
|
71
src/Tools/Orchard/ResponseFiles/ResponseFileReader.cs
Normal file
71
src/Tools/Orchard/ResponseFiles/ResponseFileReader.cs
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace Orchard.ResponseFiles {
|
||||||
|
public class ResponseLine : MarshalByRefObject {
|
||||||
|
public string Filename { get; set; }
|
||||||
|
public string LineText { get; set; }
|
||||||
|
public int LineNumber { get; set; }
|
||||||
|
public string[] Args { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ResponseFileReader {
|
||||||
|
public IEnumerable<ResponseLine> ReadLines(string filename) {
|
||||||
|
using (var reader = File.OpenText(filename)) {
|
||||||
|
for (int i = 0; ; i++) {
|
||||||
|
string lineText = reader.ReadLine();
|
||||||
|
if (lineText == null)
|
||||||
|
yield break;
|
||||||
|
|
||||||
|
yield return new ResponseLine {
|
||||||
|
Filename = filename,
|
||||||
|
LineText = lineText,
|
||||||
|
LineNumber = i,
|
||||||
|
Args = SplitArgs(lineText).ToArray()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private IEnumerable<string> SplitArgs(string text) {
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
bool inString = false;
|
||||||
|
foreach (char ch in text) {
|
||||||
|
switch(ch){
|
||||||
|
case '"':
|
||||||
|
if (inString) {
|
||||||
|
inString = false;
|
||||||
|
yield return sb.ToString();
|
||||||
|
sb.Length = 0;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
inString = true;
|
||||||
|
sb.Length = 0;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case ' ':
|
||||||
|
case '\t':
|
||||||
|
if (sb.Length > 0) {
|
||||||
|
yield return sb.ToString();
|
||||||
|
sb.Length = 0;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
sb.Append(ch);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If there was anything accumulated
|
||||||
|
if (sb.Length > 0) {
|
||||||
|
yield return sb.ToString();
|
||||||
|
sb.Length = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
18
src/Tools/Orchard/ResponseFiles/ResponseFiles.cs
Normal file
18
src/Tools/Orchard/ResponseFiles/ResponseFiles.cs
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace Orchard.ResponseFiles {
|
||||||
|
public class ResponseFiles {
|
||||||
|
public IEnumerable<ResponseLine> ReadFiles(IEnumerable<string> filenames) {
|
||||||
|
foreach (var filename in filenames) {
|
||||||
|
foreach(var line in new ResponseFileReader().ReadLines(filename))
|
||||||
|
{
|
||||||
|
yield return line;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Reference in New Issue
Block a user