Browse Source

Merge pull request #1771 from debris/v8console

v8 javascript console
cl-refactor
Gav Wood 10 years ago
parent
commit
d3ee4263bd
  1. 13
      CMakeLists.txt
  2. 10
      cmake/EthDependencies.cmake
  3. 49
      cmake/Findv8.cmake
  4. 8
      eth/CMakeLists.txt
  5. 36
      eth/main.cpp
  6. 30
      libjsconsole/CMakeLists.txt
  7. 86
      libjsconsole/JSConsole.cpp
  8. 53
      libjsconsole/JSConsole.h
  9. 54
      libjsconsole/JSV8Connector.cpp
  10. 50
      libjsconsole/JSV8Connector.h
  11. 36
      libjsengine/CMakeLists.txt
  12. 11
      libjsengine/Common.js
  13. 36
      libjsengine/JSEngine.cpp
  14. 60
      libjsengine/JSEngine.h
  15. 23
      libjsengine/JSPrinter.cpp
  16. 41
      libjsengine/JSPrinter.h
  17. 8
      libjsengine/JSResources.cmake
  18. 187
      libjsengine/JSV8Engine.cpp
  19. 61
      libjsengine/JSV8Engine.h
  20. 49
      libjsengine/JSV8Printer.cpp
  21. 43
      libjsengine/JSV8Printer.h
  22. 84
      libjsengine/JSV8RPC.cpp
  23. 47
      libjsengine/JSV8RPC.h
  24. 91
      libjsengine/PrettyPrint.js
  25. 17
      test/CMakeLists.txt
  26. 5
      test/libjsengine/CMakeLists.txt
  27. 71
      test/libjsengine/JSV8Engine.cpp

13
CMakeLists.txt

@ -40,6 +40,7 @@ option(GUI "Build GUI components (AlethZero, Mix)" ON)
option(TESTS "Build the tests." ON) option(TESTS "Build the tests." ON)
option(EVMJIT "Build just-in-time compiler for EVM code (requires LLVM)" OFF) option(EVMJIT "Build just-in-time compiler for EVM code (requires LLVM)" OFF)
option(ETHASHCL "Build in support for GPU mining via OpenCL" OFF) option(ETHASHCL "Build in support for GPU mining via OpenCL" OFF)
option(JSCONSOLE "Build in javascript console" OFF)
# propagates CMake configuration options to the compiler # propagates CMake configuration options to the compiler
function(configureProject) function(configureProject)
@ -193,9 +194,14 @@ eth_format_option(GUI)
eth_format_option(TESTS) eth_format_option(TESTS)
eth_format_option(TOOLS) eth_format_option(TOOLS)
eth_format_option(ETHASHCL) eth_format_option(ETHASHCL)
eth_format_option(JSCONSOLE)
eth_format_option_on_decent_platform(SERPENT) eth_format_option_on_decent_platform(SERPENT)
eth_format_option_on_decent_platform(NCURSES) eth_format_option_on_decent_platform(NCURSES)
if (JSCONSOLE)
set(JSONRPC ON)
endif()
if (GUI) if (GUI)
set(JSONRPC ON) set(JSONRPC ON)
endif() endif()
@ -284,6 +290,7 @@ message("-- GUI Build GUI components ${GUI}")
message("-- NCURSES Build NCurses components ${NCURSES}") message("-- NCURSES Build NCurses components ${NCURSES}")
message("-- TESTS Build tests ${TESTS}") message("-- TESTS Build tests ${TESTS}")
message("-- ETHASHCL Build OpenCL components (experimental!) ${ETHASHCL}") message("-- ETHASHCL Build OpenCL components (experimental!) ${ETHASHCL}")
message("-- JSCONSOLE Build with javascript console ${JSCONSOLE}")
message("-- EVMJIT Build LLVM-based JIT EVM (experimental!) ${EVMJIT}") message("-- EVMJIT Build LLVM-based JIT EVM (experimental!) ${EVMJIT}")
message("------------------------------------------------------------------------") message("------------------------------------------------------------------------")
message("") message("")
@ -328,6 +335,11 @@ if (JSONRPC)
add_subdirectory(libweb3jsonrpc) add_subdirectory(libweb3jsonrpc)
endif() endif()
if (JSCONSOLE)
add_subdirectory(libjsengine)
add_subdirectory(libjsconsole)
endif()
add_subdirectory(secp256k1) add_subdirectory(secp256k1)
add_subdirectory(libp2p) add_subdirectory(libp2p)
add_subdirectory(libdevcrypto) add_subdirectory(libdevcrypto)
@ -384,6 +396,7 @@ if (GUI)
endif() endif()
#unset(TARGET_PLATFORM CACHE) #unset(TARGET_PLATFORM CACHE)
if (WIN32) if (WIN32)

10
cmake/EthDependencies.cmake

