/*
;    Project:       Open Vehicle Monitor System
;    Date:          14th March 2017
;
;    Changes:
;    1.0  Initial release
;
;    (C) 2011       Michael Stegen / Stegen Electronics
;    (C) 2011-2017  Mark Webb-Johnson
;    (C) 2011        Sonny Chen @ EPRO/DX
;
; Permission is hereby granted, free of charge, to any person obtaining a copy
; of this software and associated documentation files (the "Software"), to deal
; in the Software without restriction, including without limitation the rights
; to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
; copies of the Software, and to permit persons to whom the Software is
; furnished to do so, subject to the following conditions:
;
; The above copyright notice and this permission notice shall be included in
; all copies or substantial portions of the Software.
;
; THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
; IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
; FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
; AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
; LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
; OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
; THE SOFTWARE.
*/

#include "ovms_log.h"
static const char *TAG = "command";

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <functional>
#include <esp_log.h>
#include <sys/time.h>
#include <sys/types.h>
#include <dirent.h>
#include "freertos/FreeRTOS.h"
#include "ovms_command.h"
#include "ovms_config.h"
#include "ovms_events.h"
#include "ovms_peripherals.h"
#include "ovms_utils.h"
#include "ovms_script.h"
#include "buffered_shell.h"
#include "log_buffers.h"
#include "ovms_semaphore.h"

OvmsCommandApp MyCommandApp __attribute__ ((init_priority (1010)));

bool CompareCharPtr::operator()(const char* a, const char* b)
  {
  return strcmp(a, b) < 0;
  }

OvmsWriter::OvmsWriter()
  {
  std::string p = MyConfig.GetParamValue("password","module");
  if (p.empty())
    m_issecure = true;
  else
    m_issecure = false;
  m_insert = NULL;
  m_userData = NULL;
  m_monitoring = false;
  }

OvmsWriter::~OvmsWriter()
  {
  }

void OvmsWriter::Exit()
  {
  puts("This console cannot exit.");
  }

void OvmsWriter::RegisterInsertCallback(InsertCallback cb, void* ctx)
  {
  m_insert = cb;
  m_userData = ctx;
  }

void OvmsWriter::DeregisterInsertCallback(InsertCallback cb)
  {
  if (m_insert == cb)
    {
    m_insert = NULL;
    m_userData = NULL;
    finalise();
    ProcessChar('\n');
    }
  }

bool OvmsWriter::IsSecure()
  {
  return m_issecure;
  }

void OvmsWriter::SetSecure(bool secure)
  {
  m_issecure = secure;
  }

OvmsCommand* OvmsCommandMap::FindUniquePrefix(const char* key)
  {
  int len = strlen(key);
  OvmsCommand* found = NULL;
  for (iterator it = begin(); it != end(); ++it)
    {
    if (strncmp(it->first, key, len) == 0)
      {
      if (len == strlen(it->first))
        {
        return it->second;
        }
      if (found)
        {
        return NULL;
        }
      else
        {
        found = it->second;
        }
      }
    }
  return found;
  }

OvmsCommand* OvmsCommandMap::FindCommand(const char* key)
  {
  iterator it = find(key);
  if (it == end())
    return NULL;
  else
    return it->second;
  }

char ** OvmsCommandMap::GetCompletion(OvmsWriter* writer, const char* token)
  {
  unsigned int index = 0;
  char** tokens = writer->SetCompletion(index, NULL);
  if (token)
    {
    for (iterator it = begin(); it != end(); ++it)
      {
      if (it->second->IsSecure() && !writer->IsSecure())
        continue;
      if (strncmp(it->first, token, strlen(token)) == 0)
        writer->SetCompletion(index++, it->first);
      }
    }
  return tokens;
  }

OvmsCommand::OvmsCommand()
  {
  m_execute = NULL;
  m_usage_template= "";
  m_parent = NULL;
  m_validate = NULL;
  }

OvmsCommand::OvmsCommand(const char* name, const char* title, OvmsCommandExecuteCallback_t execute,
                         const char *usage, int min, int max, bool secure,
                         OvmsCommandValidateCallback_t validate)
  {
  m_name = name;
  m_title = title;
  m_execute = execute;
  m_usage_template= !usage ? "" : usage;
  m_min = min;
  m_max = max;
  m_parent = NULL;
  m_secure = secure;
  m_validate = validate;
  }

OvmsCommand::~OvmsCommand()
  {
  for (auto it = m_children.begin(); it != m_children.end(); ++it)
    {
    OvmsCommand* cmd = it->second;
    delete cmd;
    }
  m_children.clear();
  }

const char* OvmsCommand::GetName()
  {
  return m_name;
  }

const char* OvmsCommand::GetTitle()
  {
  return m_title;
  }

// Dynamic generation of "Usage:" messages.  Syntax of the usage template string:
// - Prefix is "Usage: " followed by names from ancestors (if any) and name of self
// - "$C" expands to children as child1|child2|child3
// - "[$C]" expands to optional children as [child1|child2|child3]
// - $G$ expands to the usage of the first child (typically used after $C)
// - $Gfoo$ expands to the usage of the child named "foo"
// - $L lists a full usage line for each of the children
// - Parameters after command and subcommand tokens may be explicit like " <metric>"
// - Empty usage template "" defaults to "$C" for non-terminal OvmsCommand
void OvmsCommand::PutUsage(OvmsWriter* writer)
  {
  std::string result ="Usage: ";
  size_t pos = result.size();
  for (OvmsCommand* parent = m_parent; parent && parent->m_parent; parent = parent->m_parent)
    {
    if (parent->m_validate)
      {
      size_t len = strlen(parent->m_usage_template);
      char* dollar = index(parent->m_usage_template, '$');
      if (dollar)
        {
        len = dollar - parent->m_usage_template;
        if (len > 0 && *(dollar-1) == '[')
          --len;
        }
      else
        result.insert(pos, " ");
      result.insert(pos, parent->m_usage_template, len);
      }
    result.insert(pos, " ");
    result.insert(pos, parent->m_name);
    }
  result += m_name;
  result += " ";
  ExpandUsage(m_usage_template, writer, result);
  writer->puts(result.c_str());
  }

void OvmsCommand::ExpandUsage(const char* templ, OvmsWriter* writer, std::string& result)
  {
  std::string usage = (*templ || m_children.empty()) ? templ : m_execute ? "[$C]" : "$C";
  size_t pos;
  if ((pos = usage.find("$L")) != std::string::npos)
    {
    result += usage.substr(0, pos);
    pos += 2;
    size_t z = result.size();
    bool found = false;
    for (OvmsCommandMap::iterator it = m_children.begin(); it != m_children.end(); ++it)
      {
      OvmsCommand* child = it->second;
      if (!child->m_secure || writer->m_issecure)
        {
        if (found)
          {
          result += "\n";
          result += result.substr(0, z);
          }
        result += it->first;
        result += " ";
        found = true;
        child->ExpandUsage(child->m_usage_template, writer, result);
        }
      }
    if (result.size() == z)
      {
      result = "All subcommands require 'enable' mode";
      pos = usage.size();       // Don't append any more
      }
    result += usage.substr(pos);
    return;
    }

  if ((pos = usage.find("$C")) != std::string::npos)
    {
    result += usage.substr(0, pos);
    pos += 2;
    size_t z = result.size();
    bool found = false;
    for (OvmsCommandMap::iterator it = m_children.begin(); it != m_children.end(); ++it)
      {
      if (!it->second->m_secure || writer->m_issecure)
        {
        if (found)
          result += "|";
        result += it->first;
        found = true;
        }
      }
    if (result.size() == z)
      {
      result = "All subcommands require 'enable' mode";
      pos = usage.size();       // Don't append any more
      }
    }
  else pos = 0;
  size_t pos2;
  if ((pos2 = usage.find("$G", pos)) != std::string::npos)
    {
    result += usage.substr(pos, pos2-pos);
    pos2 += 2;
    size_t pos3;
    OvmsCommandMap::iterator it = m_children.end();
    if ((pos3 = usage.find('$', pos2)) != std::string::npos)
      {
      if (pos3 == pos2)
        it = m_children.begin();
      else
        it = m_children.find(usage.substr(pos2, pos3-pos2).c_str());
      pos = pos3 + 1;
      if (it != m_children.end() && (!it->second->m_secure || writer->m_issecure))
        {
        OvmsCommand* child = it->second;
        child->ExpandUsage(child->m_usage_template, writer, result);
        }
      }
    else
      pos = pos2;
    if (it == m_children.end())
      result += "ERROR IN USAGE TEMPLATE";
    }
  result += usage.substr(pos);
  }

OvmsCommand* OvmsCommand::RegisterCommand(const char* name, const char* title, OvmsCommandExecuteCallback_t execute,
                                          const char *usage, int min, int max, bool secure,
                                          OvmsCommandValidateCallback_t validate)
  {
  // Protect against duplicate registrations
  OvmsCommand* cmd = FindCommand(name);
  if (cmd == NULL)
    {
    cmd = new OvmsCommand(name, title, execute, usage, min, max, secure, validate);
    m_children[name] = cmd;
    cmd->m_parent = this;
    }
  return cmd;
  }

bool OvmsCommand::UnregisterCommand(const char* name)
  {
  if (name == NULL)
    {
    // Unregister this command
    return m_parent->UnregisterCommand(m_name);
    }

  // Unregister the specified child command
  auto pos = m_children.find(name);
  if (pos == m_children.end())
    {
    return false;
    }
  else
    {
    OvmsCommand* cmd = pos->second;
    m_children.erase(pos);
    delete cmd;
    return true;
    }
  }

char ** OvmsCommand::Complete(OvmsWriter* writer, int argc, const char * const * argv)
  {
  writer->SetCompletion(0, NULL);       // Start with no completion tokens
  if (m_validate)
    {
    int used = -1;
    if (argc > 0)
      used = m_validate(writer, this, argc > m_max ? m_max : argc, argv, true);
    if (used < 0 || used == argc)
      return writer->GetCompletions();
    argc -= used;
    argv += used;
    }
  if (argc <= 1)
    {
    return m_children.GetCompletion(writer, argc > 0 ? argv[0] : "");
    }
  OvmsCommand* cmd = m_children.FindUniquePrefix(argv[0]);
  if (!cmd)
    {
    return writer->SetCompletion(0, NULL);
    }
  return cmd->Complete(writer, argc-1, ++argv);
  }

void OvmsCommand::Execute(int verbosity, OvmsWriter* writer, int argc, const char * const * argv)
  {
//  if (argc>0)
//    {
//    printf("Execute(%s/%d) verbosity=%d argc=%d first=%s\n",m_title, m_children.size(), verbosity,argc,argv[0]);
//    }
//  else
//    {
//    printf("Execute(%s/%d) verbosity=%d (no args)\n",m_title, m_children.size(), verbosity);
//    }

  if (m_execute && (m_children.empty() || argc == 0))
    {
    //puts("Executing directly...");
    if (argc < m_min || argc > m_max || (argc > 0 && strcmp(argv[argc-1],"?")==0))
      {
      PutUsage(writer);
      return;
      }
    if ((!m_secure)||(m_secure && writer->m_issecure))
      m_execute(verbosity,writer,this,argc,argv);
    else
      writer->puts("Error: Secure command requires 'enable' mode");
    return;
    }
  else
    {
    if (m_validate && argc >= m_min)
      {
      int used = m_validate(writer, this, argc > m_max ? m_max : argc, argv, false);
      if (used < 0)
        {
        if (argc > 0 && strcmp(argv[argc-1],"?") != 0)
          writer->puts("Unrecognised command");
	PutUsage(writer);
        return;
        }
      argc -= used;
      argv += used;
      }
    //puts("Looking for a matching command");
    if (argc <= 0)
      {
      if (m_execute)
        {
        if ((!m_secure)||(m_secure && writer->m_issecure))
          m_execute(verbosity,writer,this,argc,argv);
        else
          writer->puts("Error: Secure command requires 'enable' mode");
        return;
        }
      writer->puts("Subcommand required");
      PutUsage(writer);
      return;
      }
    if (strcmp(argv[0],"?")==0)
      {
      // Skip usage line if it's just the one-line list of children.
      if (*m_usage_template || m_execute)
        PutUsage(writer);
      // Show available commands
      int avail = 0;
      for (OvmsCommandMap::iterator it=m_children.begin(); it!=m_children.end(); ++it)
        {
        if (it->second->IsSecure() && !writer->m_issecure)
          continue;
        const char* k = it->first;
        const char* v = it->second->GetTitle();
        writer->printf("%-20.20s %s\n",k,v);
        ++avail;
        }
      if (!avail)
        writer->printf("All subcommands require 'enable' mode\n");
      return;
      }
    OvmsCommand* cmd = m_children.FindUniquePrefix(argv[0]);
    if (!cmd)
      {
      writer->puts("Unrecognised command");
      if (GetParent())    // No usage line for root command
        PutUsage(writer);
      return;
      }
    cmd->Execute(verbosity,writer,argc-1,++argv);
    }
  }

OvmsCommand* OvmsCommand::GetParent()
  {
  return m_parent;
  }

OvmsCommand* OvmsCommand::FindCommand(const char* name)
  {
  return m_children.FindCommand(name);
  }

// List all OvmsCommand objects in the tree as a tab-separated CSV-format table
// (which requires doubling " marks).
void OvmsCommand::Display(OvmsWriter* writer, int level)
  {
  static const char* const spaces = "                                        ";
  static const int len = strlen(spaces);
  static const char* const end = spaces + len;
  if (level >= 0)
    {
    const char* usage = m_usage_template;
    if (!usage)
	usage = "NULL";
    const char* p = usage;
    int quotes = 0;
    for ( ; *p; ++p)
      if (*p == '"')
        ++quotes;
    char* m = (char*)malloc(p - m_usage_template + quotes + 1);
    if (m)
      {
      char* q = m;
      for (p = usage; *p; )
        {
        if (*p == '"')
          *q++ = '"';
        *q++ = *p++;
        }
      *q = '\0';
      usage = m;
      }
    if (2*level > len)
      level = 0;
    writer->printf("\"%s%s\"\t\"%s\"\t\"%s\"\t%d\t%d\t%s\t%s\t%s\t%s\n",
      end-2*level, m_name, m_title, usage, m_min, m_max, m_children.empty() ? "--" : "children",
      m_execute ? "execute" : "--", m_secure ? "secure" : "--", m_validate ? "validate" : "--");
    if (m)
      free(m);
    }
  for (OvmsCommandMap::iterator it=m_children.begin(); it!=m_children.end(); ++it)
    it->second->Display(writer, level + 1);
  }

void help(int verbosity, OvmsWriter* writer, OvmsCommand* cmd, int argc, const char* const* argv)
  {
  writer->puts("Enter a single \"?\" to get the root command list.");
  writer->puts("Commands can have multiple levels of subcommands.");
  writer->puts("Use \"command [...] ?\" to get the list of subcommands and parameters.");
  writer->puts("Commands can be abbreviated, push <TAB> for auto completion at any level");
  writer->puts("including at the start of a subcommand to get a list of subcommands.");
  writer->puts("Use \"enable\" to enter secure (admin) mode.");
  }

void cmd_exit(int verbosity, OvmsWriter* writer, OvmsCommand* cmd, int argc, const char* const* argv)
  {
  writer->Exit();
  }

void log_level(int verbosity, OvmsWriter* writer, OvmsCommand* cmd, int argc, const char* const* argv)
  {
  const char* tag = "*";
  if (argc > 0)
    tag = argv[0];
  const char* title = cmd->GetTitle();
  esp_log_level_t level_num = (esp_log_level_t)(*(title+strlen(title)-2) - '0');
  esp_log_level_set(tag, level_num);
  writer->printf("Logging level for %s set to %s\n",tag,cmd->GetName());
  }

void log_file(int verbosity, OvmsWriter* writer, OvmsCommand* cmd, int argc, const char* const* argv)
  {
  std::string path;
  if (argc == 1)
    path = argv[0];
  else
    path = MyConfig.GetParamValue("log", "file.path");
  if (MyConfig.ProtectedPath(path))
    {
    writer->puts("Error: protected path");
    return;
    }
  if (!MyCommandApp.SetLogfile(path))
    {
    writer->puts("Error: VFS file cannot be opened for append");
    return;
    }
  writer->printf("Logging to file: %s\n", path.c_str());
  }

void log_close(int verbosity, OvmsWriter* writer, OvmsCommand* cmd, int argc, const char* const* argv)
  {
  std::string path = MyCommandApp.GetLogfile();
  if (path.empty())
    {
    writer->puts("Error: no log file path has been set");
    return;
    }
  if (MyCommandApp.CloseLogfile())
    writer->printf("File logging to '%s' stopped\n", path.c_str());
  else
    writer->printf("Error: stop file logging to '%s' failed, see log for details\n", path.c_str());
  }

void log_open(int verbosity, OvmsWriter* writer, OvmsCommand* cmd, int argc, const char* const* argv)
  {
  std::string path = MyCommandApp.GetLogfile();
  if (path.empty())
    {
    writer->puts("Error: no log file path has been set");
    return;
    }
  if (MyCommandApp.OpenLogfile())
    writer->printf("File logging to '%s' started\n", path.c_str());
  else
    writer->printf("Error: start file logging to '%s' failed, see log for details\n", path.c_str());
  }

void log_status(int verbosity, OvmsWriter* writer, OvmsCommand* cmd, int argc, const char* const* argv)
  {
  MyCommandApp.ShowLogStatus(verbosity, writer);
  }

void log_expire(int verbosity, OvmsWriter* writer, OvmsCommand* cmd, int argc, const char* const* argv)
  {
  if (MyCommandApp.m_expiretask)
    {
    writer->puts("Abort: expire task is currently running");
    return;
    }
  int keepdays;
  if (argc == 0)
    keepdays = MyConfig.GetParamValueInt("log", "file.keepdays", 30);
  else
    keepdays = atoi(argv[0]);
  MyCommandApp.ExpireLogFiles(verbosity, writer, keepdays);
  }

static OvmsCommand* monitor;
static OvmsCommand* monitor_yes;

void log_monitor(int verbosity, OvmsWriter* writer, OvmsCommand* cmd, int argc, const char* const* argv)
  {
  bool state;
  if (cmd == monitor)
    state = !writer->IsMonitoring();
  else
    state = (cmd == monitor_yes);
  writer->printf("Monitoring log messages %s\n", state ? "enabled" : "disabled");
  writer->SetMonitoring(state);
  }

typedef struct
  {
  std::string password;
  int tries;
  } PasswordContext;

bool enableInsert(OvmsWriter* writer, void* v, char ch)
  {
  PasswordContext* pc = (PasswordContext*)v;
  if (ch == '\n')
    {
    std::string p = MyConfig.GetParamValue("password","module");
    if (p.compare(pc->password) == 0)
      {
      writer->SetSecure(true);
      writer->printf("\nSecure mode");
      delete pc;
      return false;
      }
    if (++pc->tries == 3)
      {
      writer->printf("\nError: %d incorrect password attempts", pc->tries);
      vTaskDelay(5000 / portTICK_PERIOD_MS);
      delete pc;
      return false;
      }
    writer->printf("\nSorry, try again.\nPassword:");
    pc->password.erase();
    return true;
    }
  if (ch == 'C'-0100)
    {
    delete pc;
    return false;
    }
  if (ch != '\r')
    pc->password.append(1, ch);
  return true;
  }

void enable(int verbosity, OvmsWriter* writer, OvmsCommand* cmd, int argc, const char* const* argv)
  {
  std::string p = MyConfig.GetParamValue("password","module");
  if ((p.empty())||((argc==1)&&(p.compare(argv[0])==0)))
    {
    writer->SetSecure(true);
    writer->puts("Secure mode");
    }
  else if (argc == 1)
    {
    vTaskDelay(5000 / portTICK_PERIOD_MS);
    writer->puts("Error: Invalid password");
    }
  else
    {
    PasswordContext* pc = new PasswordContext;
    pc->tries = 0;
    writer->printf("Password:");
    writer->RegisterInsertCallback(enableInsert, pc);
    }
  }

void disable(int verbosity, OvmsWriter* writer, OvmsCommand* cmd, int argc, const char* const* argv)
  {
  writer->SetSecure(false);
  }

void cmd_sleep(int verbosity, OvmsWriter* writer, OvmsCommand* cmd, int argc, const char* const* argv)
  {
  int milliseconds = atof(argv[0]) * 1000;
  if (milliseconds >= 0)
    vTaskDelay(pdMS_TO_TICKS(milliseconds));
  }

void cmd_echo(int verbosity, OvmsWriter* writer, OvmsCommand* cmd, int argc, const char* const* argv)
  {
  int i;
  for (i = 0; i < argc; i++)
    writer->puts(argv[i]);
  if (!i)
    writer->puts("");
  }

#ifdef CONFIG_OVMS_SC_JAVASCRIPT_DUKTAPE

static duk_ret_t DukOvmsCommandExec(duk_context *ctx)
  {
  const char *cmd = duk_to_string(ctx,0);

  if (cmd != NULL)
    {
    BufferedShell* bs = new BufferedShell(false, COMMAND_RESULT_NORMAL);
    bs->SetSecure(true); // this is an authorized channel
    bs->ProcessChars(cmd, strlen(cmd));
    bs->ProcessChar('\n');
    std::string val; bs->Dump(val);
    delete bs;
    duk_push_string(ctx, val.c_str());
    return 1;  /* one return value */
    }
  else
    {
    return 0;
    }
  }

//static void DukOvmsCommandRegisterRun(int verbosity, OvmsWriter* writer, OvmsCommand* cmd, int argc, const char* const* argv)
//  {
//  writer->printf("Duktape: Command %s run\n",cmd->GetName());
//  }

static duk_ret_t DukOvmsCommandRegister(duk_context *ctx)
  {
  std::string filename, function;
  int linenumber = 0;
  MyDuktape.DukGetCallInfo(ctx, &filename, &linenumber, &function);

  const char *fullcommand = duk_to_string(ctx,0);
  const char *name = duk_to_string(ctx,1);
  const char *title = duk_to_string(ctx,2);
  const char *usage = duk_to_string(ctx,3);
  uint32_t min = duk_is_number(ctx,4) ? duk_to_uint32(ctx,4) : 0;
  uint32_t max = duk_is_number(ctx,5) ? duk_to_uint32(ctx,5) : 0;

  MyDuktape.RegisterDuktapeConsoleCommand(
    ctx, 0,
    filename.c_str(),
    fullcommand,
    name,
    title,
    usage,
    min,
    max);

  ESP_LOGD(TAG,"Duktape: Script %s %s:%d registered command %s/%s",
      filename.c_str(), function.c_str(), linenumber, fullcommand, name);

  return 0;
  }

#endif // #ifdef CONFIG_OVMS_SC_JAVASCRIPT_DUKTAPE

OvmsCommandApp::OvmsCommandApp()
  {
  ESP_LOGI(TAG, "Initialising COMMAND (1010)");

  m_logfile = NULL;
  m_logfile_path = "";
  m_logfile_size = 0;
  m_logfile_maxsize = 0;
  m_logtask = NULL;
  m_logtask_queue = NULL;
  m_logtask_dropcnt = 0;
  m_logfile_cyclecnt = 0;
  m_expiretask = 0;

  m_root.RegisterCommand("help", "Ask for help", help, "", 0, 0, false);
  m_root.RegisterCommand("exit", "End console session", cmd_exit, "", 0, 0, false);
  OvmsCommand* cmd_log = MyCommandApp.RegisterCommand("log","LOG framework", log_status, "", 0, 0, false);
  cmd_log->RegisterCommand("file", "Start logging to specified file", log_file, "[<vfspath>]\nDefault: config log[file.path]", 0, 1);
  cmd_log->RegisterCommand("open", "Start file logging", log_open);
  cmd_log->RegisterCommand("close", "Stop file logging", log_close);
  cmd_log->RegisterCommand("status", "Show logging status", log_status);
  cmd_log->RegisterCommand("expire", "Expire old log files", log_expire, "[<keepdays>]", 0, 1);
  OvmsCommand* level_cmd = cmd_log->RegisterCommand("level", "Set logging level", NULL, "$C [<tag>]", 0, 0, false);
  level_cmd->RegisterCommand("verbose", "Log at the VERBOSE level (5)", log_level , "[<tag>]", 0, 1);
  level_cmd->RegisterCommand("debug", "Log at the DEBUG level (4)", log_level , "[<tag>]", 0, 1);
  level_cmd->RegisterCommand("info", "Log at the INFO level (3)", log_level , "[<tag>]", 0, 1);
  level_cmd->RegisterCommand("warn", "Log at the WARN level (2)", log_level , "[<tag>]", 0, 1, false);
  level_cmd->RegisterCommand("error", "Log at the ERROR level (1)", log_level , "[<tag>]", 0, 1, false);
  level_cmd->RegisterCommand("none", "No logging (0)", log_level , "[<tag>]", 0, 1, false);
  monitor = cmd_log->RegisterCommand("monitor", "Monitor log on this console", log_monitor);
  monitor_yes = monitor->RegisterCommand("yes", "Monitor log", log_monitor);
  monitor->RegisterCommand("no", "Don't monitor log", log_monitor);
  m_root.RegisterCommand("enable","Enter secure mode (enable access to all commands)", enable, "[<password>]", 0, 1, false);
  m_root.RegisterCommand("disable","Leave secure mode (disable access to most commands)", disable);
  m_root.RegisterCommand("sleep", "Script utility: pause execution", cmd_sleep,
    "<seconds>\nFractions of seconds are supported, e.g. 0.2 = 200 ms", 1, 1);
  m_root.RegisterCommand("echo", "Script utility: output text", cmd_echo,
    "[<text>] […]\nOutputs up to 10 arguments as separate lines, just a newline if no text is given.", 0, 10);

#ifdef CONFIG_OVMS_SC_JAVASCRIPT_DUKTAPE
  ESP_LOGI(TAG, "Expanding DUKTAPE javascript engine");
  DuktapeObjectRegistration* dto = new DuktapeObjectRegistration("OvmsCommand");
  dto->RegisterDuktapeFunction(DukOvmsCommandExec, 1, "Exec");
  dto->RegisterDuktapeFunction(DukOvmsCommandRegister, 6, "Register");
  MyDuktape.RegisterDuktapeObject(dto);
#endif // #ifdef CONFIG_OVMS_SC_JAVASCRIPT_DUKTAPE
  }

OvmsCommandApp::~OvmsCommandApp()
  {
  }

void OvmsCommandApp::ConfigureLogging()
  {
  MyConfig.RegisterParam("log", "Logging configuration", true, true);

  using std::placeholders::_1;
  using std::placeholders::_2;
  MyEvents.RegisterEvent(TAG, "config.changed", std::bind(&OvmsCommandApp::EventHandler, this, _1, _2));
  MyEvents.RegisterEvent(TAG, "sd.mounted", std::bind(&OvmsCommandApp::EventHandler, this, _1, _2));
  MyEvents.RegisterEvent(TAG, "sd.unmounting", std::bind(&OvmsCommandApp::EventHandler, this, _1, _2));
  MyEvents.RegisterEvent(TAG, "ticker.3600", std::bind(&OvmsCommandApp::EventHandler, this, _1, _2));

  ReadConfig();
  }

OvmsCommand* OvmsCommandApp::RegisterCommand(const char* name, const char* title, OvmsCommandExecuteCallback_t execute,
                                             const char *usage, int min, int max, bool secure)
  {
  return m_root.RegisterCommand(name, title, execute, usage, min, max, secure);
  }

bool OvmsCommandApp::UnregisterCommand(const char* name)
  {
  // Unregister the specified child command
  return m_root.UnregisterCommand(name);
  }

OvmsCommand* OvmsCommandApp::FindCommand(const char* name)
  {
  return m_root.FindCommand(name);
  }

OvmsCommand* OvmsCommandApp::FindCommandFullName(const char* name)
  {
  OvmsCommand* found = &m_root;
  const char* p = name;

  while (*p != 0)
    {
    const char* d = strchr(p,' ');
    if (d)
      {
      std::string command(p,0,d-p);
      found = found->FindCommand(command.c_str());
      p = d+1;
      }
    else
      {
      found = found->FindCommand(p);
      return found;
      }
    if (found==NULL) return found;
    }

  return found;
  }

void OvmsCommandApp::RegisterConsole(OvmsWriter* writer)
  {
  m_consoles.insert(writer);
  }

void OvmsCommandApp::DeregisterConsole(OvmsWriter* writer)
  {
  m_consoles.erase(writer);
  }

int OvmsCommandApp::Log(const char* fmt, ...)
  {
  va_list args;
  va_start(args, fmt);
  size_t ret = Log(fmt, args);
  va_end(args);
  return ret;
  }

int OvmsCommandApp::Log(const char* fmt, va_list args)
  {
  LogBuffers* lb;
  TaskHandle_t task = xTaskGetCurrentTaskHandle();
  PartialLogs::iterator it = m_partials.find(task);
  if (it == m_partials.end())
    lb = new LogBuffers();
  else
    {
    lb = it->second;
    m_partials.erase(task);
    }
  int ret = LogBuffer(lb, fmt, args);
  lb->set(m_consoles.size());
  for (ConsoleSet::iterator it = m_consoles.begin(); it != m_consoles.end(); ++it)
    {
    (*it)->Log(lb);
    }
  return ret;
  }

int OvmsCommandApp::LogPartial(const char* fmt, ...)
  {
  LogBuffers* lb;
  TaskHandle_t task = xTaskGetCurrentTaskHandle();
  PartialLogs::iterator it = m_partials.find(task);
  if (it == m_partials.end())
    {
    lb = new LogBuffers();
    m_partials[task] = lb;
    }
  else
    {
    lb = it->second;
    }
  va_list args;
  va_start(args, fmt);
  int ret = LogBuffer(lb, fmt, args);
  va_end(args);
  return ret;
  }

int OvmsCommandApp::LogBuffer(LogBuffers* lb, const char* fmt, va_list args)
  {
  char *buffer;
  int ret = vasprintf(&buffer, fmt, args);
  if (ret < 0) return ret;

  // Replace CR/LF except last by "|", but don't leave '|' at the end.
  // An ESC sequence to change color may be appended after the log text.
  char* s;
  for (s=buffer; *s; s++)
    {
    if (*s=='\r' || *s=='\n')
      {
      char *t = s;
      if (*(s+1) == '\033')
        ++s;
      else if (*(s+1) != '\0')
        {
        *s = '|';
        continue;
        }
      while (t > buffer && *(t-1) == '|')
        --t;
      while ((*t++ = *s++)) ;
      break;
      }
    }

  lb->append(buffer);
  return ret;
  }

int OvmsCommandApp::HexDump(const char* tag, const char* prefix, const char* data, size_t length, size_t colsize /*=16*/)
  {
  char* buffer = NULL;
  int rlength = (int)length;

  while (rlength>0)
    {
    rlength = FormatHexDump(&buffer, data, rlength, colsize);
    data += colsize;
    ESP_LOGV(tag, "%s: %s", prefix, buffer);
    }

  if (buffer)
    free(buffer);
  return length;
  }

char ** OvmsCommandApp::Complete(OvmsWriter* writer, int argc, const char * const * argv)
  {
  return m_root.Complete(writer, argc, argv);
  }

void OvmsCommandApp::Execute(int verbosity, OvmsWriter* writer, int argc, const char * const * argv)
  {
  if (argc == 0)
    {
    writer->puts("Error: Empty command unrecognised");
    }
  else
    {
    writer->SetArgv(argv);
    m_root.Execute(verbosity, writer, argc, argv);
    }
  }


/**
 * LogTask: file logging task
 */

