/*
* Copyright (c) Contributors, http://opensimulator.org/
* See CONTRIBUTORS.TXT for a full list of copyright holders.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* * Neither the name of the OpenSimulator Project nor the
* names of its contributors may be used to endorse or promote products
* derived from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE DEVELOPERS ``AS IS'' AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE CONTRIBUTORS BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Reflection;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.IO;
using Nini.Config;
using log4net;
namespace OpenSim.Framework.Console
{
///
/// A console that uses cursor control and color
///
public class LocalConsole : CommandConsole
{
private static readonly ILog m_log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType);
private string m_historyPath;
private bool m_historyEnable;
private bool m_historytimestamps;
// private readonly object m_syncRoot = new object();
private const string LOGLEVEL_NONE = "(none)";
// Used to extract categories for colourization.
private Regex m_categoryRegex
= new Regex(
@"^(?.*?)\[(?[^\]]+)\]:?(?.*)", RegexOptions.Singleline | RegexOptions.Compiled);
private int m_cursorYPosition = -1;
private int m_cursorXPosition = 0;
private StringBuilder m_commandLine = new StringBuilder();
private bool m_echo = true;
private List m_history = new List();
private static readonly ConsoleColor[] Colors = {
// the dark colors don't seem to be visible on some black background terminals like putty :(
//ConsoleColor.DarkBlue,
//ConsoleColor.DarkGreen,
//ConsoleColor.DarkCyan,
//ConsoleColor.DarkMagenta,
//ConsoleColor.DarkYellow,
ConsoleColor.Gray,
//ConsoleColor.DarkGray,
ConsoleColor.Blue,
ConsoleColor.Green,
ConsoleColor.Cyan,
ConsoleColor.Magenta,
ConsoleColor.Yellow
};
private static ConsoleColor DeriveColor(string input)
{
// it is important to do Abs, hash values can be negative
return Colors[(Math.Abs(input.ToUpper().GetHashCode()) % Colors.Length)];
}
public LocalConsole(string defaultPrompt, IConfig startupConfig = null) : base(defaultPrompt)
{
if (startupConfig == null) return;
m_historyEnable = startupConfig.GetBoolean("ConsoleHistoryFileEnabled", false);
if (!m_historyEnable)
{
m_log.Info("[LOCAL CONSOLE]: Persistent command line history from file is Disabled");
return;
}
string m_historyFile = startupConfig.GetString("ConsoleHistoryFile", "OpenSimConsoleHistory.txt");
int m_historySize = startupConfig.GetInt("ConsoleHistoryFileLines", 100);
m_historyPath = Path.GetFullPath(Path.Combine(Util.configDir(), m_historyFile));
m_historytimestamps = startupConfig.GetBoolean("ConsoleHistoryTimeStamp", false);
m_log.InfoFormat("[LOCAL CONSOLE]: Persistent command line history is Enabled, up to {0} lines from file {1} {2} timestamps",
m_historySize, m_historyPath, m_historytimestamps?"with":"without");
if (File.Exists(m_historyPath))
{
List originallines = new List();
using (StreamReader history_file = new StreamReader(m_historyPath))
{
string line;
while ((line = history_file.ReadLine()) != null)
{
originallines.Add(line);
if(line.StartsWith("["))
{
int indx = line.IndexOf("]:> ");
if(indx > 0)
{
if(indx + 4 >= line.Length)
line = String.Empty;
else
line = line.Substring(indx + 4);
}
}
m_history.Add(line);
}
}
if (m_history.Count > m_historySize)
{
while (m_history.Count > m_historySize)
{
m_history.RemoveAt(0);
originallines.RemoveAt(0);
}
using (StreamWriter history_file = new StreamWriter(m_historyPath))
{
foreach (string line in originallines)
{
history_file.WriteLine(line);
}
}
}
m_log.InfoFormat("[LOCAL CONSOLE]: Read {0} lines of command line history from file {1}", m_history.Count, m_historyPath);
}
else
{
m_log.InfoFormat("[LOCAL CONSOLE]: Creating new empty command line history file {0}", m_historyPath);
File.Create(m_historyPath).Dispose();
}
}
private void AddToHistory(string text)
{
while (m_history.Count >= 100)
m_history.RemoveAt(0);
m_history.Add(text);
if (m_historyEnable)
{
if (m_historytimestamps)
text = String.Format("[{0} {1}]:> {2}", DateTime.Now.ToShortDateString(), DateTime.Now.ToShortTimeString(), text);
File.AppendAllText(m_historyPath, text + Environment.NewLine);
}
}
///
/// Set the cursor row.
///
///
///
/// Row to set. If this is below 0, then the row is set to 0. If it is equal to the buffer height or greater
/// then it is set to one less than the height.
///
///
/// The new cursor row.
///
private int SetCursorTop(int top)
{
// From at least mono 2.4.2.3, window resizing can give mono an invalid row and column values. If we try
// to set a cursor row position with a currently invalid column, mono will throw an exception.
// Therefore, we need to make sure that the column position is valid first.
int left = System.Console.CursorLeft;
if (left < 0)
{
System.Console.CursorLeft = 0;
}
else
{
int bufferWidth = System.Console.BufferWidth;
// On Mono 2.4.2.3 (and possibly above), the buffer value is sometimes erroneously zero (Mantis 4657)
if (bufferWidth > 0 && left >= bufferWidth)
System.Console.CursorLeft = bufferWidth - 1;
}
if (top < 0)
{
top = 0;
}
else
{
int bufferHeight = System.Console.BufferHeight;
// On Mono 2.4.2.3 (and possibly above), the buffer value is sometimes erroneously zero (Mantis 4657)
if (bufferHeight > 0 && top >= bufferHeight)
top = bufferHeight - 1;
}
System.Console.CursorTop = top;
return top;
}
///
/// Set the cursor column.
///
///
///
/// Column to set. If this is below 0, then the column is set to 0. If it is equal to the buffer width or greater
/// then it is set to one less than the width.
///
///
/// The new cursor column.
///
private int SetCursorLeft(int left)
{
// From at least mono 2.4.2.3, window resizing can give mono an invalid row and column values. If we try
// to set a cursor column position with a currently invalid row, mono will throw an exception.
// Therefore, we need to make sure that the row position is valid first.
int top = System.Console.CursorTop;
if (top < 0)
{
System.Console.CursorTop = 0;
}
else
{
int bufferHeight = System.Console.BufferHeight;
// On Mono 2.4.2.3 (and possibly above), the buffer value is sometimes erroneously zero (Mantis 4657)
if (bufferHeight > 0 && top >= bufferHeight)
System.Console.CursorTop = bufferHeight - 1;
}
if (left < 0)
{
left = 0;
}
else
{
int bufferWidth = System.Console.BufferWidth;
// On Mono 2.4.2.3 (and possibly above), the buffer value is sometimes erroneously zero (Mantis 4657)
if (bufferWidth > 0 && left >= bufferWidth)
left = bufferWidth - 1;
}
System.Console.CursorLeft = left;
return left;
}
private void Show()
{
lock (m_commandLine)
{
if (m_cursorYPosition == -1 || System.Console.BufferWidth == 0)
return;
int xc = prompt.Length + m_cursorXPosition;
int new_x = xc % System.Console.BufferWidth;
int new_y = m_cursorYPosition + xc / System.Console.BufferWidth;
int end_y = m_cursorYPosition + (m_commandLine.Length + prompt.Length) / System.Console.BufferWidth;
if (end_y >= System.Console.BufferHeight) // wrap
{
m_cursorYPosition--;
new_y--;
SetCursorLeft(0);
SetCursorTop(System.Console.BufferHeight - 1);
System.Console.WriteLine(" ");
}
m_cursorYPosition = SetCursorTop(m_cursorYPosition);
SetCursorLeft(0);
if (m_echo)
System.Console.Write("{0}{1}", prompt, m_commandLine);
else
System.Console.Write("{0}", prompt);
SetCursorTop(new_y);
SetCursorLeft(new_x);
}
}
public override void LockOutput()
{
Monitor.Enter(m_commandLine);
try
{
if (m_cursorYPosition != -1)
{
m_cursorYPosition = SetCursorTop(m_cursorYPosition);
System.Console.CursorLeft = 0;
int count = m_commandLine.Length + prompt.Length;
while (count-- > 0)
System.Console.Write(" ");
m_cursorYPosition = SetCursorTop(m_cursorYPosition);
SetCursorLeft(0);
}
}
catch (Exception)
{
}
}
public override void UnlockOutput()
{
if (m_cursorYPosition != -1)
{
m_cursorYPosition = System.Console.CursorTop;
Show();
}
Monitor.Exit(m_commandLine);
}
private void WriteColorText(ConsoleColor color, string sender)
{
try
{
lock (this)
{
try
{
System.Console.ForegroundColor = color;
System.Console.Write(sender);
System.Console.ResetColor();
}
catch (ArgumentNullException)
{
// Some older systems dont support coloured text.
System.Console.WriteLine(sender);
}
}
}
catch (ObjectDisposedException)
{
}
}
private void WriteLocalText(string text, string level)
{
string outText = text;
if (level != null)
{
MatchCollection matches = m_categoryRegex.Matches(text);
if (matches.Count == 1)
{
outText = matches[0].Groups["End"].Value;
System.Console.Write(matches[0].Groups["Front"].Value);
System.Console.Write("[");
WriteColorText(DeriveColor(matches[0].Groups["Category"].Value),
matches[0].Groups["Category"].Value);
System.Console.Write("]:");
}
else
{
outText = outText.Trim();
}
}
if (level == "error")
WriteColorText(ConsoleColor.Red, outText);
else if (level == "warn")
WriteColorText(ConsoleColor.Yellow, outText);
else
System.Console.Write(outText);
System.Console.WriteLine();
}
public override void Output(string format, string level = null, params object[] components)
{
string text = String.Format(format, components);
FireOnOutput(text);
lock (m_commandLine)
{
if (m_cursorYPosition == -1)
{
WriteLocalText(text, level);
return;
}
m_cursorYPosition = SetCursorTop(m_cursorYPosition);
SetCursorLeft(0);
int count = m_commandLine.Length + prompt.Length;
while (count-- > 0)
System.Console.Write(" ");
m_cursorYPosition = SetCursorTop(m_cursorYPosition);
SetCursorLeft(0);
WriteLocalText(text, level);
m_cursorYPosition = System.Console.CursorTop;
Show();
}
}
private bool ContextHelp()
{
string[] words = Parser.Parse(m_commandLine.ToString());
bool trailingSpace = m_commandLine.ToString().EndsWith(" ");
// Allow ? through while typing a URI
//
if (words.Length > 0 && words[words.Length-1].StartsWith("http") && !trailingSpace)
return false;
string[] opts = Commands.FindNextOption(words, trailingSpace);
if (opts[0].StartsWith("Command help:"))
Output(opts[0]);
else
Output(String.Format("Options: {0}", String.Join(" ", opts)));
return true;
}
public override string ReadLine(string p, bool isCommand, bool e)
{
m_cursorXPosition = 0;
prompt = p;
m_echo = e;
int historyLine = m_history.Count;
SetCursorLeft(0); // Needed for mono
System.Console.Write(" "); // Needed for mono
lock (m_commandLine)
{
m_cursorYPosition = System.Console.CursorTop;
m_commandLine.Remove(0, m_commandLine.Length);
}
while (true)
{
Show();
ConsoleKeyInfo key = System.Console.ReadKey(true);
char enteredChar = key.KeyChar;
if (!Char.IsControl(enteredChar))
{
if (m_cursorXPosition >= 318)
continue;
if (enteredChar == '?' && isCommand)
{
if (ContextHelp())
continue;
}
m_commandLine.Insert(m_cursorXPosition, enteredChar);
m_cursorXPosition++;
}
else
{
switch (key.Key)
{
case ConsoleKey.Backspace:
if (m_cursorXPosition == 0)
break;
m_commandLine.Remove(m_cursorXPosition-1, 1);
m_cursorXPosition--;
SetCursorLeft(0);
m_cursorYPosition = SetCursorTop(m_cursorYPosition);
if (m_echo)
System.Console.Write("{0}{1} ", prompt, m_commandLine);
else
System.Console.Write("{0}", prompt);
break;
case ConsoleKey.Delete:
if (m_cursorXPosition == m_commandLine.Length)
break;
m_commandLine.Remove(m_cursorXPosition, 1);
SetCursorLeft(0);
m_cursorYPosition = SetCursorTop(m_cursorYPosition);
if (m_echo)
System.Console.Write("{0}{1} ", prompt, m_commandLine);
else
System.Console.Write("{0}", prompt);
break;
case ConsoleKey.End:
m_cursorXPosition = m_commandLine.Length;
break;
case ConsoleKey.Home:
m_cursorXPosition = 0;
break;
case ConsoleKey.UpArrow:
if (historyLine < 1)
break;
historyLine--;
LockOutput();
m_commandLine.Remove(0, m_commandLine.Length);
m_commandLine.Append(m_history[historyLine]);
m_cursorXPosition = m_commandLine.Length;
UnlockOutput();
break;
case ConsoleKey.DownArrow:
if (historyLine >= m_history.Count)
break;
historyLine++;
LockOutput();
if (historyLine == m_history.Count)
{
m_commandLine.Remove(0, m_commandLine.Length);
}
else
{
m_commandLine.Remove(0, m_commandLine.Length);
m_commandLine.Append(m_history[historyLine]);
}
m_cursorXPosition = m_commandLine.Length;
UnlockOutput();
break;
case ConsoleKey.LeftArrow:
if (m_cursorXPosition > 0)
m_cursorXPosition--;
break;
case ConsoleKey.RightArrow:
if (m_cursorXPosition < m_commandLine.Length)
m_cursorXPosition++;
break;
case ConsoleKey.Enter:
SetCursorLeft(0);
m_cursorYPosition = SetCursorTop(m_cursorYPosition);
System.Console.WriteLine();
//Show();
lock (m_commandLine)
{
m_cursorYPosition = -1;
}
string commandLine = m_commandLine.ToString();
if (isCommand)
{
string[] cmd = Commands.Resolve(Parser.Parse(commandLine));
if (cmd.Length != 0)
{
int index;
for (index=0 ; index < cmd.Length ; index++)
{
if (cmd[index].Contains(" "))
cmd[index] = "\"" + cmd[index] + "\"";
}
AddToHistory(String.Join(" ", cmd));
return String.Empty;
}
}
// If we're not echoing to screen (e.g. a password) then we probably don't want it in history
if (m_echo && commandLine != "")
AddToHistory(commandLine);
return commandLine;
default:
break;
}
}
}
}
}
}