@ -31,7 +31,8 @@ endif()
# homebrew installs qts in opt # homebrew installs qts in opt
if (APPLE) if (APPLE)
set (CMAKE_PREFIX_PATH ${CMAKE_PREFIX_PATH} "/usr/local/opt/qt5") set (CMAKE_PREFIX_PATH "/usr/local/opt/qt5" ${CMAKE_PREFIX_PATH})
set (CMAKE_PREFIX_PATH "/usr/local/opt/v8-315" ${CMAKE_PREFIX_PATH})
endif() endif()
find_program(CTEST_COMMAND ctest) find_program(CTEST_COMMAND ctest)
@ -47,6 +48,13 @@ find_package (LevelDB REQUIRED)
message(" - LevelDB header: ${LEVELDB_INCLUDE_DIRS}") message(" - LevelDB header: ${LEVELDB_INCLUDE_DIRS}")
message(" - LevelDB lib: ${LEVELDB_LIBRARIES}") message(" - LevelDB lib: ${LEVELDB_LIBRARIES}")
if (JSCONSOLE)
find_package (v8 REQUIRED)
message(" - v8 header: ${V8_INCLUDE_DIRS}")
message(" - v8 lib : ${V8_LIBRARIES}")
add_definitions(-DETH_JSCONSOLE)
endif()
# TODO the Jsoncpp package does not yet check for correct version number # TODO the Jsoncpp package does not yet check for correct version number
find_package (Jsoncpp 0.60 REQUIRED) find_package (Jsoncpp 0.60 REQUIRED)
message(" - Jsoncpp header: ${JSONCPP_INCLUDE_DIRS}") message(" - Jsoncpp header: ${JSONCPP_INCLUDE_DIRS}")

49
cmake/Findv8.cmake

@ -0,0 +1,49 @@
# Find v8
#
# Find the v8 includes and library
#
# if you nee to add a custom library search path, do it via via CMAKE_PREFIX_PATH
#
# This module defines
# V8_INCLUDE_DIRS, where to find header, etc.
# V8_LIBRARIES, the libraries needed to use v8.
# V8_FOUND, If false, do not try to use v8.
# only look in default directories
find_path(
V8_INCLUDE_DIR
NAMES v8.h
DOC "v8 include dir"
)
find_library(
V8_LIBRARY
NAMES v8
DOC "v8 library"
)
set(V8_INCLUDE_DIRS ${V8_INCLUDE_DIR})
set(V8_LIBRARIES ${V8_LIBRARY})
# debug library on windows
# same naming convention as in qt (appending debug library with d)
# boost is using the same "hack" as us with "optimized" and "debug"
if ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "MSVC")
find_library(
V8_LIBRARY_DEBUG
NAMES v8d
DOC "v8 debug library"
)
set(V8_LIBRARIES optimized ${V8_LIBRARIES} debug ${V8_LIBRARY_DEBUG})
endif()
# handle the QUIETLY and REQUIRED arguments and set V8_FOUND to TRUE
# if all listed variables are TRUE, hide their existence from configuration view
include(FindPackageHandleStandardArgs)
find_package_handle_standard_args(v8 DEFAULT_MSG
V8_INCLUDE_DIR V8_LIBRARY)
mark_as_advanced (V8_INCLUDE_DIR V8_LIBRARY)

8
eth/CMakeLists.txt

@ -7,6 +7,10 @@ include_directories(BEFORE ..)
include_directories(${Boost_INCLUDE_DIRS}) include_directories(${Boost_INCLUDE_DIRS})
include_directories(${JSON_RPC_CPP_INCLUDE_DIRS}) include_directories(${JSON_RPC_CPP_INCLUDE_DIRS})
if (JSCONSOLE)
include_directories(${V8_INCLUDE_DIRS})
endif()
set(EXECUTABLE eth) set(EXECUTABLE eth)
file(GLOB HEADERS "*.h") file(GLOB HEADERS "*.h")
@ -33,6 +37,10 @@ endif()
target_link_libraries(${EXECUTABLE} webthree) target_link_libraries(${EXECUTABLE} webthree)
target_link_libraries(${EXECUTABLE} ethash) target_link_libraries(${EXECUTABLE} ethash)
if (JSCONSOLE)
target_link_libraries(${EXECUTABLE} jsconsole)
endif()
if (DEFINED WIN32 AND NOT DEFINED CMAKE_COMPILER_IS_MINGW) if (DEFINED WIN32 AND NOT DEFINED CMAKE_COMPILER_IS_MINGW)
eth_copy_dlls("${EXECUTABLE}" MHD_DLLS) eth_copy_dlls("${EXECUTABLE}" MHD_DLLS)
endif() endif()

36
eth/main.cpp

@ -38,6 +38,9 @@
#include <libevm/VMFactory.h> #include <libevm/VMFactory.h>
#include <libethereum/All.h> #include <libethereum/All.h>
#include <libwebthree/WebThree.h> #include <libwebthree/WebThree.h>
#if ETH_JSCONSOLE || !ETH_TRUE
#include <libjsconsole/JSConsole.h>
#endif
#if ETH_READLINE || !ETH_TRUE #if ETH_READLINE || !ETH_TRUE
#include <readline/readline.h> #include <readline/readline.h>
#include <readline/history.h> #include <readline/history.h>
@ -180,6 +183,9 @@ void help()
<< " -v,--verbosity <0 - 9> Set the log verbosity from 0 to 9 (default: 8)." << endl << " -v,--verbosity <0 - 9> Set the log verbosity from 0 to 9 (default: 8)." << endl
<< " -V,--version Show the version and exit." << endl << " -V,--version Show the version and exit." << endl
<< " -h,--help Show this help message and exit." << endl << " -h,--help Show this help message and exit." << endl
#if ETH_JSCONSOLE || !ETH_TRUE
<< " --console Use interactive javascript console" << endl
#endif
; ;
exit(0); exit(0);
} }
@ -407,6 +413,13 @@ void doFarm(MinerType _m, string const& _remote, unsigned _recheckPeriod)
exit(0); exit(0);
} }
void stopMiningAfterXBlocks(eth::Client* _c, unsigned _start, unsigned _mining)
{
if (_c->isMining() && _c->blockChain().details().number - _start == _mining)
_c->stopMining();
this_thread::sleep_for(chrono::milliseconds(100));
}
int main(int argc, char** argv) int main(int argc, char** argv)
{ {
#if 0 #if 0
@ -543,6 +556,9 @@ int main(int argc, char** argv)
unsigned benchmarkTrial = 3; unsigned benchmarkTrial = 3;
unsigned benchmarkTrials = 5; unsigned benchmarkTrials = 5;
// javascript console
bool useConsole = false;
/// Farm params /// Farm params
string farmURL = "http://127.0.0.1:8080"; string farmURL = "http://127.0.0.1:8080";
unsigned farmRecheckPeriod = 500; unsigned farmRecheckPeriod = 500;
@ -894,6 +910,10 @@ int main(int argc, char** argv)
jsonrpc = jsonrpc == -1 ? SensibleHttpPort : jsonrpc; jsonrpc = jsonrpc == -1 ? SensibleHttpPort : jsonrpc;
else if (arg == "--json-rpc-port" && i + 1 < argc) else if (arg == "--json-rpc-port" && i + 1 < argc)
jsonrpc = atoi(argv[++i]); jsonrpc = atoi(argv[++i]);
#endif
#if ETH_JSCONSOLE
else if (arg == "--console")
useConsole = true;
#endif #endif
else if ((arg == "-v" || arg == "--verbosity") && i + 1 < argc) else if ((arg == "-v" || arg == "--verbosity") && i + 1 < argc)
g_logVerbosity = atoi(argv[++i]); g_logVerbosity = atoi(argv[++i]);
@ -1631,12 +1651,20 @@ int main(int argc, char** argv)
unsigned n =c->blockChain().details().number; unsigned n =c->blockChain().details().number;
if (mining) if (mining)
c->startMining(); c->startMining();
while (!g_exit) if (useConsole)
{ {
if ( c->isMining() &&c->blockChain().details().number - n == mining) #if ETH_JSCONSOLE
c->stopMining(); JSConsole console(web3, vector<KeyPair>({sigKey}));
this_thread::sleep_for(chrono::milliseconds(100)); while (!g_exit)
{
console.repl();
stopMiningAfterXBlocks(c, n, mining);
}
#endif
} }
else
while (!g_exit)
stopMiningAfterXBlocks(c, n, mining);
} }
else else
while (!g_exit) while (!g_exit)

30
libjsconsole/CMakeLists.txt

@ -0,0 +1,30 @@
cmake_policy(SET CMP0015 NEW)
# this policy was introduced in cmake 3.0
# remove if, once 3.0 will be used on unix
if (${CMAKE_MAJOR_VERSION} GREATER 2)
# old policy do not use MACOSX_RPATH
cmake_policy(SET CMP0042 OLD)
endif()
set(CMAKE_AUTOMOC OFF)
aux_source_directory(. SRC_LIST)
include_directories(BEFORE ${V8_INCLUDE_DIRS})
include_directories(BEFORE ..)
include_directories(${READLINE_INCLUDE_DIRS})
include_directories(${JSON_RPC_CPP_INCLUDE_DIRS})
set(EXECUTABLE jsconsole)
file(GLOB HEADERS "*.h")
add_library(${EXECUTABLE} ${SRC_LIST} ${HEADERS})
target_link_libraries(${EXECUTABLE} jsengine)
target_link_libraries(${EXECUTABLE} devcore)
target_link_libraries(${EXECUTABLE} ${READLINE_LIBRARIES})
target_link_libraries(${EXECUTABLE} web3jsonrpc)
install( TARGETS ${EXECUTABLE} RUNTIME DESTINATION bin ARCHIVE DESTINATION lib LIBRARY DESTINATION lib )
install( FILES ${HEADERS} DESTINATION include/${EXECUTABLE} )

86
libjsconsole/JSConsole.cpp

@ -0,0 +1,86 @@
/*
This file is part of cpp-ethereum.
cpp-ethereum is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
cpp-ethereum is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with cpp-ethereum. If not, see <http://www.gnu.org/licenses/>.
*/
/** @file JSConsole.cpp
* @author Marek Kotewicz <marek@ethdev.com>
* @date 2015
* Ethereum client.
*/
#include <iostream>
#include <libdevcore/Log.h>
#include <libweb3jsonrpc/WebThreeStubServer.h>
#include "JSConsole.h"
#include "JSV8Connector.h"
#include "libjsconsole/JSConsoleResources.hpp"
// TODO! make readline optional!
#include <readline/readline.h>
#include <readline/history.h>
using namespace std;
using namespace dev;
using namespace dev::eth;
JSConsole::JSConsole(WebThreeDirect& _web3, std::vector<dev::KeyPair> const& _accounts):
m_engine(),
m_printer(m_engine)
{
m_jsonrpcConnector.reset(new JSV8Connector(m_engine));
m_jsonrpcServer.reset(new WebThreeStubServer(*m_jsonrpcConnector.get(), _web3, _accounts));
}
JSConsole::~JSConsole() {}
void JSConsole::repl() const
{
string cmd = "";
g_logPost = [](std::string const& a, char const*) { cout << "\r \r" << a << endl << flush; rl_forced_update_display(); };
bool isEmpty = true;
int openBrackets = 0;
do {
char* buff = readline(promptForIndentionLevel(openBrackets).c_str());
isEmpty = !(buff && *buff);
if (!isEmpty)
{
cmd += string(buff);
cmd += " ";
free(buff);
int open = count(cmd.begin(), cmd.end(), '{');
open += count(cmd.begin(), cmd.end(), '(');
int closed = count(cmd.begin(), cmd.end(), '}');
closed += count(cmd.begin(), cmd.end(), ')');
openBrackets = open - closed;
}
} while (openBrackets > 0);
if (!isEmpty)
{
add_history(cmd.c_str());
auto value = m_engine.eval(cmd.c_str());
string result = m_printer.prettyPrint(value).cstr();
cout << result << endl;
}
}
std::string JSConsole::promptForIndentionLevel(int _i) const
{
if (_i == 0)
return "> ";
return string((_i + 1) * 2, ' ');
}

53
libjsconsole/JSConsole.h

@ -0,0 +1,53 @@
/*
This file is part of cpp-ethereum.
cpp-ethereum is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
cpp-ethereum is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with cpp-ethereum. If not, see <http://www.gnu.org/licenses/>.
*/
/** @file JSConsole.h
* @author Marek Kotewicz <marek@ethdev.com>
* @date 2015
* Ethereum client.
*/
#pragma once
#include <libjsengine/JSV8Engine.h>
#include <libjsengine/JSV8Printer.h>
class WebThreeStubServer;
namespace jsonrpc { class AbstractServerConnector; }
namespace dev
{
namespace eth
{
class JSConsole
{
public:
JSConsole(WebThreeDirect& _web3, std::vector<dev::KeyPair> const& _accounts);
~JSConsole();
void repl() const;
private:
std::string promptForIndentionLevel(int _i) const;
JSV8Engine m_engine;
JSV8Printer m_printer;
std::unique_ptr<WebThreeStubServer> m_jsonrpcServer;
std::unique_ptr<jsonrpc::AbstractServerConnector> m_jsonrpcConnector;
};
}
}

54
libjsconsole/JSV8Connector.cpp

@ -0,0 +1,54 @@
/*
This file is part of cpp-ethereum.
cpp-ethereum is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
cpp-ethereum is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with cpp-ethereum. If not, see <http://www.gnu.org/licenses/>.
*/
/** @file JSV8Connector.cpp
* @author Marek Kotewicz <marek@ethdev.com>
* @date 2015
* Ethereum client.
*/
#include "JSV8Connector.h"
using namespace std;
using namespace dev;
using namespace dev::eth;
bool JSV8Connector::StartListening()
{
return true;
}
bool JSV8Connector::StopListening()
{
return true;
}
bool JSV8Connector::SendResponse(std::string const& _response, void* _addInfo)
{
(void)_addInfo;
m_lastResponse = _response.c_str();
return true;
}
void JSV8Connector::onSend(char const* payload)
{
OnRequest(payload, NULL);
}
JSV8Connector::~JSV8Connector()
{
StopListening();
}

50
libjsconsole/JSV8Connector.h

@ -0,0 +1,50 @@
/*
This file is part of cpp-ethereum.
cpp-ethereum is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
cpp-ethereum is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with cpp-ethereum. If not, see <http://www.gnu.org/licenses/>.
*/
/** @file JSV8Connector.h
* @author Marek Kotewicz <marek@ethdev.com>
* @date 2015
* Ethereum client.
*/
#pragma once
#include <jsonrpccpp/server/abstractserverconnector.h>
#include <libjsengine/JSV8RPC.h>
namespace dev
{
namespace eth
{
class JSV8Connector: public jsonrpc::AbstractServerConnector, public JSV8RPC
{
public:
JSV8Connector(JSV8Engine const& _engine): JSV8RPC(_engine) {}
virtual ~JSV8Connector();
// implement AbstractServerConnector interface
bool StartListening();
bool StopListening();
bool SendResponse(std::string const& _response, void* _addInfo = nullptr);
// implement JSV8RPC interface
void onSend(char const* payload);
};
}
}

36
libjsengine/CMakeLists.txt

@ -0,0 +1,36 @@
cmake_policy(SET CMP0015 NEW)
# this policy was introduced in cmake 3.0
# remove if, once 3.0 will be used on unix
if (${CMAKE_MAJOR_VERSION} GREATER 2)
# old policy do not use MACOSX_RPATH
cmake_policy(SET CMP0042 OLD)
endif()
set(CMAKE_AUTOMOC OFF)
aux_source_directory(. SRC_LIST)
include_directories(BEFORE ${V8_INCLUDE_DIRS})
include_directories(BEFORE ..)
set(EXECUTABLE jsengine)
file(GLOB HEADERS "*.h")
include(EthUtils)
eth_add_resources("${CMAKE_CURRENT_SOURCE_DIR}/JSResources.cmake" "JSRES")
message(STATUS "HERE!!! ${JSRES}")
add_library(${EXECUTABLE} ${SRC_LIST} ${HEADERS} ${JSRES})
# macos brew version of v8 needs to be compiled with libstdc++
# it also needs to be dynamic library
# xcode needs libstdc++ to be explicitly set as it's attribute
if (APPLE)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -stdlib=libstdc++")
set_property(TARGET ${EXECUTABLE} PROPERTY XCODE_ATTRIBUTE_CLANG_CXX_LIBRARY "libstdc++")
endif()
target_link_libraries(${EXECUTABLE} ${V8_LIBRARIES})
install( TARGETS ${EXECUTABLE} RUNTIME DESTINATION bin ARCHIVE DESTINATION lib LIBRARY DESTINATION lib )
install( FILES ${HEADERS} DESTINATION include/${EXECUTABLE} )

11
libjsengine/Common.js

@ -0,0 +1,11 @@
console = {};
console.log = function () {
};
console.warn = function () {
};
console.error = function () {
};
setTimeout = function () {
};

36
libjsengine/JSEngine.cpp

@ -0,0 +1,36 @@
/*
This file is part of cpp-ethereum.
cpp-ethereum is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
cpp-ethereum is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with cpp-ethereum. If not, see <http://www.gnu.org/licenses/>.
*/
/** @file JSEngine.cpp
* @author Marek Kotewicz <marek@ethdev.com>
* @date 2015
* Ethereum client.
*/
#include <string.h>
#include <stdlib.h>
#include "JSEngine.h"
using namespace dev;
using namespace dev::eth;
JSString::JSString(char const* _cstr): m_cstr(strdup(_cstr)) {}
JSString::~JSString()
{
if (m_cstr)
free(m_cstr);
}

60
libjsengine/JSEngine.h

@ -0,0 +1,60 @@
/*
This file is part of cpp-ethereum.
cpp-ethereum is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
cpp-ethereum is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with cpp-ethereum. If not, see <http://www.gnu.org/licenses/>.
*/
/** @file JSEngine.h
* @author Marek Kotewicz <marek@ethdev.com>
* @date 2015
* Ethereum client.
*/
#pragma once
#include <exception>
namespace dev
{
namespace eth
{
class JSException: public std::exception {};
class JSPrintException: public JSException { char const* what() const noexcept { return "Cannot print expression!"; } };
class JSString
{
public:
JSString(char const* _cstr);
~JSString();
char const* cstr() const { return m_cstr; }
private:
char* m_cstr;
};
class JSValue
{
public:
virtual JSString toString() const = 0;
};
template <typename T>
class JSEngine
{
public:
// should be used to evalute javascript expression
virtual T eval(char const* _cstr) const = 0;
};
}
}

23
libjsengine/JSPrinter.cpp

@ -0,0 +1,23 @@
/*
This file is part of cpp-ethereum.
cpp-ethereum is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
cpp-ethereum is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with cpp-ethereum. If not, see <http://www.gnu.org/licenses/>.
*/
/** @file JSPrinter.cpp
* @author Marek Kotewicz <marek@ethdev.com>
* @date 2015
* Ethereum client.
*/
#include "JSPrinter.h"

41
libjsengine/JSPrinter.h

@ -0,0 +1,41 @@
/*
This file is part of cpp-ethereum.
cpp-ethereum is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
cpp-ethereum is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with cpp-ethereum. If not, see <http://www.gnu.org/licenses/>.
*/
/** @file JSPrinter.h
* @author Marek Kotewicz <marek@ethdev.com>
* @date 2015
* Ethereum client.
*/
#pragma once
#include "JSEngine.h"
namespace dev
{
namespace eth
{
template <typename T>
class JSPrinter
{
public:
virtual JSString print(T const& _value) const { return _value.toString(); }
virtual JSString prettyPrint(T const& _value) const { return print(_value); }
};
}
}

8
libjsengine/JSResources.cmake

@ -0,0 +1,8 @@
set(web3 "${CMAKE_CURRENT_LIST_DIR}/../libjsqrc/ethereumjs/dist/web3.js")
set(pretty_print "${CMAKE_CURRENT_LIST_DIR}/PrettyPrint.js")
set(common "${CMAKE_CURRENT_LIST_DIR}/Common.js")
set(ETH_RESOURCE_NAME "JSEngineResources")
set(ETH_RESOURCE_LOCATION "${CMAKE_CURRENT_BINARY_DIR}")
set(ETH_RESOURCES "web3" "pretty_print" "common")

187
libjsengine/JSV8Engine.cpp

@ -0,0 +1,187 @@
/*
This file is part of cpp-ethereum.
cpp-ethereum is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
cpp-ethereum is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with cpp-ethereum. If not, see <http://www.gnu.org/licenses/>.
*/
/** @file JSV8Engine.cpp
* @author Marek Kotewicz <marek@ethdev.com>
* @date 2015
* Ethereum client.
*/
#include <memory>
#include "JSV8Engine.h"
#include "libjsengine/JSEngineResources.hpp"
using namespace std;
using namespace dev;
using namespace dev::eth;
namespace dev
{
namespace eth
{
static char const* toCString(v8::String::Utf8Value const& _value)
{
if (*_value)
return *_value;
throw JSPrintException();
}
// from: https://github.com/v8/v8-git-mirror/blob/master/samples/shell.cc
// v3.15 from: https://chromium.googlesource.com/v8/v8.git/+/3.14.5.9/samples/shell.cc
void reportException(v8::TryCatch* _tryCatch)
{
v8::HandleScope handle_scope;
v8::String::Utf8Value exception(_tryCatch->Exception());
char const* exceptionString = toCString(exception);
v8::Handle<v8::Message> message = _tryCatch->Message();
// V8 didn't provide any extra information about this error; just
// print the exception.
if (message.IsEmpty())
printf("%s\n", exceptionString);
else
{
// Print (filename):(line number): (message).
v8::String::Utf8Value filename(message->GetScriptResourceName());
char const* filenameString = toCString(filename);
int linenum = message->GetLineNumber();
printf("%s:%i: %s\n", filenameString, linenum, exceptionString);
// Print line of source code.
v8::String::Utf8Value sourceline(message->GetSourceLine());
char const* sourcelineString = toCString(sourceline);
printf("%s\n", sourcelineString);
// Print wavy underline (GetUnderline is deprecated).
int start = message->GetStartColumn();
for (int i = 0; i < start; i++)
printf(" ");
int end = message->GetEndColumn();
for (int i = start; i < end; i++)
printf("^");
printf("\n");
v8::String::Utf8Value stackTrace(_tryCatch->StackTrace());
if (stackTrace.length() > 0)
{
char const* stackTraceString = toCString(stackTrace);
printf("%s\n", stackTraceString);
}
}
}
class JSV8Env
{
public:
~JSV8Env()
{
v8::V8::Dispose();
}
};
class JSV8Scope
{
public:
JSV8Scope():
m_handleScope(),
m_context(v8::Context::New(NULL, v8::ObjectTemplate::New())),
m_contextScope(m_context)
{
m_context->Enter();
}
~JSV8Scope()
{
m_context->Exit();
m_context.Dispose();
}
v8::Persistent <v8::Context> const& context() const { return m_context; }
private:
v8::HandleScope m_handleScope;
v8::Persistent <v8::Context> m_context;
v8::Context::Scope m_contextScope;
};
}
}
JSV8Env JSV8Engine::s_env = JSV8Env();
JSString JSV8Value::toString() const
{
if (m_value.IsEmpty())
return "";
else if (m_value->IsUndefined())
return "undefined";
v8::String::Utf8Value str(m_value);
return toCString(str);
}
JSV8Engine::JSV8Engine(): m_scope(new JSV8Scope())
{
JSEngineResources resources;
string common = resources.loadResourceAsString("common");
string web3 = resources.loadResourceAsString("web3");
eval(common.c_str());
eval(web3.c_str());
eval("web3 = require('web3');");
}
JSV8Engine::~JSV8Engine()
{
delete m_scope;
}
JSV8Value JSV8Engine::eval(char const* _cstr) const
{
v8::HandleScope handleScope;
v8::TryCatch tryCatch;
v8::Local<v8::String> source = v8::String::New(_cstr);
v8::Local<v8::String> name(v8::String::New("(shell)"));
v8::ScriptOrigin origin(name);
v8::Handle<v8::Script> script = v8::Script::Compile(source, &origin);
// Make sure to wrap the exception in a new handle because
// the handle returned from the TryCatch is destroyed
if (script.IsEmpty())
{
reportException(&tryCatch);
return v8::Exception::Error(v8::Local<v8::String>::New(tryCatch.Message()->Get()));
}
auto result = script->Run();
if (result.IsEmpty())
{
reportException(&tryCatch);
return v8::Exception::Error(v8::Local<v8::String>::New(tryCatch.Message()->Get()));
}
return result;
}
v8::Handle<v8::Context> const& JSV8Engine::context() const
{
return m_scope->context();
}

61
libjsengine/JSV8Engine.h

@ -0,0 +1,61 @@
/*
This file is part of cpp-ethereum.
cpp-ethereum is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
cpp-ethereum is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with cpp-ethereum. If not, see <http://www.gnu.org/licenses/>.
*/
/** @file JSV8Engine.h
* @author Marek Kotewicz <marek@ethdev.com>
* @date 2015
* Ethereum client.
*/
#pragma once
#include <v8.h>
#include "JSEngine.h"
namespace dev
{
namespace eth
{
class JSV8Env;
class JSV8Scope;
class JSV8Value: public JSValue
{
public:
JSV8Value(v8::Handle<v8::Value> _value): m_value(_value) {}
JSString toString() const;
v8::Handle<v8::Value> const& value() const { return m_value; }
private:
v8::Handle<v8::Value> m_value;
};
class JSV8Engine: public JSEngine<JSV8Value>
{
public:
JSV8Engine();
virtual ~JSV8Engine();
JSV8Value eval(char const* _cstr) const;
v8::Handle<v8::Context> const& context() const;
private:
static JSV8Env s_env;
JSV8Scope* m_scope;
};
}
}

49
libjsengine/JSV8Printer.cpp

@ -0,0 +1,49 @@
/*
This file is part of cpp-ethereum.
cpp-ethereum is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
cpp-ethereum is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with cpp-ethereum. If not, see <http://www.gnu.org/licenses/>.
*/
/** @file JSV8Printer.cpp
* @author Marek Kotewicz <marek@ethdev.com>
* @date 2015
* Ethereum client.
*/
#include <string>
#include "JSV8Printer.h"
#include "libjsengine/JSEngineResources.hpp"
using namespace std;
using namespace dev;
using namespace eth;
JSV8Printer::JSV8Printer(JSV8Engine const& _engine): m_engine(_engine)
{
JSEngineResources resources;
string prettyPrint = resources.loadResourceAsString("pretty_print");
m_engine.eval(prettyPrint.c_str());
}
JSString JSV8Printer::prettyPrint(JSV8Value const& _value) const
{
v8::HandleScope handleScope;
v8::Local<v8::String> pp = v8::String::New("prettyPrint");
v8::Handle<v8::Function> func = v8::Handle<v8::Function>::Cast(m_engine.context()->Global()->Get(pp));
v8::Local<v8::Value> values[1] = {v8::Local<v8::Value>::New(_value.value())};
v8::Local<v8::Value> res = func->Call(func, 1, values);
v8::String::Utf8Value str(res);
if (*str)
return *str;
throw JSPrintException();
}

43
libjsengine/JSV8Printer.h

@ -0,0 +1,43 @@
/*
This file is part of cpp-ethereum.
cpp-ethereum is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
cpp-ethereum is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with cpp-ethereum. If not, see <http://www.gnu.org/licenses/>.
*/
/** @file JSV8Printer.h
* @author Marek Kotewicz <marek@ethdev.com>
* @date 2015
* Ethereum client.
*/
#pragma once
#include "JSPrinter.h"
#include "JSV8Engine.h"
namespace dev
{
namespace eth
{
class JSV8Printer: public JSPrinter<JSV8Value>
{
public:
JSV8Printer(JSV8Engine const& _engine);
JSString prettyPrint(JSV8Value const& _value) const;
private:
JSV8Engine const& m_engine;
};
}
}

84
libjsengine/JSV8RPC.cpp

@ -0,0 +1,84 @@
/*
This file is part of cpp-ethereum.
cpp-ethereum is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
cpp-ethereum is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with cpp-ethereum. If not, see <http://www.gnu.org/licenses/>.
*/
/** @file JSV8RPC.cpp
* @author Marek Kotewicz <marek@ethdev.com>
* @date 2015
* Ethereum client.
*/
#include "JSV8RPC.h"
using namespace dev;
using namespace dev::eth;
namespace dev
{
namespace eth
{
v8::Handle<v8::Value> JSV8RPCSend(v8::Arguments const& _args)
{
v8::Local<v8::String> JSON = v8::String::New("JSON");
v8::Local<v8::String> parse = v8::String::New("parse");
v8::Local<v8::String> stringify = v8::String::New("stringify");
v8::Handle<v8::Object> jsonObject = v8::Handle<v8::Object>::Cast(v8::Context::GetCurrent()->Global()->Get(JSON));
v8::Handle<v8::Function> parseFunc = v8::Handle<v8::Function>::Cast(jsonObject->Get(parse));
v8::Handle<v8::Function> stringifyFunc = v8::Handle<v8::Function>::Cast(jsonObject->Get(stringify));
v8::Local<v8::Object> self = _args.Holder();
v8::Local<v8::External> wrap = v8::Local<v8::External>::Cast(self->GetInternalField(0));
JSV8RPC* that = static_cast<JSV8RPC*>(wrap->Value());
v8::Local<v8::Value> vals[1] = {_args[0]->ToObject()};
v8::Local<v8::Value> stringifiedArg = stringifyFunc->Call(stringifyFunc, 1, vals);
v8::String::Utf8Value str(stringifiedArg);
that->onSend(*str);
v8::Local<v8::Value> values[1] = {v8::String::New(that->lastResponse())};
return parseFunc->Call(parseFunc, 1, values);
}
}
}
JSV8RPC::JSV8RPC(JSV8Engine const& _engine): m_engine(_engine)
{
v8::HandleScope scope;
v8::Local<v8::ObjectTemplate> rpcTemplate = v8::ObjectTemplate::New();
rpcTemplate->SetInternalFieldCount(1);
rpcTemplate->Set(v8::String::New("send"),
v8::FunctionTemplate::New(JSV8RPCSend));
rpcTemplate->Set(v8::String::New("sendAsync"),
v8::FunctionTemplate::New(JSV8RPCSend));
v8::Local<v8::Object> obj = rpcTemplate->NewInstance();
obj->SetInternalField(0, v8::External::New(this));
v8::Local<v8::String> web3 = v8::String::New("web3");
v8::Local<v8::String> setProvider = v8::String::New("setProvider");
v8::Handle<v8::Object> web3object = v8::Handle<v8::Object>::Cast(m_engine.context()->Global()->Get(web3));
v8::Handle<v8::Function> func = v8::Handle<v8::Function>::Cast(web3object->Get(setProvider));
v8::Local<v8::Value> values[1] = {obj};
func->Call(func, 1, values);
m_lastResponse = R"(
{
"id": 1,
"jsonrpc": "2.0",
"error": "Uninitalized JSV8RPC!"
}
)";
}

47
libjsengine/JSV8RPC.h

@ -0,0 +1,47 @@
/*
This file is part of cpp-ethereum.
cpp-ethereum is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
cpp-ethereum is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with cpp-ethereum. If not, see <http://www.gnu.org/licenses/>.
*/
/** @file JSV8RPC.h
* @author Marek Kotewicz <marek@ethdev.com>
* @date 2015
* Ethereum client.
*/
#pragma once
#include <libjsengine/JSV8Engine.h>
namespace dev
{
namespace eth
{
class JSV8RPC
{
public:
JSV8RPC(JSV8Engine const& _engine);
virtual void onSend(char const* _payload) = 0;
char const* lastResponse() const { return m_lastResponse; }
private:
JSV8Engine const& m_engine;
protected:
char const* m_lastResponse;
};
}
}

91
libjsengine/PrettyPrint.js

@ -0,0 +1,91 @@
var prettyPrint = (function () {
function pp(object, indent) {
try {
JSON.stringify(object)
} catch(e) {
return pp(e, indent);
}
var str = "";
if(object instanceof Array) {
str += "[";
for(var i = 0, l = object.length; i < l; i++) {
str += pp(object[i], indent);
if(i < l-1) {
str += ", ";
}
}
str += " ]";
} else if (object instanceof Error) {
str += "\033[31m" + "Error:\033[0m " + object.message;
} else if (object === null) {
str += "\033[1m\033[30m" + "null";
} else if(typeof(object) === "undefined") {
str += "\033[1m\033[30m" + object;
} else if (isBigNumber(object)) {
str += "\033[32m'" + object.toString(10) + "'";
} else if(typeof(object) === "object") {
str += "{\n";
indent += " ";
var last = getFields(object).pop()
getFields(object).forEach(function (k) {
str += indent + k + ": ";
try {
str += pp(object[k], indent);
} catch (e) {
str += pp(e, indent);
}
if(k !== last) {
str += ",";
}
str += "\n";
});
str += indent.substr(2, indent.length) + "}";
} else if(typeof(object) === "string") {
str += "\033[32m'" + object + "'";
} else if(typeof(object) === "number") {
str += "\033[31m" + object;
} else if(typeof(object) === "function") {
str += "\033[35m[Function]";
} else {
str += object;
}
str += "\033[0m";
return str;
}
var redundantFields = [
'valueOf',
'toString',
'toLocaleString',
'hasOwnProperty',
'isPrototypeOf',
'propertyIsEnumerable',
'constructor',
'__defineGetter__',
'__defineSetter__',
'__lookupGetter__',
'__lookupSetter__',
'__proto__'
];
var getFields = function (object) {
var result = Object.getOwnPropertyNames(object);
if (object.constructor && object.constructor.prototype) {
result = result.concat(Object.getOwnPropertyNames(object.constructor.prototype));
}
return result.filter(function (field) {
return redundantFields.indexOf(field) === -1;
});
};
var isBigNumber = function (object) {
return (!!object.constructor && object.constructor.name === 'BigNumber') ||
(typeof BigNumber !== 'undefined' && object instanceof BigNumber)
};
function prettyPrintInner(/* */) {
var args = arguments;
var ret = "";
for(var i = 0, l = args.length; i < l; i++) {
ret += pp(args[i], "") + "\n";
}
return ret;
};
return prettyPrintInner;
})();