struct LogTaskCmd
  {
  enum
    {
    LTC_Log,          // write data.logbuffers to file
    LTC_Exit,         // close file, give data.cmdack, exit
    } type;
  union
    {
    LogBuffers*       logbuffers;
    OvmsSemaphore*    cmdack;
    } data;
  };

static void LogTaskEntry(void* me)
  {
  ((OvmsCommandApp*)me)->LogTask();
  }

void OvmsCommandApp::LogTask()
  {
  LogTaskCmd cmd;
  char tb[64];

  m_logtask_linecnt = 0;
  m_logtask_fsynctime = 0;
  m_logtask_laststamp = -11;
  m_logtask_basetime.tv_sec = 0;
  m_logtask_basetime.tv_usec = 0;

  // syncperiod: 0 = never, <0 = every n lines, >0 = after n/2 seconds idle
  uint32_t linecnt_synced = 0;
  int syncperiod = MyConfig.GetParamValueInt("log", "file.syncperiod", 3);
  TickType_t timeout = (syncperiod<=0) ? portMAX_DELAY : pdMS_TO_TICKS(syncperiod*500);

  for (;;)
    {
    if (xQueueReceive(m_logtask_queue, (void*)&cmd, timeout) == pdTRUE)
      {
      // cmd received:
      if (cmd.type == LogTaskCmd::LTC_Log)
        {
        // write logbuffers messages:
        for (auto it = cmd.data.logbuffers->begin(); it != cmd.data.logbuffers->end(); it++)
          {
          std::string le = stripesc(*it);
          if (*(le.data() + 1) == ' ' && *(le.data() + 2) == '(')
            {
            struct timeval stamp;
            stamp.tv_sec = atoi(le.data() + 3);
            stamp.tv_usec = (stamp.tv_sec % 1000) * 1000;
            stamp.tv_sec /= 1000;
            // If 10 seconds have elapsed since the previous log message or if a
            // real base time hasn't been set yet, recalculate the correspondence
            // of real time to system time.
            if (stamp.tv_sec - m_logtask_laststamp > 10 || m_logtask_basetime.tv_sec < 1609459200)
              {
              struct timeval daytime, uptime;
              gettimeofday(&daytime, NULL);
              uptime.tv_sec = xTaskGetTickCount();
              uptime.tv_usec = (uptime.tv_sec % 100) * 10000;
              uptime.tv_sec /= 100;
              daytime.tv_usec -= daytime.tv_usec % 10000;       // Always show 0 for ms units
              timersub(&daytime, &uptime, &m_logtask_basetime);
              }
            m_logtask_laststamp = stamp.tv_sec;
            // write timestamp:
            timeradd(&m_logtask_basetime, &stamp, &stamp);
            struct tm* tmu = localtime(&stamp.tv_sec);
            strftime(tb, sizeof(tb), "%Y-%m-%d %H:%M:%S", tmu);
            m_logfile_size += fwrite(tb, 1, strlen(tb), m_logfile);
            snprintf(tb, sizeof(tb), ".%03lu ", stamp.tv_usec / 1000);
            int len = strlen(tb);
            strftime(tb+len, sizeof(tb)-len, "%Z ", tmu);
            m_logfile_size += fwrite(tb, 1, strlen(tb), m_logfile);
            }
          // write log entry:
          m_logfile_size += fwrite(le.data(), 1, le.size(), m_logfile);
          m_logtask_linecnt++;
          }
        cmd.data.logbuffers->release();

        // check file size:
        if (m_logfile_maxsize && m_logfile_size > (m_logfile_maxsize*1024))
          {
          if (!CycleLogfile())
            break;
          }
        else if (syncperiod < 0 && m_logtask_linecnt >= linecnt_synced - syncperiod)
          {
          linecnt_synced = m_logtask_linecnt;
          uint32_t t0 = esp_timer_get_time();
          fflush(m_logfile);
          fsync(fileno(m_logfile));
          m_logtask_fsynctime += esp_timer_get_time() - t0;
          }

        // check file status:
        if (ferror(m_logfile))
          {
          ESP_LOGE(TAG, "LogTask: writing to file failed, terminating");
          break;
          }
        }
      else if (cmd.type == LogTaskCmd::LTC_Exit)
        {
        break;
        }
      }
    else
      {
      // cmd timeout: anything to sync?
      if (m_logtask_linecnt != linecnt_synced)
        {
        linecnt_synced = m_logtask_linecnt;
        uint32_t t0 = esp_timer_get_time();
        fflush(m_logfile);
        fsync(fileno(m_logfile));
        m_logtask_fsynctime += esp_timer_get_time() - t0;
        }
      }
    }

  // cleanup & terminate:
  if (m_logfile)
    fclose(m_logfile);
  LogTaskCmd drop;
  while (xQueueReceive(m_logtask_queue, (void*)&drop, 0) == pdTRUE)
    {
    if (drop.type == LogTaskCmd::LTC_Log)
      {
      drop.data.logbuffers->release();
      }
    else if (drop.type == LogTaskCmd::LTC_Exit)
      {
      if (drop.data.cmdack)
        drop.data.cmdack->Give();
      }
    }
  vQueueDelete(m_logtask_queue);
  m_logfile = NULL;
  m_logtask_queue = NULL;
  m_logtask = NULL;
  if (cmd.type == LogTaskCmd::LTC_Exit && cmd.data.cmdack)
    cmd.data.cmdack->Give();
  vTaskDelete(NULL);
  }

bool OvmsCommandApp::StartLogTask(FILE* file)
  {
  OvmsMutexLock lock(&m_logtask_mutex);
  m_logfile = file;
  if (m_logtask)
    return true;
  // create queue:
  m_logtask_dropcnt = 0;
  m_logtask_queue = xQueueCreate(CONFIG_OVMS_LOGFILE_QUEUE_SIZE, sizeof(LogTaskCmd));
  if (!m_logtask_queue)
    {
    ESP_LOGE(TAG, "StartLogTask: unable to create queue (out of memory)");
    return false;
    }
  // create task:
  BaseType_t res = xTaskCreatePinnedToCore(LogTaskEntry, "OVMS FileLog", 3*1024, (void*)this,
    CONFIG_OVMS_LOGFILE_TASK_PRIORITY, &m_logtask, CORE(1));
  if (res != pdPASS)
    {
    ESP_LOGE(TAG, "StartLogTask: unable to create task, error code=%d", res);
    vQueueDelete(m_logtask_queue);
    m_logtask_queue = NULL;
    return false;
    }
  // register as logging console:
  SetMonitoring(true);
  MyCommandApp.RegisterConsole(this);
  return true;
  }

bool OvmsCommandApp::StopLogTask()
  {
  OvmsMutexLock lock(&m_logtask_mutex);
  if (!m_logtask)
    return true;
  // detach from logging:
  SetMonitoring(false);
  MyCommandApp.DeregisterConsole(this);
  // send exit command to task:
  OvmsSemaphore ack;
  LogTaskCmd cmd;
  cmd.type = LogTaskCmd::LTC_Exit;
  cmd.data.cmdack = &ack;
  if (xQueueSend(m_logtask_queue, &cmd, (portTickType)portMAX_DELAY) != pdTRUE)
    {
    ESP_LOGE(TAG, "StopLogTask: unable to send command to task");
    return false;
    }
  // …and wait for it to finish:
  ack.Take();
  return true;
  }

bool OvmsCommandApp::CloseLogfile()
  {
  if (!m_logfile)
    return true;
  if (!StopLogTask())
    return false;
  ESP_LOGI(TAG, "CloseLogfile: file logging stopped");
  return true;
  }

bool OvmsCommandApp::OpenLogfile()
  {
  if (m_logfile && !CloseLogfile())
    return false;
  if (m_logfile_path.empty())
    return true;

#ifdef CONFIG_OVMS_COMP_SDCARD
  if (startsWith(m_logfile_path, "/sd") &&
      (!MyPeripherals || !MyPeripherals->m_sdcard || !MyPeripherals->m_sdcard->isavailable()))
    {
    ESP_LOGW(TAG, "OpenLogfile: cannot open '%s', will retry on SD mount", m_logfile_path.c_str());
    return false;
    }
#endif // #ifdef CONFIG_OVMS_COMP_SDCARD

  // get current file size:
  struct stat st;
  if (stat(m_logfile_path.c_str(), &st) == 0)
    m_logfile_size = st.st_size;
  else
    m_logfile_size = 0;

  // open file, start task:
  FILE* file = fopen(m_logfile_path.c_str(), "a+");
  if (file == NULL)
    {
    ESP_LOGE(TAG, "OpenLogfile: cannot open '%s'", m_logfile_path.c_str());
    return false;
    }
  if (!StartLogTask(file))
    {
    ESP_LOGE(TAG, "OpenLogfile: cannot start log task on '%s'", m_logfile_path.c_str());
    return false;
    }

  ESP_LOGI(TAG, "OpenLogfile: now logging to file '%s'", m_logfile_path.c_str());
  return true;
  }

bool OvmsCommandApp::SetLogfile(std::string path)
  {
  if (path.empty())
    {
    // close:
    if (m_logfile && !CloseLogfile())
      {
      ESP_LOGE(TAG, "SetLogfile: error closing '%s'", m_logfile_path.c_str());
      return false;
      }
    m_logfile_path = "";
    }
  else
    {
    // check new path:
    if (MyConfig.ProtectedPath(path))
      {
      ESP_LOGE(TAG, "SetLogfile: '%s' is a protected path", path.c_str());
      return false;
      }
    // open:
    m_logfile_path = path;
    if (!OpenLogfile())
      return false;
    }
  return true;
  }

bool OvmsCommandApp::CycleLogfile()
  {
  if (!m_logfile || m_logfile_path.empty())
    return false;
  fclose(m_logfile);
  m_logfile = NULL;

  char ts[20];
  time_t tm = time(NULL);
  strftime(ts, sizeof(ts), ".%Y%m%d-%H%M%S", localtime(&tm));
  std::string archpath = m_logfile_path;
  archpath.append(ts);
  if (rename(m_logfile_path.c_str(), archpath.c_str()) == 0)
    {
    ESP_LOGI(TAG, "CycleLogfile: log file '%s' archived as '%s'", m_logfile_path.c_str(), archpath.c_str());
    m_logfile_cyclecnt++;
    }
  else
    {
    ESP_LOGE(TAG, "CycleLogfile: rename log file '%s' to '%s' failed", m_logfile_path.c_str(), archpath.c_str());
    }

  return OpenLogfile();
  }

void OvmsCommandApp::Log(LogBuffers* msg)
  {
  if (!m_logtask || !m_logtask_queue)
    {
    msg->release();
    return;
    }
  // send to LogTask:
  LogTaskCmd cmd;
  cmd.type = LogTaskCmd::LTC_Log;
  cmd.data.logbuffers = msg;
  if (xQueueSend(m_logtask_queue, &cmd, 0) != pdTRUE)
    {
    m_logtask_dropcnt++;
    msg->release();
    }
  }

void OvmsCommandApp::SetLoglevel(std::string tag, std::string level)
  {
  int level_num;
  if (level == "verbose")
    level_num = 5;
  else if (level == "debug")
    level_num = 4;
  else if (level == "info")
    level_num = 3;
  else if (level == "warn")
    level_num = 2;
  else if (level == "error")
    level_num = 1;
  else if (level == "none")
    level_num = 0;
  else
    level_num = CONFIG_LOG_DEFAULT_LEVEL;

  if (tag.empty())
    esp_log_level_set("*", (esp_log_level_t)level_num);
  else
    esp_log_level_set(tag.c_str(), (esp_log_level_t)level_num);
  }

