From 8db5ecb26db9be47b7ee3ac128316fc70c33ce5d Mon Sep 17 00:00:00 2001 From: Cyberboss Date: Tue, 31 Oct 2017 16:55:01 -0400 Subject: [PATCH] Heavy instancing work Added InstanceRootCommand for blocking instance based commands if no instance is chosen CLI now prints the service version Added service level CLI commands Fixed Service.LockLoggingID Added InstanceMetadata DataContract Implemented Service.ListInstances ExitCode is now set to int parent type Fixed ConnectivityLevel flags Optimized channel closing during instance switching --- TGCommandLine/AdminCommands.cs | 2 +- TGCommandLine/BYONDCommands.cs | 2 +- TGCommandLine/ChatCommands.cs | 4 +- TGCommandLine/ConfigCommands.cs | 2 +- TGCommandLine/DDCommands.cs | 2 +- TGCommandLine/DMCommands.cs | 2 +- TGCommandLine/InstanceRootCommand.cs | 33 ++ TGCommandLine/Program.cs | 55 ++- TGCommandLine/RootCommands.cs | 2 + TGCommandLine/ServiceCommands.cs | 433 +++++++++++++++++++ TGCommandLine/TGCommandLine.csproj | 2 + TGServerService/Service.cs | 20 +- TGServiceInterface/Command.cs | 2 +- TGServiceInterface/Components/Service.cs | 2 +- TGServiceInterface/Enumerations.cs | 4 +- TGServiceInterface/InstanceMetadata.cs | 32 ++ TGServiceInterface/Interface.cs | 52 +-- TGServiceInterface/TGServiceInterface.csproj | 1 + 18 files changed, 607 insertions(+), 45 deletions(-) create mode 100644 TGCommandLine/InstanceRootCommand.cs create mode 100644 TGCommandLine/ServiceCommands.cs create mode 100644 TGServiceInterface/InstanceMetadata.cs diff --git a/TGCommandLine/AdminCommands.cs b/TGCommandLine/AdminCommands.cs index 90c50f0b6e..aedddd15b1 100644 --- a/TGCommandLine/AdminCommands.cs +++ b/TGCommandLine/AdminCommands.cs @@ -4,7 +4,7 @@ using TGServiceInterface.Components; namespace TGCommandLine { - class AdminCommand : RootCommand + class AdminCommand : InstanceRootCommand { public AdminCommand() { diff --git a/TGCommandLine/BYONDCommands.cs b/TGCommandLine/BYONDCommands.cs index 875cca63a1..2666104e44 100644 --- a/TGCommandLine/BYONDCommands.cs +++ b/TGCommandLine/BYONDCommands.cs @@ -6,7 +6,7 @@ using TGServiceInterface.Components; namespace TGCommandLine { - class BYONDCommand : RootCommand + class BYONDCommand : InstanceRootCommand { public BYONDCommand() { diff --git a/TGCommandLine/ChatCommands.cs b/TGCommandLine/ChatCommands.cs index 268fc39d6a..314a8725a1 100644 --- a/TGCommandLine/ChatCommands.cs +++ b/TGCommandLine/ChatCommands.cs @@ -5,7 +5,7 @@ using TGServiceInterface.Components; namespace TGCommandLine { - class IRCCommand : RootCommand + class IRCCommand : InstanceRootCommand { public IRCCommand() { @@ -17,7 +17,7 @@ namespace TGCommandLine return "Manages the IRC bot"; } } - class DiscordCommand : RootCommand + class DiscordCommand : InstanceRootCommand { public DiscordCommand() { diff --git a/TGCommandLine/ConfigCommands.cs b/TGCommandLine/ConfigCommands.cs index d3cdb2d800..0aae903ed4 100644 --- a/TGCommandLine/ConfigCommands.cs +++ b/TGCommandLine/ConfigCommands.cs @@ -6,7 +6,7 @@ using TGServiceInterface.Components; namespace TGCommandLine { - class ConfigCommand : RootCommand + class ConfigCommand : InstanceRootCommand { public ConfigCommand() { diff --git a/TGCommandLine/DDCommands.cs b/TGCommandLine/DDCommands.cs index a0d073b9be..ef518c8911 100644 --- a/TGCommandLine/DDCommands.cs +++ b/TGCommandLine/DDCommands.cs @@ -5,7 +5,7 @@ using TGServiceInterface.Components; namespace TGCommandLine { - class DDCommand : RootCommand + class DDCommand : InstanceRootCommand { public DDCommand() { diff --git a/TGCommandLine/DMCommands.cs b/TGCommandLine/DMCommands.cs index acdd7bf686..cbc1ef6ae2 100644 --- a/TGCommandLine/DMCommands.cs +++ b/TGCommandLine/DMCommands.cs @@ -6,7 +6,7 @@ using TGServiceInterface.Components; namespace TGCommandLine { - class DMCommand : RootCommand + class DMCommand : InstanceRootCommand { public DMCommand() { diff --git a/TGCommandLine/InstanceRootCommand.cs b/TGCommandLine/InstanceRootCommand.cs new file mode 100644 index 0000000000..34685e8395 --- /dev/null +++ b/TGCommandLine/InstanceRootCommand.cs @@ -0,0 +1,33 @@ +using System.Collections.Generic; +using TGServiceInterface; + +namespace TGCommandLine +{ + abstract class InstanceRootCommand : RootCommand + { + public static Interface currentInterface; + public override ExitCode DoRun(IList parameters) + { + if (currentInterface.InstanceName == null) + { + OutputProc("Missing instance!"); + return ExitCode.BadCommand; + } + else + { + var res = currentInterface.ConnectToInstance(); + if (!res.HasFlag(ConnectivityLevel.Connected)) + { + OutputProc("Unable to connect to instance!"); + return ExitCode.ConnectionError; + } + else if (!res.HasFlag(ConnectivityLevel.Authenticated)) + { + OutputProc("The current user is not authorized to use this instance!"); + return ExitCode.ConnectionError; + } + } + return base.DoRun(parameters); + } + } +} diff --git a/TGCommandLine/Program.cs b/TGCommandLine/Program.cs index d3f46d17cd..d396240b4b 100644 --- a/TGCommandLine/Program.cs +++ b/TGCommandLine/Program.cs @@ -9,7 +9,7 @@ namespace TGCommandLine class Program { - static bool interactive = false; + static bool interactive = false, saidSrvVersion = false; static Interface currentInterface; static Command.ExitCode RunCommandLine(IList argsAsList) { @@ -83,6 +83,11 @@ namespace TGCommandLine SentVMMWarning = true; Console.WriteLine(error); } + else if (interactive && !saidSrvVersion) + { + Console.WriteLine("Connectd to service version: " + currentInterface.GetService().Version()); + saidSrvVersion = true; + } try { @@ -97,7 +102,6 @@ namespace TGCommandLine static void ReplaceInterface(Interface I) { - if (!interactive) I.SetBadCertificateHandler((message) => { @@ -110,6 +114,8 @@ namespace TGCommandLine I.SetBadCertificateHandler(BadCertificateInteractive); currentInterface = I; ConsoleCommand.Interface = I; + InstanceRootCommand.currentInterface = I; + saidSrvVersion = false; } public static string ReadLineSecure() @@ -156,21 +162,60 @@ namespace TGCommandLine return false; } + /// + /// Tries to set 's to , outputting appropriate messages + /// + /// The name of the to test + /// If , does not output on success + /// if a was achieved with , otherwise + static bool CheckInstanceConnectivity(string instanceName, bool silentSuccess) + { + var res = currentInterface.ConnectToInstance(instanceName); + if (!res.HasFlag(ConnectivityLevel.Connected)) + Console.WriteLine("Unable to connect to instance! Does it exist?"); + else if (!res.HasFlag(ConnectivityLevel.Authenticated)) + Console.WriteLine("The current user is not authorized to use this instance!"); + else + { + if(!silentSuccess) + Console.WriteLine("Successfully conected to instance!"); + return true; + } + return false; + } + static int Main(string[] args) { ReplaceInterface(new Interface()); Command.OutputProcVar.Value = Console.WriteLine; if (args.Length != 0) - return (int)RunCommandLine(new List(args)); - - Console.WriteLine("Type 'remote' to connect to a remote service"); + { + var argsAsList = new List(args); + for (var I = 0; I < argsAsList.Count - 1; ++I) + if (argsAsList[I].ToLower() == "--instance") + { + if (!CheckInstanceConnectivity(args[I + 1], true)) + return (int)Command.ExitCode.ConnectionError; + argsAsList.RemoveRange(I, 2); + break; + } + return (int)RunCommandLine(argsAsList); + } //interactive mode + + Console.WriteLine("Type 'instance' to connect to a server instance"); + Console.WriteLine("Type 'remote' to connect to a remote service"); + while (true) { Console.Write("Enter command: "); var NextCommand = Console.ReadLine(); switch (NextCommand.ToLower()) { + case "instance": + Console.Write("Enter instance name: "); + CheckInstanceConnectivity(Console.ReadLine(), false); + break; case "remote": SentVMMWarning = false; Console.Write("Enter server address: "); diff --git a/TGCommandLine/RootCommands.cs b/TGCommandLine/RootCommands.cs index 3f35d95518..4e6e3d4b69 100644 --- a/TGCommandLine/RootCommands.cs +++ b/TGCommandLine/RootCommands.cs @@ -12,6 +12,8 @@ namespace TGCommandLine var tmp = new List { new UpdateCommand(), new TestmergeCommand(), new RepoCommand(), new BYONDCommand(), new DMCommand(), new DDCommand(), new ConfigCommand(), new IRCCommand(), new DiscordCommand(), new AutoUpdateCommand(), new SetAutoUpdateCommand() }; if (I.ConnectToInstance().HasFlag(ConnectivityLevel.Administrator)) tmp.Add(new AdminCommand()); + if (I.ConnectionStatus().HasFlag(ConnectivityLevel.Administrator)) + tmp.Add(new ServiceCommand()); Children = tmp.ToArray(); } diff --git a/TGCommandLine/ServiceCommands.cs b/TGCommandLine/ServiceCommands.cs new file mode 100644 index 0000000000..efbc628e7d --- /dev/null +++ b/TGCommandLine/ServiceCommands.cs @@ -0,0 +1,433 @@ +using System; +using System.Collections.Generic; +using TGServiceInterface; + +namespace TGCommandLine +{ + /// + /// Used for managing the + /// + class ServiceCommand : RootCommand + { + /// + /// Construct a + /// + public ServiceCommand() + { + Keyword = "service"; + Children = new Command[] { new ServiceCreateInstanceCommand(), new ServiceDetachInstanceCommand(), new ServiceEnableInstanceCommand(), new ServiceImportInstanceCommand(), new ServiceListInstancesCommand(), new ServicePythonPathCommand(), new ServiceSetPythonPathCommand(), new ServiceSetRemoteAccessPortCommand(), new ServiceRemoteAccessPortCommand(), new ServiceDisableInstanceCommand(), new ServiceRenameInstanceCommand() }; + } + + /// + public override string GetHelpText() + { + return "Manage service wide settings"; + } + } + + /// + /// Command for calling + /// + class ServiceCreateInstanceCommand : ConsoleCommand + { + /// + /// Construct a + /// + public ServiceCreateInstanceCommand() + { + Keyword = "create-instance"; + RequiredParameters = 2; + } + + /// + public override string GetHelpText() + { + return "Creates a new instance at the given path"; + } + + /// + public override string GetArgumentString() + { + return " "; + } + + /// + protected override ExitCode Run(IList parameters) + { + var res = Interface.GetService().CreateInstance(parameters[0], parameters[1]); + if (res != null) + { + OutputProc(res); + return ExitCode.ServerError; + } + return ExitCode.Normal; + } + } + + /// + /// Command for calling + /// + class ServiceListInstancesCommand : ConsoleCommand + { + /// + /// Construct a + /// + public ServiceListInstancesCommand() + { + Keyword = "list-instances"; + } + + /// + public override string GetHelpText() + { + return "Lists all instances"; + } + + /// + protected override ExitCode Run(IList parameters) + { + foreach (var I in Interface.GetService().ListInstances()) + OutputProc(String.Format("{0} ({1}):\t{2}{3}", I.Name, I.Path, I.Enabled ? "Online" : "Offline", I.Enabled ? String.Format(" ({0})", I.LoggingID) : "")); + return ExitCode.Normal; + } + } + + /// + /// Command for calling + /// + class ServiceDetachInstanceCommand : ConsoleCommand + { + /// + /// Construct a + /// + public ServiceDetachInstanceCommand() + { + Keyword = "detach-instance"; + RequiredParameters = 1; + } + + /// + public override string GetHelpText() + { + return "Detaches an instance"; + } + + /// + public override string GetArgumentString() + { + return ""; + } + + /// + protected override ExitCode Run(IList parameters) + { + var res = Interface.GetService().DetachInstance(parameters[0]); + if (res != null) + { + OutputProc(res); + return ExitCode.ServerError; + } + return ExitCode.Normal; + } + } + + /// + /// Command for calling + /// + class ServiceImportInstanceCommand : ConsoleCommand + { + /// + /// Construct a + /// + public ServiceImportInstanceCommand() + { + Keyword = "import-instance"; + RequiredParameters = 1; + } + + /// + public override string GetHelpText() + { + return "Imports an instance (Chat settings will be lost if they are from a different windows installation)"; + } + + /// + public override string GetArgumentString() + { + return ""; + } + + /// + protected override ExitCode Run(IList parameters) + { + var res = Interface.GetService().ImportInstance(parameters[0]); + if (res != null) + { + OutputProc(res); + return ExitCode.ServerError; + } + return ExitCode.Normal; + } + } + + /// + /// Command for calling + /// + class ServicePythonPathCommand : ConsoleCommand + { + /// + /// Construct a + /// + public ServicePythonPathCommand() + { + Keyword = "python"; + } + + /// + public override string GetHelpText() + { + return "Displays configured path the service uses for python"; + } + + /// + protected override ExitCode Run(IList parameters) + { + var res = Interface.GetService().PythonPath(); + if (res != null) + { + OutputProc(res); + return ExitCode.ServerError; + } + return ExitCode.Normal; + } + } + + /// + /// Command for calling + /// + class ServiceSetPythonPathCommand : ConsoleCommand + { + /// + /// Construct a + /// + public ServiceSetPythonPathCommand() + { + Keyword = "set-python"; + RequiredParameters = 1; + } + + /// + public override string GetHelpText() + { + return "Sets the path to the python installation"; + } + + /// + public override string GetArgumentString() + { + return ""; + } + + /// + protected override ExitCode Run(IList parameters) + { + Interface.GetService().SetPythonPath(parameters[0]); + return ExitCode.Normal; + } + } + + /// + /// Command for calling with a parameter + /// + class ServiceEnableInstanceCommand : ConsoleCommand + { + /// + /// Construct a + /// + public ServiceEnableInstanceCommand() + { + Keyword = "enable-instance"; + RequiredParameters = 1; + } + + /// + public override string GetHelpText() + { + return "Enables the specified instance"; + } + + /// + public override string GetArgumentString() + { + return ""; + } + + /// + protected override ExitCode Run(IList parameters) + { + var res = Interface.GetService().SetInstanceEnabled(parameters[0], true); + if (res != null) + { + OutputProc(res); + return ExitCode.ServerError; + } + return ExitCode.Normal; + } + } + + /// + /// Command for calling with a parameter + /// + class ServiceDisableInstanceCommand : ConsoleCommand + { + /// + /// Construct a + /// + public ServiceDisableInstanceCommand() + { + Keyword = "disable-instance"; + RequiredParameters = 1; + } + + /// + public override string GetHelpText() + { + return "Disables the specified instance"; + } + + /// + public override string GetArgumentString() + { + return ""; + } + + /// + protected override ExitCode Run(IList parameters) + { + var res = Interface.GetService().SetInstanceEnabled(parameters[0], false); + if (res != null) + { + OutputProc(res); + return ExitCode.ServerError; + } + return ExitCode.Normal; + } + } + + /// + /// Command for calling + /// + class ServiceRemoteAccessPortCommand : ConsoleCommand + { + /// + /// Construct a + /// + public ServiceRemoteAccessPortCommand() + { + Keyword = "port"; + } + + /// + public override string GetHelpText() + { + return "Displays the service's remote access port"; + } + + /// + protected override ExitCode Run(IList parameters) + { + OutputProc(Interface.GetService().RemoteAccessPort().ToString()); + return ExitCode.Normal; + } + } + + /// + /// Command for calling + /// + class ServiceSetRemoteAccessPortCommand : ConsoleCommand + { + /// + /// Construct a + /// + public ServiceSetRemoteAccessPortCommand() + { + Keyword = "set-port"; + RequiredParameters = 1; + } + + /// + public override string GetHelpText() + { + return "Sets the service's remote access port"; + } + + /// + public override string GetArgumentString() + { + return ""; + } + + /// + protected override ExitCode Run(IList parameters) + { + ushort port; + try + { + port = Convert.ToUInt16(parameters[0]); + } + catch + { + OutputProc("Invalid port number!"); + return ExitCode.BadCommand; + } + + var res = Interface.GetService().SetRemoteAccessPort(port); + if (res != null) + { + OutputProc(res); + return ExitCode.ServerError; + } + OutputProc("Change will be applied after service restart"); + return ExitCode.Normal; + } + } + + /// + /// Command for calling + /// + class ServiceRenameInstanceCommand : ConsoleCommand + { + /// + /// Construct a + /// + public ServiceRenameInstanceCommand() + { + Keyword = "rename-instance"; + RequiredParameters = 1; + } + + /// + public override string GetHelpText() + { + return "Renames an instance. Will temporarily disable the instance if it is active"; + } + + /// + public override string GetArgumentString() + { + return " "; + } + + /// + protected override ExitCode Run(IList parameters) + { + var res = Interface.GetService().RenameInstance(parameters[0], parameters[1]); + if (res != null) + { + OutputProc(res); + return ExitCode.ServerError; + } + return ExitCode.Normal; + } + } +} diff --git a/TGCommandLine/TGCommandLine.csproj b/TGCommandLine/TGCommandLine.csproj index dc4522573e..aa3d32e639 100644 --- a/TGCommandLine/TGCommandLine.csproj +++ b/TGCommandLine/TGCommandLine.csproj @@ -48,11 +48,13 @@ + + diff --git a/TGServerService/Service.cs b/TGServerService/Service.cs index 6c6f0c897d..539d44826b 100644 --- a/TGServerService/Service.cs +++ b/TGServerService/Service.cs @@ -265,6 +265,7 @@ namespace TGServerService for (byte I = 1; I < 100; ++I) if (!UsedLoggingIDs.Contains(I)) { + UsedLoggingIDs.Add(I); return I; } } @@ -407,9 +408,21 @@ namespace TGServerService } /// - public IDictionary ListInstances() + public IList ListInstances() { - throw new NotImplementedException(); + var result = new List(); + lock (this) + foreach (var I in Properties.Settings.Default.InstancePaths) { + var ic = InstanceConfig.Load(I); + result.Add(new InstanceMetadata + { + Name = ic.Name, + Path = I, + Enabled = ic.Enabled, + LoggingID = (byte)(ic.Enabled ? ((ServerInstance)hosts[ic.Name].SingletonInstance).LoggingID : 0) + }); + } + return result; } /// @@ -428,6 +441,7 @@ namespace TGServerService try { var ic = new InstanceConfig(path); + ic.Name = Name; Directory.CreateDirectory(path); ic.Save(); Properties.Settings.Default.InstancePaths.Add(path); @@ -593,7 +607,7 @@ namespace TGServerService finally { if (ie) - result = SetInstanceEnabled(new_name, true) + " "+ result; + result = SetInstanceEnabled(new_name, true) + " " + result; } return result; } diff --git a/TGServiceInterface/Command.cs b/TGServiceInterface/Command.cs index c018a9c6b1..5355a0d55e 100644 --- a/TGServiceInterface/Command.cs +++ b/TGServiceInterface/Command.cs @@ -12,7 +12,7 @@ namespace TGServiceInterface /// /// Exit codes for s /// - public enum ExitCode + public enum ExitCode : int { /// /// The ran successfully diff --git a/TGServiceInterface/Components/Service.cs b/TGServiceInterface/Components/Service.cs index 695bae9849..e9057d957c 100644 --- a/TGServiceInterface/Components/Service.cs +++ b/TGServiceInterface/Components/Service.cs @@ -44,7 +44,7 @@ namespace TGServiceInterface.Components /// /// A of instance names relating to their paths [OperationContract] - IDictionary ListInstances(); + IList ListInstances(); /// /// Creates a new server instance diff --git a/TGServiceInterface/Enumerations.cs b/TGServiceInterface/Enumerations.cs index e342014b8b..5f65b463b0 100644 --- a/TGServiceInterface/Enumerations.cs +++ b/TGServiceInterface/Enumerations.cs @@ -19,11 +19,11 @@ namespace TGServiceInterface /// /// The connected user is authenticated /// - Authenticated = 2, + Authenticated = 2 | Connected, /// /// The connected user is an administrator /// - Administrator = 4, + Administrator = 4 | Authenticated, } /// diff --git a/TGServiceInterface/InstanceMetadata.cs b/TGServiceInterface/InstanceMetadata.cs new file mode 100644 index 0000000000..5a08e5b355 --- /dev/null +++ b/TGServiceInterface/InstanceMetadata.cs @@ -0,0 +1,32 @@ +using System.Runtime.Serialization; + +namespace TGServiceInterface +{ + /// + /// Metadata about an + /// + [DataContract] + public sealed class InstanceMetadata + { + /// + /// The name of the + /// + [DataMember] + public string Name { get; set; } + /// + /// The path of the + /// + [DataMember] + public string Path { get; set; } + /// + /// Whether or not the is enabled + /// + [DataMember] + public bool Enabled { get; set; } + /// + /// The logging ID of the . Will be 0 if is + /// + [DataMember] + public byte LoggingID { get; set; } + } +} diff --git a/TGServiceInterface/Interface.cs b/TGServiceInterface/Interface.cs index bda7c09afd..ec96caf32b 100644 --- a/TGServiceInterface/Interface.cs +++ b/TGServiceInterface/Interface.cs @@ -71,9 +71,9 @@ namespace TGServiceInterface readonly string HTTPSPassword; /// - /// Associated list of open s keyed by type. A in this list may close or fault at any time. Must be locked before being accessed + /// Associated list of open s keyed by type name. A in this list may close or fault at any time. Must be locked before being accessed /// - IDictionary ChannelFactoryCache = new Dictionary(); + IDictionary ChannelFactoryCache = new Dictionary(); /// /// Returns a of s that can be used with the service @@ -136,7 +136,7 @@ namespace TGServiceInterface return ConnectivityLevel.None; var prevInstance = InstanceName; if (prevInstance != instanceName) - CloseAllChannels(); + CloseAllChannels(false); InstanceName = instanceName; if (skipAuthChecks) return ConnectivityLevel.Connected; @@ -202,13 +202,28 @@ namespace TGServiceInterface /// /// Closes all s stored in and clears it /// - void CloseAllChannels() + /// If set to , doesn't clear the channels that are used by + void CloseAllChannels(bool includingRoot) { + string[] RootThings = { typeof(ITGSService).Name, 'S' + typeof(ITGConnectivity).Name }; lock (ChannelFactoryCache) { foreach (var I in ChannelFactoryCache) - CloseChannel(I.Value); - ChannelFactoryCache.Clear(); + { + if (RootThings.Contains(I.Key)) + continue; + var cf = I.Value; + try + { + cf.Closed += ChannelFactory_Closed; + cf.Close(); + } + catch + { + cf.Abort(); + } + ChannelFactoryCache.Remove(I); + } } } @@ -231,23 +246,6 @@ namespace TGServiceInterface return false; } - /// - /// Safely shuts down a single - /// - /// The to shutdown - static void CloseChannel(ChannelFactory cf) - { - try - { - cf.Closed += ChannelFactory_Closed; - cf.Close(); - } - catch - { - cf.Abort(); - } - } - /// /// Disposes a closed /// @@ -275,8 +273,10 @@ namespace TGServiceInterface { if (useInstanceName & InstanceName == null) throw new Exception("Instance not selected!"); - - var tot = typeof(T); + var actualToT = typeof(T); + var tot = actualToT.Name; + if (actualToT == typeof(ITGConnectivity) && !useInstanceName) + tot = 'S' + tot; ChannelFactory cf; lock (ChannelFactoryCache) @@ -413,7 +413,7 @@ namespace TGServiceInterface { if (disposing) { - CloseAllChannels(); + CloseAllChannels(true); } // TODO: free unmanaged resources (unmanaged objects) and override a finalizer below. diff --git a/TGServiceInterface/TGServiceInterface.csproj b/TGServiceInterface/TGServiceInterface.csproj index 42943a1d0d..ed915f7b4a 100644 --- a/TGServiceInterface/TGServiceInterface.csproj +++ b/TGServiceInterface/TGServiceInterface.csproj @@ -60,6 +60,7 @@ +