17
test/CMakeLists.txt

@ -25,6 +25,11 @@ add_subdirectory(libethereum)
add_subdirectory(libevm) add_subdirectory(libevm)
add_subdirectory(libnatspec) add_subdirectory(libnatspec)
add_subdirectory(libp2p) add_subdirectory(libp2p)
if (JSCONSOLE)
add_subdirectory(libjsengine)
endif()
if (SOLIDITY) if (SOLIDITY)
add_subdirectory(libsolidity) add_subdirectory(libsolidity)
endif () endif ()
@ -41,6 +46,10 @@ include_directories(${Boost_INCLUDE_DIRS})
include_directories(${CRYPTOPP_INCLUDE_DIRS}) include_directories(${CRYPTOPP_INCLUDE_DIRS})
include_directories(${JSON_RPC_CPP_INCLUDE_DIRS}) include_directories(${JSON_RPC_CPP_INCLUDE_DIRS})
if (JSCONSOLE)
include_directories(${V8_INCLUDE_DIRS})
endif()
# search for test names and create ctest tests # search for test names and create ctest tests
enable_testing() enable_testing()
foreach(file ${SRC_LIST}) foreach(file ${SRC_LIST})
@ -65,14 +74,22 @@ target_link_libraries(testeth ${CURL_LIBRARIES})
target_link_libraries(testeth ethereum) target_link_libraries(testeth ethereum)
target_link_libraries(testeth ethcore) target_link_libraries(testeth ethcore)
target_link_libraries(testeth secp256k1) target_link_libraries(testeth secp256k1)
if (JSCONSOLE)
target_link_libraries(testeth jsengine)
endif()
if (SOLIDITY) if (SOLIDITY)
target_link_libraries(testeth solidity) target_link_libraries(testeth solidity)
endif () endif ()
target_link_libraries(testeth testutils) target_link_libraries(testeth testutils)
if (GUI AND NOT JUSTTESTS) if (GUI AND NOT JUSTTESTS)
target_link_libraries(testeth webthree) target_link_libraries(testeth webthree)
target_link_libraries(testeth natspec) target_link_libraries(testeth natspec)
endif() endif()
if (JSONRPC) if (JSONRPC)
target_link_libraries(testeth web3jsonrpc) target_link_libraries(testeth web3jsonrpc)
target_link_libraries(testeth ${JSON_RPC_CPP_CLIENT_LIBRARIES}) target_link_libraries(testeth ${JSON_RPC_CPP_CLIENT_LIBRARIES})

5
test/libjsengine/CMakeLists.txt

@ -0,0 +1,5 @@
cmake_policy(SET CMP0015 NEW)
aux_source_directory(. SRCS)
add_sources(${SRCS})

71
test/libjsengine/JSV8Engine.cpp

@ -0,0 +1,71 @@
//
// Created by Marek Kotewicz on 27/04/15.
//
#include <boost/test/unit_test.hpp>
#include <libjsengine/JSV8Engine.h>
#include <libjsengine/JSV8Printer.h>
using namespace std;
using namespace dev;
using namespace dev::eth;
BOOST_AUTO_TEST_SUITE(jsv8engine)
BOOST_AUTO_TEST_CASE(evalInteger)
{
JSV8Engine engine;
JSV8Printer printer(engine);
auto value = engine.eval("1 + 1");
string result = printer.print(value).cstr();
BOOST_CHECK_EQUAL(result, "2");
}
BOOST_AUTO_TEST_CASE(evalString)
{
JSV8Engine engine;
JSV8Printer printer(engine);
auto value = engine.eval("'hello ' + 'world'");
string result = printer.print(value).cstr();
BOOST_CHECK_EQUAL(result, "hello world");
}
BOOST_AUTO_TEST_CASE(evalEmpty)
{
JSV8Engine engine;
JSV8Printer printer(engine);
auto value = engine.eval("");
string result = printer.print(value).cstr();
BOOST_CHECK_EQUAL(result, "undefined");
}
BOOST_AUTO_TEST_CASE(evalAssignment)
{
JSV8Engine engine;
JSV8Printer printer(engine);
auto value = engine.eval("x = 5");
string result = printer.print(value).cstr();
BOOST_CHECK_EQUAL(result, "5");
}
BOOST_AUTO_TEST_CASE(evalIncorrectExpression)
{
JSV8Engine engine;
JSV8Printer printer(engine);
auto value = engine.eval("[");
string result = printer.print(value).cstr();
BOOST_CHECK_EQUAL(result, "Error: Uncaught SyntaxError: Unexpected end of input");
}
BOOST_AUTO_TEST_CASE(evalNull)
{
JSV8Engine engine;
JSV8Printer printer(engine);
auto value = engine.eval("null");
string result = printer.print(value).cstr();
string prettyResult = printer.prettyPrint(value).cstr();
BOOST_CHECK_EQUAL(result, "null");
BOOST_CHECK_EQUAL(prettyResult.find("null") != std::string::npos, true);
}
BOOST_AUTO_TEST_SUITE_END()
Loading…
Cancel
Save