void OvmsCommandApp::ExpireLogFiles(int verbosity, OvmsWriter* writer, int keepdays)
  {
  if (keepdays <= 0)
    {
    if (writer)
      writer->printf("Abort: expire disabled (keepdays=%d)\n", keepdays);
    else
      ESP_LOGD(TAG, "ExpireLogFiles: disabled (keepdays=%d)", keepdays);
    return;
    }

  // get archive directory:
  std::string archdir = m_logfile_path;
  std::string::size_type p = archdir.find_last_of('/');
  if (p == std::string::npos)
    {
    if (writer)
      writer->puts("Error: log path not set");
    else
      ESP_LOGE(TAG, "ExpireLogFiles: log path not set");
    return;
    }
  archdir.resize(p);
  DIR *dir = opendir(archdir.c_str());
  if (!dir)
    {
    if (writer)
      writer->printf("Error: cannot open log directory '%s'\n", archdir.c_str());
    else
      ESP_LOGE(TAG, "ExpireLogFiles: cannot open log directory '%s'", archdir.c_str());
    return;
    }
  else if (writer && verbosity >= COMMAND_RESULT_NORMAL)
    {
    writer->printf("Scanning directory '%s'...\n", archdir.c_str());
    }

  time_t tm = time(NULL) - keepdays * 86400;
  struct dirent *dp;
  char path[PATH_MAX];
  struct stat st;
  int delcnt = 0;

  while ((dp = readdir(dir)) != NULL)
    {
    snprintf(path, sizeof(path), "%s/%s", archdir.c_str(), dp->d_name);
    if (strncmp(path, m_logfile_path.c_str(), m_logfile_path.size()) != 0)
      continue;
    if (stat(path, &st))
      {
      if (writer)
        writer->printf("Error: cannot stat '%s'\n", path);
      else
        ESP_LOGE(TAG, "ExpireLogFiles: cannot stat '%s'", path);
      continue;
      }
    if (st.st_mtime < tm)
      {
      if (writer && verbosity >= COMMAND_RESULT_NORMAL)
        writer->printf("Deleting '%s'\n", path);
      else
        ESP_LOGD(TAG, "ExpireLogFiles: deleting '%s'", path);
      if (unlink(path))
        {
        if (writer)
          writer->printf("Error: cannot delete '%s'\n", path);
        else
          ESP_LOGE(TAG, "ExpireLogFiles: cannot delete '%s'", path);
        }
      else
        delcnt++;
      }
    }

  closedir(dir);

  if (writer)
    writer->printf("Done, %d file(s) deleted.\n", delcnt);
  else
    ESP_LOGI(TAG, "ExpireLogFiles: %d file(s) deleted", delcnt);
  }

void OvmsCommandApp::ExpireTask(void* data)
  {
  int keepdays = MyConfig.GetParamValueInt("log", "file.keepdays", 30);
  MyCommandApp.ExpireLogFiles(0, NULL, keepdays);
  MyCommandApp.m_expiretask = 0;
  vTaskDelete(NULL);
  }

void OvmsCommandApp::ShowLogStatus(int verbosity, OvmsWriter* writer)
  {
  writer->printf(
    "Log listeners      : %u\n"
    "File logging status: %s\n"
    "  Log file path    : %s\n"
    "  Current size     : %.1f kB\n"
    "  Cycle size       : %u kB\n"
    "  Cycle count      : %u\n"
    "  Dropped messages : %u\n"
    "  Messages logged  : %u\n"
    "  Total fsync time : %.1f s\n"
    , m_consoles.size()
    , m_logfile ? "active" : "inactive"
    , m_logfile_path.empty() ? "-" : m_logfile_path.c_str()
    , (float) m_logfile_size / 1024.0f
    , m_logfile_maxsize
    , m_logfile_cyclecnt
    , m_logtask_dropcnt
    , m_logtask_linecnt
    , m_logtask_fsynctime / 1e6);
  }

void OvmsCommandApp::EventHandler(std::string event, void* data)
  {
  if (event == "config.changed")
    {
    OvmsConfigParam* param = (OvmsConfigParam*) data;
    if (param && param->GetName() == "log")
      ReadConfig();
    }
  else if (event == "sd.mounted")
    {
    if (startsWith(m_logfile_path, "/sd"))
      OpenLogfile();
    }
  else if (event == "sd.unmounting")
    {
    if (startsWith(m_logfile_path, "/sd"))
      CloseLogfile();
    }
  else if (event == "ticker.3600")
    {
    int keepdays = MyConfig.GetParamValueInt("log", "file.keepdays", 30);
    time_t utm = time(NULL);
    struct tm* ltm = localtime(&utm);
    if (keepdays && ltm->tm_hour == 0 && !m_expiretask)
      xTaskCreatePinnedToCore(ExpireTask, "OVMS ExpireLogs", 4096, NULL, 0, &m_expiretask, CORE(1));
    }
  }

void OvmsCommandApp::ReadConfig()
  {
  OvmsConfigParam* param = MyConfig.CachedParam("log");

  // configure log levels:
  std::string level = MyConfig.GetParamValue("log", "level");
  if (!level.empty())
    SetLoglevel("*", level);
  for (auto const& kv : param->m_map)
    {
    if (startsWith(kv.first, "level.") && !kv.second.empty())
      SetLoglevel(kv.first.substr(6), kv.second);
    }

  // configure log file:
  m_logfile_maxsize = MyConfig.GetParamValueInt("log", "file.maxsize", 1024);
  if (MyConfig.GetParamValueBool("log", "file.enable", false) == true)
    SetLogfile(MyConfig.GetParamValue("log", "file.path"));
  }

void OvmsCommandApp::Display(OvmsWriter* writer)
  {
  m_root.Display(writer, -1);
  }


OvmsCommandTask::OvmsCommandTask(int _verbosity, OvmsWriter* _writer, OvmsCommand* _cmd, int _argc, const char* const* _argv)
  : TaskBase(_cmd->GetName(), CONFIG_OVMS_SYS_COMMAND_STACK_SIZE, CONFIG_OVMS_SYS_COMMAND_PRIORITY)
  {
  m_state = OCS_Init;

  // clone command arguments:
  verbosity = _verbosity;
  writer = _writer;
  cmd = _cmd;
  argc = _argc;
  if (argc == 0)
    argv = NULL;
  else
    {
    argv = (char**) ExternalRamMalloc(argc * sizeof(char*));
    for (int i=0; i < argc; i++)
      argv[i] = strdup(_argv[i]);
    }
  }

OvmsCommandState_t OvmsCommandTask::Prepare()
  {
  return (writer->IsInteractive()) ? OCS_RunLoop : OCS_RunOnce;
  }

bool OvmsCommandTask::Run()
  {
  m_state = Prepare();
  switch (m_state)
    {
    case OCS_RunLoop:
      // start task:
      writer->RegisterInsertCallback(Terminator, (void*) this);
      if (!Instantiate())
        {
        delete this;
        return false;
        }
      return true;
      break;

    case OCS_RunOnce:
      Service();
      Cleanup();
      delete this;
      return true;
      break;

    default:
      // preparation failed:
      delete this;
      return false;
      break;
    }
  }

OvmsCommandTask::~OvmsCommandTask()
  {
  if (m_state == OCS_StopRequested)
    writer->puts("^C");
  writer->DeregisterInsertCallback(Terminator);
  if (argv)
    {
    for (int i=0; i < argc; i++)
      free(argv[i]);
    free(argv);
    }
  }

bool OvmsCommandTask::Terminator(OvmsWriter* writer, void* userdata, char ch)
  {
  if (ch == 3) // Ctrl-C
    ((OvmsCommandTask*) userdata)->m_state = OCS_StopRequested;
  return true;
  }
