Commit 43dcfd16 authored by Philippe Gorley's avatar Philippe Gorley

build: add symbol visibility support

Changes default visibility of symbols to hidden and makes only the API
public. This patch aims to provide not only a stable API, but a stable
ABI as well to any users of libring.

This decreases the number of exported symbols significantly, benefitting
dynamic linking time. Also decreases the chances of a symbol collision,
and enables compiler optimizations.

Bumps version to 6.0.0; clients must be recompiled.

Change-Id: I5b639a6c0933af3021e40369b2e80d9a0b825e89
parent 7d128fac
......@@ -25,6 +25,7 @@
#include <map>
#include <string>
#include "dring/def.h"
#include "dbus_cpp.h"
#if __GNUC__ >= 5 || (__GNUC__ >=4 && __GNUC_MINOR__ >= 6)
......@@ -45,7 +46,7 @@
#include <stdexcept>
class DBusCallManager :
class DRING_PUBLIC DBusCallManager :
public cx::ring::Ring::CallManager_adaptor,
public DBus::IntrospectableAdaptor,
public DBus::ObjectAdaptor
......
......@@ -25,6 +25,7 @@
#include "config.h"
#endif // HAVE_CONFIG_H
#include "dring/def.h"
#include "dring.h"
#include <memory>
......@@ -43,7 +44,7 @@ namespace DBus {
class DefaultTimeout;
}
class DBusClient {
class DRING_PUBLIC DBusClient {
public:
DBusClient(int flags, bool persistent);
~DBusClient();
......
......@@ -29,6 +29,7 @@
#include <map>
#include <string>
#include "dring/def.h"
#include "dbus_cpp.h"
#include "dring/datatransfer_interface.h"
......@@ -51,7 +52,7 @@
using RingDBusMessage = DBus::Struct<std::string, std::map<std::string, std::string>, uint64_t>;
class DBusConfigurationManager :
class DRING_PUBLIC DBusConfigurationManager :
public cx::ring::Ring::ConfigurationManager_adaptor,
public DBus::IntrospectableAdaptor,
public DBus::ObjectAdaptor
......
......@@ -21,6 +21,8 @@
#ifndef __RING_DBUSINSTANCE_H__
#define __RING_DBUSINSTANCE_H__
#include "dring/def.h"
#include <functional>
#if __GNUC__ >= 5 || (__GNUC__ >=4 && __GNUC_MINOR__ >= 6)
......@@ -37,7 +39,7 @@
#pragma GCC diagnostic warning "-Wunused-but-set-variable"
#endif
class DBusInstance :
class DRING_PUBLIC DBusInstance :
public cx::ring::Ring::Instance_adaptor,
public DBus::IntrospectableAdaptor,
public DBus::ObjectAdaptor
......
......@@ -25,6 +25,7 @@
#include <map>
#include <string>
#include "dring/def.h"
#include "dbus_cpp.h"
#if __GNUC__ >= 5 || (__GNUC__ >=4 && __GNUC_MINOR__ >= 6)
......@@ -43,7 +44,7 @@
#pragma GCC diagnostic warning "-Wunused-but-set-variable"
#endif
class DBusPresenceManager :
class DRING_PUBLIC DBusPresenceManager :
public cx::ring::Ring::PresenceManager_adaptor,
public DBus::IntrospectableAdaptor,
public DBus::ObjectAdaptor
......
......@@ -23,6 +23,7 @@
#include <map>
#include <string>
#include "dring/def.h"
#include "dbus_cpp.h"
#if __GNUC__ >= 5 || (__GNUC__ >=4 && __GNUC_MINOR__ >= 6)
......@@ -41,7 +42,7 @@
#pragma GCC diagnostic warning "-Wunused-but-set-variable"
#endif
class DBusVideoManager :
class DRING_PUBLIC DBusVideoManager :
public cx::ring::Ring::VideoManager_adaptor,
public DBus::IntrospectableAdaptor,
public DBus::ObjectAdaptor
......
......@@ -2,7 +2,7 @@ dnl Ring - configure.ac for automake 1.9 and autoconf 2.59
dnl Process this file with autoconf to produce a configure script.
AC_PREREQ([2.65])
AC_INIT([Ring Daemon],[5.2.0],[ring@gnu.org],[ring])
AC_INIT([Ring Daemon],[6.0.0],[ring@gnu.org],[ring])
AC_COPYRIGHT([[Copyright (c) Savoir-faire Linux 2004-2018]])
AC_REVISION([$Revision$])
......@@ -191,6 +191,18 @@ AC_COMPILE_IFELSE(
AC_MSG_RESULT([$CLANG])
dnl define DRING_BUILD because we are building libring, not using it
dnl if building shared library, define dring_EXPORTS
AC_MSG_CHECKING([if compiling shared library])
CPPFLAGS="${CPPFLAGS} -fvisibility=hidden -DDRING_BUILD "
AS_IF([test "x$enable_shared" == "xyes"], [
RING_SHARED=yes
CPPFLAGS="${CPPFLAGS} -Ddring_EXPORTS "
],[
RING_SHARED=no
])
AC_MSG_RESULT([$RING_SHARED])
dnl
dnl Check for the contrib directory
dnl
......
......@@ -31,7 +31,7 @@ PROJECT_NAME = "Ring Daemon"
# This could be handy for archiving the generated documentation or
# if some version control system is used.
PROJECT_NUMBER = 5.2.0
PROJECT_NUMBER = 6.0.0
# Using the PROJECT_BRIEF tag one can provide an optional one line description
# for a project that appears at the top of each page and should give viewer
......
......@@ -165,7 +165,8 @@ nobase_include_HEADERS= dring/dring.h \
dring/account_const.h \
dring/call_const.h \
dring/presence_const.h \
dring/media_const.h
dring/media_const.h \
dring/def.h
if RING_VIDEO
nobase_include_HEADERS+= \
......
......@@ -21,6 +21,8 @@
#ifndef DRING_ACCOUNT_H
#define DRING_ACCOUNT_H
#include "def.h"
//Defined in windows.h
#ifdef ERROR
#undef ERROR
......
......@@ -20,6 +20,8 @@
#ifndef DRING_CALL_H
#define DRING_CALL_H
#include "def.h"
namespace DRing {
namespace Call {
......
This diff is collapsed.
This diff is collapsed.
......@@ -21,6 +21,8 @@
#ifndef DRING_DATATRANSFERI_H
#define DRING_DATATRANSFERI_H
#include "def.h"
#include "dring.h"
#include <string>
......@@ -31,12 +33,12 @@
namespace DRing {
[[deprecated("Replaced by registerSignalHandlers")]]
[[deprecated("Replaced by registerSignalHandlers")]] DRING_PUBLIC
void registerDataXferHandlers(const std::map<std::string, std::shared_ptr<CallbackWrapperBase>>&);
using DataTransferId = uint64_t;
enum class DataTransferEventCode : uint32_t
enum class DRING_PUBLIC DataTransferEventCode : uint32_t
{
invalid=0,
created,
......@@ -52,7 +54,7 @@ enum class DataTransferEventCode : uint32_t
timeout_expired,
};
enum class DataTransferError : uint32_t
enum class DRING_PUBLIC DataTransferError : uint32_t
{
success=0,
unknown,
......@@ -61,12 +63,12 @@ enum class DataTransferError : uint32_t
};
/// Bit definition for DataTransferInfo.flags field
enum class DataTransferFlags
enum class DRING_PUBLIC DataTransferFlags
{
direction=0, ///< 0: outgoing, 1: incoming
};
struct DataTransferInfo
struct DRING_PUBLIC DataTransferInfo
{
std::string accountId; ///< Identifier of the emiter/receiver account
DataTransferEventCode lastEvent { DataTransferEventCode::invalid }; ///< Latest event code sent to the user
......@@ -79,7 +81,7 @@ struct DataTransferInfo
std::string mimetype; ///< MimeType of transferred data (https://www.iana.org/assignments/media-types/media-types.xhtml)
};
std::vector<DataTransferId> dataTransferList() noexcept;
DRING_PUBLIC std::vector<DataTransferId> dataTransferList() noexcept;
/// Asynchronously send a file to a peer using given account connection.
///
......@@ -105,7 +107,7 @@ std::vector<DataTransferId> dataTransferList() noexcept;
/// the processing is asynchronous. Application will be signaled throught DataTransferEvent signal
/// for such event. There is no reserved or special values on DataTransferId type.
///
DataTransferError sendFile(const DataTransferInfo& info, DataTransferId& id) noexcept;
DRING_PUBLIC DataTransferError sendFile(const DataTransferInfo& info, DataTransferId& id) noexcept;
/// Accept an incoming file transfer.
///
......@@ -122,7 +124,7 @@ DataTransferError sendFile(const DataTransferInfo& info, DataTransferId& id) noe
/// \return DataTransferError::invalid_argument if id is unknown.
/// \note unknown \a id results to a no-op call.
///
DataTransferError acceptFileTransfer(const DataTransferId& id, const std::string& file_path,
DRING_PUBLIC DataTransferError acceptFileTransfer(const DataTransferId& id, const std::string& file_path,
int64_t offset) noexcept;
/// Refuse or abort an outgoing or an incoming file transfer.
......@@ -137,7 +139,7 @@ DataTransferError acceptFileTransfer(const DataTransferId& id, const std::string
/// \return DataTransferError::invalid_argument if id is unknown.
/// \note unknown \a id results to a no-op call.
///
DataTransferError cancelDataTransfer(const DataTransferId& id) noexcept;
DataTransferError cancelDataTransfer(const DataTransferId& id) noexcept DRING_PUBLIC;
/// Return some information on given data transfer.
///
......@@ -147,7 +149,7 @@ DataTransferError cancelDataTransfer(const DataTransferId& id) noexcept;
/// \return DataTransferError::invalid_argument if id is unknown.
/// \note \a info structure is in undefined state in case of error.
///
DataTransferError dataTransferInfo(const DataTransferId& id, DataTransferInfo& info) noexcept;
DRING_PUBLIC DataTransferError dataTransferInfo(const DataTransferId& id, DataTransferInfo& info) noexcept;
/// Return the amount of sent/received bytes of an existing data transfer.
///
......@@ -158,13 +160,13 @@ DataTransferError dataTransferInfo(const DataTransferId& id, DataTransferInfo& i
/// \return DataTransferError::success if \a total and \a progress is set with valid values.
/// DataTransferError::invalid_argument if the id is unknown.
///
DataTransferError dataTransferBytesProgress(const DataTransferId& id, int64_t& total,
DRING_PUBLIC DataTransferError dataTransferBytesProgress(const DataTransferId& id, int64_t& total,
int64_t& progress) noexcept;
// Signals
struct DataTransferSignal
struct DRING_PUBLIC DataTransferSignal
{
struct DataTransferEvent
struct DRING_PUBLIC DataTransferEvent
{
constexpr static const char* name = "DataTransferEvent";
using cb_type = void(const DataTransferId& transferId, int eventCode);
......
/*
* Copyright (C) 2018 Savoir-faire Linux Inc.
*
* Author: Adrien Béraud <adrien.beraud@savoirfairelinux.com>
* Author: Philippe Gorley <philippe.gorley@savoirfairelinux.com>
*
* This program 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.
*
* This program 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 this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
#pragma once
// Generic helper definitions for shared library support
#if defined _WIN32 || defined __CYGWIN__
#define DRING_IMPORT __declspec(dllimport)
#define DRING_EXPORT __declspec(dllexport)
#define DRING_HIDDEN
#else
#define DRING_IMPORT __attribute__ ((visibility ("default")))
#define DRING_EXPORT __attribute__ ((visibility ("default")))
#define DRING_HIDDEN __attribute__ ((visibility ("hidden")))
#endif
// Now we use the generic helper definitions above to define DRING_PUBLIC and DRING_LOCAL.
// DRING_PUBLIC is used for the public API symbols. It is either DLL imports or DLL exports (or does nothing for static build)
// DRING_LOCAL is used for non-api symbols.
#ifdef dring_EXPORTS // defined if DRing is compiled as a shared library
#ifdef DRING_BUILD // defined if we are building the DRing shared library (instead of using it)
#define DRING_PUBLIC DRING_EXPORT
#else
#define DRING_PUBLIC DRING_IMPORT
#endif // DRING_BUILD
#define DRING_LOCAL DRING_HIDDEN
#else // dring_EXPORTS is not defined: this means DRing is a static lib.
#define DRING_PUBLIC
#define DRING_LOCAL
#endif // dring_EXPORTS
......@@ -21,6 +21,8 @@
#ifndef DRING_H
#define DRING_H
#include "def.h"
#include <vector>
#include <functional>
#include <string>
......@@ -40,7 +42,7 @@ enum InitFlag {
/**
* Return the library version as string.
*/
const char* version() noexcept;
DRING_PUBLIC const char* version() noexcept;
/**
* Initialize globals, create underlaying daemon.
......@@ -48,18 +50,18 @@ const char* version() noexcept;
* @param flags Flags to customize this initialization
* @returns true if initialization succeed else false.
*/
bool init(enum InitFlag flags) noexcept;
DRING_PUBLIC bool init(enum InitFlag flags) noexcept;
/**
* Start asynchronously daemon created by init().
* @returns true if daemon started successfully
*/
bool start(const std::string& config_file={}) noexcept;
DRING_PUBLIC bool start(const std::string& config_file={}) noexcept;
/**
* Stop and freeing any resource allocated by daemon
*/
void fini() noexcept;
DRING_PUBLIC void fini() noexcept;
/* External Callback Dynamic Utilities
*
......@@ -82,7 +84,7 @@ void fini() noexcept;
* Used conjointly with std::shared_ptr to hide the concrete class.
* See CallbackWrapper template for details.
*/
class CallbackWrapperBase {};
class DRING_PUBLIC CallbackWrapperBase {};
/* Concrete class of CallbackWrapperBase.
* This class wraps callbacks of a specific signature.
......@@ -142,7 +144,7 @@ exportable_callback(std::function<typename Ts::cb_type>&& func) {
(std::forward<std::function<typename Ts::cb_type>>(func)));
}
void registerSignalHandlers(const std::map<std::string, std::shared_ptr<CallbackWrapperBase>>&);
DRING_PUBLIC void registerSignalHandlers(const std::map<std::string, std::shared_ptr<CallbackWrapperBase>>&);
} // namespace DRing
......
......@@ -20,6 +20,8 @@
#ifndef DRING_MEDIA_H
#define DRING_MEDIA_H
#include "def.h"
namespace DRing {
namespace Media {
......
......@@ -20,6 +20,8 @@
#ifndef DRING_PRESENCE_CONST_H
#define DRING_PRESENCE_CONST_H
#include "def.h"
namespace DRing {
namespace Presence {
......
......@@ -21,6 +21,8 @@
#ifndef DRING_PRESENCEMANAGERI_H
#define DRING_PRESENCEMANAGERI_H
#include "def.h"
#include <vector>
#include <map>
#include <string>
......@@ -31,31 +33,31 @@
namespace DRing {
[[deprecated("Replaced by registerSignalHandlers")]]
[[deprecated("Replaced by registerSignalHandlers")]] DRING_PUBLIC
void registerPresHandlers(const std::map<std::string, std::shared_ptr<CallbackWrapperBase>>&);
/* Presence subscription/Notification. */
void publish(const std::string& accountID, bool status, const std::string& note);
void answerServerRequest(const std::string& uri, bool flag);
void subscribeBuddy(const std::string& accountID, const std::string& uri, bool flag);
std::vector<std::map<std::string, std::string>> getSubscriptions(const std::string& accountID);
void setSubscriptions(const std::string& accountID, const std::vector<std::string>& uris);
DRING_PUBLIC void publish(const std::string& accountID, bool status, const std::string& note);
DRING_PUBLIC void answerServerRequest(const std::string& uri, bool flag);
DRING_PUBLIC void subscribeBuddy(const std::string& accountID, const std::string& uri, bool flag);
DRING_PUBLIC std::vector<std::map<std::string, std::string>> getSubscriptions(const std::string& accountID);
DRING_PUBLIC void setSubscriptions(const std::string& accountID, const std::vector<std::string>& uris);
// Presence signal type definitions
struct PresenceSignal {
struct NewServerSubscriptionRequest {
struct DRING_PUBLIC PresenceSignal {
struct DRING_PUBLIC NewServerSubscriptionRequest {
constexpr static const char* name = "NewServerSubscriptionRequest";
using cb_type = void(const std::string& /*remote*/);
};
struct ServerError {
struct DRING_PUBLIC ServerError {
constexpr static const char* name = "ServerError";
using cb_type = void(const std::string& /*account_id*/, const std::string& /*error*/, const std::string& /*msg*/);
};
struct NewBuddyNotification {
struct DRING_PUBLIC NewBuddyNotification {
constexpr static const char* name = "NewBuddyNotification";
using cb_type = void(const std::string& /*account_id*/, const std::string& /*buddy_uri*/, int /*status*/, const std::string& /*line_status*/);
};
struct SubscriptionStateChanged {
struct DRING_PUBLIC SubscriptionStateChanged {
constexpr static const char* name = "SubscriptionStateChanged";
using cb_type = void(const std::string& /*account_id*/, const std::string& /*buddy_uri*/, int /*state*/);
};
......
......@@ -20,6 +20,8 @@
#ifndef DRING_SECURITY_H
#define DRING_SECURITY_H
#include "def.h"
namespace DRing {
namespace Certificate {
......
......@@ -28,6 +28,8 @@ struct AVFrame;
struct AVPacket;
}
#include "def.h"
#include <memory>
#include <vector>
#include <map>
......@@ -43,11 +45,11 @@ struct AVPacket;
namespace DRing {
[[deprecated("Replaced by registerSignalHandlers")]]
[[deprecated("Replaced by registerSignalHandlers")]] DRING_PUBLIC
void registerVideoHandlers(const std::map<std::string, std::shared_ptr<CallbackWrapperBase>>&);
/* FrameBuffer is a generic video frame container */
struct FrameBuffer {
struct DRING_PUBLIC FrameBuffer {
uint8_t* ptr {nullptr}; // data as a plain raw pointer
std::size_t ptrSize {0}; // size in byte of ptr array
int format {0}; // as listed by AVPixelFormat (avutils/pixfmt.h)
......@@ -56,7 +58,7 @@ struct FrameBuffer {
std::vector<uint8_t> storage;
};
struct SinkTarget {
struct DRING_PUBLIC SinkTarget {
using FrameBufferPtr = std::unique_ptr<FrameBuffer>;
std::function<FrameBufferPtr(std::size_t bytes)> pull;
std::function<void(FrameBufferPtr)> push;
......@@ -125,76 +127,76 @@ private:
using VideoCapabilities = std::map<std::string, std::map<std::string, std::vector<std::string>>>;
std::vector<std::string> getDeviceList();
VideoCapabilities getCapabilities(const std::string& name);
std::map<std::string, std::string> getSettings(const std::string& name);
void applySettings(const std::string& name, const std::map<std::string, std::string>& settings);
void setDefaultDevice(const std::string& name);
DRING_PUBLIC std::vector<std::string> getDeviceList();
DRING_PUBLIC VideoCapabilities getCapabilities(const std::string& name);
DRING_PUBLIC std::map<std::string, std::string> getSettings(const std::string& name);
DRING_PUBLIC void applySettings(const std::string& name, const std::map<std::string, std::string>& settings);
DRING_PUBLIC void setDefaultDevice(const std::string& name);
std::map<std::string, std::string> getDeviceParams(const std::string& name);
DRING_PUBLIC std::map<std::string, std::string> getDeviceParams(const std::string& name);
std::string getDefaultDevice();
std::string getCurrentCodecName(const std::string& callID);
void startCamera();
void stopCamera();
bool hasCameraStarted();
bool switchInput(const std::string& resource);
bool switchToCamera();
void registerSinkTarget(const std::string& sinkId, const SinkTarget& target);
DRING_PUBLIC std::string getDefaultDevice();
DRING_PUBLIC std::string getCurrentCodecName(const std::string& callID);
DRING_PUBLIC void startCamera();
DRING_PUBLIC void stopCamera();
DRING_PUBLIC bool hasCameraStarted();
DRING_PUBLIC bool switchInput(const std::string& resource);
DRING_PUBLIC bool switchToCamera();
DRING_PUBLIC void registerSinkTarget(const std::string& sinkId, const SinkTarget& target);
std::string startLocalRecorder(const bool& audioOnly, const std::string& filepath);
void stopLocalRecorder(const std::string& filepath);
DRING_PUBLIC std::string startLocalRecorder(const bool& audioOnly, const std::string& filepath);
DRING_PUBLIC void stopLocalRecorder(const std::string& filepath);
#if defined(__ANDROID__) || defined(RING_UWP) || (defined(TARGET_OS_IOS) && TARGET_OS_IOS)
void addVideoDevice(const std::string &node, const std::vector<std::map<std::string, std::string>>* devInfo=nullptr);
void removeVideoDevice(const std::string &node);
void* obtainFrame(int length);
void releaseFrame(void* frame);
DRING_PUBLIC void addVideoDevice(const std::string &node, const std::vector<std::map<std::string, std::string>>* devInfo=nullptr);
DRING_PUBLIC void removeVideoDevice(const std::string &node);
DRING_PUBLIC void* obtainFrame(int length);
DRING_PUBLIC void releaseFrame(void* frame);
VideoFrame* getNewFrame();
void publishFrame();
DRING_PUBLIC VideoFrame* getNewFrame();
DRING_PUBLIC void publishFrame();
#endif
bool getDecodingAccelerated();
void setDecodingAccelerated(bool state);
DRING_PUBLIC bool getDecodingAccelerated();
DRING_PUBLIC void setDecodingAccelerated(bool state);
// Video signal type definitions
struct VideoSignal {
struct DeviceEvent {
struct DRING_PUBLIC VideoSignal {
struct DRING_PUBLIC DeviceEvent {
constexpr static const char* name = "DeviceEvent";
using cb_type = void(void);
};
struct DecodingStarted {
struct DRING_PUBLIC DecodingStarted {
constexpr static const char* name = "DecodingStarted";
using cb_type = void(const std::string& /*id*/, const std::string& /*shm_path*/, int /*w*/, int /*h*/, bool /*is_mixer*/id);
};
struct DecodingStopped {
struct DRING_PUBLIC DecodingStopped {
constexpr static const char* name = "DecodingStopped";
using cb_type = void(const std::string& /*id*/, const std::string& /*shm_path*/, bool /*is_mixer*/);
};
#if __ANDROID__
struct SetParameters {
struct DRING_PUBLIC SetParameters {
constexpr static const char* name = "SetParameters";
using cb_type = void(const std::string& device, const int format, const int width, const int height, const int rate);
};
struct GetCameraInfo {
struct DRING_PUBLIC GetCameraInfo {
constexpr static const char* name = "GetCameraInfo";
using cb_type = void(const std::string& device, std::vector<int> *formats, std::vector<unsigned> *sizes, std::vector<unsigned> *rates);
};
#endif
struct StartCapture {
struct DRING_PUBLIC StartCapture {
constexpr static const char* name = "StartCapture";
using cb_type = void(const std::string& /*device*/);
};
struct StopCapture {
struct DRING_PUBLIC StopCapture {
constexpr static const char* name = "StopCapture";
using cb_type = void(void);
};
struct DeviceAdded {
struct DRING_PUBLIC DeviceAdded {
constexpr static const char* name = "DeviceAdded";
using cb_type = void(const std::string& /*device*/);
};
struct ParametersChanged {
struct DRING_PUBLIC ParametersChanged {
constexpr static const char* name = "ParametersChanged";
using cb_type = void(const std::string& /*device*/);
};
......
......@@ -27,6 +27,8 @@
#include <mutex>
#include <cstdio>
#include "dring/def.h"
#ifndef _MSC_VER
#define PROTECTED_GETENV(str) ({char *envvar_ = getenv((str)); \
envvar_ ? envvar_ : "";})
......@@ -65,7 +67,7 @@ namespace ring { namespace fileutils {
* @param parents default mode for all created directories except the last
*/
bool check_dir(const char *path, mode_t dir=0755, mode_t parents=0755);
void set_program_dir(char *program_path);
DRING_PUBLIC void set_program_dir(char *program_path); // public because bin/main.cpp uses it
std::string expand_path(const std::string &path);
bool isDirectoryWritable(const std::string &directory);
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment