diff --git a/docs/dictionary/command/log.lcdoc b/docs/dictionary/command/log.lcdoc new file mode 100644 index 00000000000..0bed4394a5c --- /dev/null +++ b/docs/dictionary/command/log.lcdoc @@ -0,0 +1,52 @@ +Name: log + +Type: command + +Syntax: log [ ] + +Summary: +Invokes the if it is not empty. + +Introduced: 9.5 + +OS: mac, windows, linux, ios, android, html5 + +Platforms: desktop, server, mobile + +Example: +on preOpenStack + -- uBuildMode property set before building standalone + if the uBuildMode of this stack is "release" then + set the logMessage to empty + end if + + loadResources +end preOpenStack + +command loadResources + log "loading resources" +end loadResources + +on log pInfo + -- unhandled put will go to system logs + put pInfo +end log + +Parameters: +argumentList: +A comma separated list of expressions containing the arguments to send. +Arrays are expressions and are valid to send as arguments. + +Description: +The command invokes the handler. When the is the +default value of `log` then the command behaves in the same way as any +other scripted handler. If the is set to empty then the +command does not invoke any handler or evaluate parameters, therefore, allowing +for many logs to be added to scripts for development and an easy low-cost method +to turn the logging off for a release build. The may be set to any +handler name, however, if the handler is not in the message path then use of the + command will throw a `can't find handler` error. + +References: log (command), put (command), msgChanged (message) + +Tags: debugging diff --git a/docs/dictionary/property/logmessage.lcdoc b/docs/dictionary/property/logmessage.lcdoc new file mode 100644 index 00000000000..ff99317357f --- /dev/null +++ b/docs/dictionary/property/logmessage.lcdoc @@ -0,0 +1,47 @@ +Name: logMessage + +Type: property + +Syntax: set the logMessage to + +Summary: +The name of the handler that is called by the command. + +Introduced: 9.5 + +OS: mac, windows, linux, ios, android, html5 + +Platforms: desktop, server, mobile + +Example: +on preOpenStack + -- uBuildMode property set before building standalone + if the uBuildMode of this stack is "release" then + set the logMessage to empty + end if + + loadResources +end preOpenStack + +command loadResources + log "loading resources" +end loadResources + +on log pInfo + -- unhandled put will go to system logs + put pInfo +end log + +Value: +The is the name of the handler called by the command. The +default is `log`. If set to empty then the command does not +invoke any handler or evaluate parameters, therefore, allowing for many logs to +be added to scripts for development and an easy low-cost method to turn the +logging off for a release build. The may be set to any handler name, +however, if the handler is not in the message path then use of the command +will throw a `can't find handler` error. + +References: log (command), put (command), msgChanged (message) + +Tags: debugging + diff --git a/docs/notes/feature-log.md b/docs/notes/feature-log.md new file mode 100644 index 00000000000..b87aa69b6bb --- /dev/null +++ b/docs/notes/feature-log.md @@ -0,0 +1,34 @@ +# New `log` command and `logMessage` property + +A new command (`log`) and global property (`logMessage`) have been added to allow +an easy and low-cost method to disable or redirect script logs. + +The `log` command invokes the handler named by the `logMessage` as though the +`logMessage` were directly written in the script. For backwards compatability +the default value of the `logMessage` is `log` so any scripts that currently +have a `log` handler will continue to work. To allow this `log` has been special +cased as both a command name and a permitted handler name. + +If the `logMessage` is set to empty then the `log` command will not invoke any +handler or evaluate any of the parameters in the argument list. + +In this example the `log` command will not be called with `pInfo` as +`loading resources` when the `uBuildMode` of the stack is `release`: + + on preOpenStack + -- uBuildMode property set before building standalone + if the uBuildMode of this stack is "release" then + set the logMessage to empty + end if + + loadResources + end preOpenStack + + command loadResources + log "loading resources" + end loadResources + + on log pInfo + -- unhandled put will go to system logs + put pInfo + end log \ No newline at end of file diff --git a/engine/src/cmds.h b/engine/src/cmds.h index 5367da84546..b9fd87b4b47 100644 --- a/engine/src/cmds.h +++ b/engine/src/cmds.h @@ -1020,6 +1020,26 @@ class MCDispatchCmd: public MCStatement virtual void exec_ctxt(MCExecContext &ctxt); }; +class MCLogCmd: public MCStatement +{ + MCParameter *params; + struct + { + unsigned container_count : 16; + }; + +public: + MCLogCmd(void) + { + params = nullptr; + container_count = 0; + } + ~MCLogCmd(void); + + virtual Parse_stat parse(MCScriptPoint& sp); + virtual void exec_ctxt(MCExecContext &ctxt); +}; + class MCFocus : public MCStatement { MCChunk *object; diff --git a/engine/src/cmdse.cpp b/engine/src/cmdse.cpp index afe01df83ca..27b4988b2da 100644 --- a/engine/src/cmdse.cpp +++ b/engine/src/cmdse.cpp @@ -527,6 +527,101 @@ void MCDispatchCmd::exec_ctxt(MCExecContext &ctxt) MCKeywordsExecTeardownCommandOrFunction(params); } +MCLogCmd::~MCLogCmd(void) +{ + while(params != NULL) + { + MCParameter *t_param; + t_param = params; + params = params -> getnext(); + delete t_param; + } +} + +Parse_stat MCLogCmd::parse(MCScriptPoint& sp) +{ + initpoint(sp); + + if (getparams(sp, ¶ms) != PS_NORMAL) + { + MCperror -> add(PE_STATEMENT_BADPARAMS, sp); + return PS_ERROR; + } + + /* If there are any parameters then compute the number of containers needed + * to execute the command. */ + if (params != nullptr) + { + container_count = params->count_containers(); + } + + return PS_NORMAL; +} + +// This method follows along the same lines as MCComref::exec +void MCLogCmd::exec_ctxt(MCExecContext &ctxt) +{ + // no-op if logMessage is empty + if (MCNameIsEmpty(MClogmessage)) + { + return; + } + + /* Attempt to allocate the number of containers needed for the call. */ + MCAutoPointer t_containers = new MCContainer[container_count]; + if (!t_containers) + { + ctxt.LegacyThrow(EE_NO_MEMORY); + return; + } + + /* If the argument list is successfully evaluated, then do the dispatch. */ + if (MCKeywordsExecSetupCommandOrFunction(ctxt, + params, + *t_containers, + line, + pos, + false)) + { + if (!ctxt.HasError()) + { + ctxt.SetLineAndPos(line, pos); + MCHandler * t_handler = nullptr; + MCKeywordsExecResolveCommandOrFunction(ctxt, MClogmessage, false, t_handler); + MCKeywordsExecCommandOrFunction(ctxt, t_handler, params, MClogmessage, line, pos, false, false); + } + } + + /* Clean up the evaluated argument list */ + MCKeywordsExecTeardownCommandOrFunction(params); + + if (MCresultmode == kMCExecResultModeReturn) + { + // Do nothing! + } + else if (MCresultmode == kMCExecResultModeReturnValue) + { + // Set 'it' to the result and clear the result + MCAutoValueRef t_value; + if (!MCresult->eval(ctxt, &t_value)) + { + ctxt.Throw(); + return; + } + + ctxt.SetItToValue(*t_value); + ctxt.SetTheResultToEmpty(); + } + else if (MCresultmode == kMCExecResultModeReturnError) + { + // Set 'it' to empty + ctxt.SetItToEmpty(); + // Leave the result as is but make sure we reset the 'return mode' to default. + MCresultmode = kMCExecResultModeReturn; + } +} + + Parse_stat MCMessage::parse(MCScriptPoint &sp) { initpoint(sp); diff --git a/engine/src/debug.cpp b/engine/src/debug.cpp index 3631bee9cc7..735ba7e08cf 100644 --- a/engine/src/debug.cpp +++ b/engine/src/debug.cpp @@ -67,6 +67,7 @@ MCExecContext *MCexecutioncontexts[MAX_CONTEXTS]; uint2 MCnexecutioncontexts = 0; uint2 MCdebugcontext = MAXUINT2; Boolean MCmessagemessages = False; +MCNameRef MClogmessage; //////////////////////////////////////////////////////////////////////////////// diff --git a/engine/src/debug.h b/engine/src/debug.h index 7c311754919..52210ae9083 100644 --- a/engine/src/debug.h +++ b/engine/src/debug.h @@ -97,6 +97,7 @@ extern MCExecContext *MCexecutioncontexts[MAX_CONTEXTS]; extern uint2 MCnexecutioncontexts; extern uint2 MCdebugcontext; extern Boolean MCmessagemessages; +extern MCNameRef MClogmessage; struct MCExecValue; diff --git a/engine/src/exec-debugging.cpp b/engine/src/exec-debugging.cpp index 53069482ed2..32e5df53ee6 100644 --- a/engine/src/exec-debugging.cpp +++ b/engine/src/exec-debugging.cpp @@ -369,6 +369,25 @@ void MCDebuggingSetWatchedVariables(MCExecContext& ctxt, MCStringRef p_value) //////////////////////////////////////////////////////////////////////////////// +void MCDebuggingGetLogMessage(MCExecContext& ctxt, MCStringRef& r_value) +{ + r_value = MCValueRetain(MCNameGetString(MClogmessage)); +} + +void MCDebuggingSetLogMessage(MCExecContext& ctxt, MCStringRef p_value) +{ + MCNewAutoNameRef t_logmessage; + if (!MCNameCreate(p_value, &t_logmessage)) + { + ctxt.Throw(); + return; + } + + MCValueAssign(MClogmessage, *t_logmessage); +} + +//////////////////////////////////////////////////////////////////////////////// + void MCDebuggingExecAssert(MCExecContext& ctxt, int type, bool p_eval_success, bool p_result) { switch(type) diff --git a/engine/src/exec.h b/engine/src/exec.h index 79c04a3eaec..6ee6967e306 100644 --- a/engine/src/exec.h +++ b/engine/src/exec.h @@ -3782,6 +3782,8 @@ void MCDebuggingGetExecutionContexts(MCExecContext& ctxt, MCStringRef& r_value); void MCDebuggingGetWatchedVariables(MCExecContext& ctxt, MCStringRef& r_value); void MCDebuggingSetWatchedVariables(MCExecContext& ctxt, MCStringRef p_value); void MCDebuggingExecPutIntoMessage(MCExecContext& ctxt, MCStringRef value, int where); +void MCDebuggingGetLogMessage(MCExecContext& ctxt, MCStringRef& r_value); +void MCDebuggingSetLogMessage(MCExecContext& ctxt, MCStringRef p_value); /////////// diff --git a/engine/src/globals.cpp b/engine/src/globals.cpp index 809ed0a7672..bb63a229dd1 100644 --- a/engine/src/globals.cpp +++ b/engine/src/globals.cpp @@ -886,6 +886,8 @@ void X_clear_globals(void) #endif MCDateTimeInitialize(); + + MClogmessage = MCNAME("log"); } /* ---------------------------------------------------------------- */ @@ -1546,6 +1548,8 @@ int X_close(void) if (MCcmd != nullptr) MCValueRelease(MCcmd); + MCValueRelease(MClogmessage); + return MCretcode; } diff --git a/engine/src/handler.cpp b/engine/src/handler.cpp index e3a19e897ba..b037170abf1 100644 --- a/engine/src/handler.cpp +++ b/engine/src/handler.cpp @@ -169,8 +169,9 @@ Parse_stat MCHandler::parse(MCScriptPoint &sp, Boolean isprop) const LT *te; // MW-2010-01-08: [[Bug 7792]] Check whether the handler name is a reserved function identifier + // special case log command is a permitted handler name if (t_type != ST_ID || - sp.lookup(SP_COMMAND, te) != PS_NO_MATCH || + (sp.lookup(SP_COMMAND, te) != PS_NO_MATCH && te -> which != S_LOG) || (sp.lookup(SP_FACTOR, te) != PS_NO_MATCH && te -> type == TT_FUNCTION)) { diff --git a/engine/src/lextable.cpp b/engine/src/lextable.cpp index c369b5c7c13..059e6ad316b 100644 --- a/engine/src/lextable.cpp +++ b/engine/src/lextable.cpp @@ -363,6 +363,7 @@ const LT command_table[] = {"load", TT_STATEMENT, S_LOAD}, {"local", TT_STATEMENT, S_LOCAL}, {"lock", TT_STATEMENT, S_LOCK}, + {"log", TT_STATEMENT, S_LOG}, {"mark", TT_STATEMENT, S_MARK}, {"modal", TT_STATEMENT, S_MODAL}, {"modeless", TT_STATEMENT, S_MODELESS}, @@ -1173,6 +1174,7 @@ const LT factor_table[] = {"lockupdates", TT_PROPERTY, P_LOCK_UPDATES}, {"log10", TT_FUNCTION, F_LOG10}, {"log2", TT_FUNCTION, F_LOG2}, + {"logmessage", TT_PROPERTY, P_LOG_MESSAGE}, {"long", TT_PROPERTY, P_LONG}, {"longfilepath", TT_FUNCTION, F_LONG_FILE_PATH}, {"longwindowtitles", TT_PROPERTY, P_LONG_WINDOW_TITLES}, diff --git a/engine/src/newobj.cpp b/engine/src/newobj.cpp index cc2d42d61b2..c78288206b0 100644 --- a/engine/src/newobj.cpp +++ b/engine/src/newobj.cpp @@ -164,7 +164,9 @@ MCStatement *MCN_new_statement(int2 which) return new MCLocalVariable; case S_LOCK: return new MCLock; - case S_MARK: + case S_LOG: + return new MCLogCmd; + case S_MARK: return new MCMarkCommand; case S_MODAL: return new MCModal; diff --git a/engine/src/parsedef.h b/engine/src/parsedef.h index 9ea5da50953..46fac35da93 100644 --- a/engine/src/parsedef.h +++ b/engine/src/parsedef.h @@ -1105,6 +1105,7 @@ enum Properties { P_EXECUTION_CONTEXTS, P_MESSAGE_MESSAGES, P_WATCHED_VARIABLES, + P_LOG_MESSAGE, P_ALLOW_INLINE_INPUT, P_SSL_CERTIFICATES, P_HIDE_BACKDROP, @@ -2061,6 +2062,7 @@ enum Statements { S_LOCAL, S_LOAD, S_LOCK, + S_LOG, S_MARK, S_MODAL, S_MODELESS, diff --git a/engine/src/property.cpp b/engine/src/property.cpp index 3f55ac4f62b..bd61a6b7776 100644 --- a/engine/src/property.cpp +++ b/engine/src/property.cpp @@ -362,6 +362,7 @@ static MCPropertyInfo kMCPropertyInfoTable[] = DEFINE_RO_PROPERTY(P_EXECUTION_CONTEXTS, String, Debugging, ExecutionContexts) DEFINE_RW_PROPERTY(P_BREAK_POINTS, String, Debugging, Breakpoints) DEFINE_RW_PROPERTY(P_WATCHED_VARIABLES, String, Debugging, WatchedVariables) + DEFINE_RW_PROPERTY(P_LOG_MESSAGE, String, Debugging, LogMessage) DEFINE_RW_ARRAY_PROPERTY(P_CLIPBOARD_DATA, Any, Pasteboard, ClipboardData) DEFINE_RW_ARRAY_PROPERTY(P_DRAG_DATA, Any, Pasteboard, DragData) @@ -860,6 +861,7 @@ Parse_stat MCProperty::parse(MCScriptPoint &sp, Boolean the) case P_EXECUTION_CONTEXTS: case P_MESSAGE_MESSAGES: case P_WATCHED_VARIABLES: + case P_LOG_MESSAGE: case P_ALLOW_INLINE_INPUT: case P_ACCEPT_DROP: case P_ALLOWABLE_DRAG_ACTIONS: diff --git a/tests/_testlib.livecodescript b/tests/_testlib.livecodescript index de34591a5d7..7c436a50e90 100644 --- a/tests/_testlib.livecodescript +++ b/tests/_testlib.livecodescript @@ -610,18 +610,21 @@ on TestRepeat pDesc, pHandler, pTarget, pTimeOut, pParamsArray end if end TestRepeat -on TestRunStack pOptions, pStackFilePath +command TestRunStack pOptions, pStackFilePath, pArgs local tEnginePath set the itemDelimiter to ":" put item 2 of the address into tEnginePath local tCommand - put format("\"%s\" %s \"%s\"", tEnginePath, pOptions, pStackFilePath) into tCommand + put format("\"%s\" %s \"%s\" %s", tEnginePath, pOptions, pStackFilePath, pArgs) into tCommand local tOutput put shell(tCommand) into tOutput - - return the result + if the result is not empty then + return the result for error + else + return tOutput for value + end if end TestRunStack function TestIsInStandalone diff --git a/tests/lcs/core/debugging/_log.lc b/tests/lcs/core/debugging/_log.lc new file mode 100644 index 00000000000..f3e8b88a98e --- /dev/null +++ b/tests/lcs/core/debugging/_log.lc @@ -0,0 +1,18 @@ +#! + +set the logMessage to $0 +log $1 +quit 0 + +command log pValue + write "Error: this should not be called" & return to stderr + quit 1 +end log + +private command logPublic pValue + write pValue & return to stdout +end logPublic + +private command logPrivate pValue + write pValue & return to stdout +end logPrivate diff --git a/tests/lcs/core/debugging/_log.livecodescript b/tests/lcs/core/debugging/_log.livecodescript new file mode 100644 index 00000000000..f6517934ccf --- /dev/null +++ b/tests/lcs/core/debugging/_log.livecodescript @@ -0,0 +1,36 @@ +script "CoreDebugging_Log" +/* +Copyright (C) 2017 LiveCode Ltd. + +This file is part of LiveCode. + +LiveCode is free software; you can redistribute it and/or modify it under +the terms of the GNU General Public License v3 as published by the Free +Software Foundation. + +LiveCode 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 LiveCode. If not see . */ + +on startup + set the logMessage to $2 + log $3 + quit 0 +end startup + +command log pValue + write "Error: this should not be called" & return to stderr + quit 1 +end log + +command logPublic pValue + write pValue & return to stdout +end logPublic + +private command logPrivate pValue + write pValue & return to stdout +end logPrivate diff --git a/tests/lcs/core/debugging/log.livecodescript b/tests/lcs/core/debugging/log.livecodescript new file mode 100644 index 00000000000..7132bc6204c --- /dev/null +++ b/tests/lcs/core/debugging/log.livecodescript @@ -0,0 +1,56 @@ +script "log" +/* +Copyright (C) 2019 LiveCode Ltd. + +This file is part of LiveCode. + +LiveCode is free software; you can redistribute it and/or modify it under +the terms of the GNU General Public License v3 as published by the Free +Software Foundation. + +LiveCode 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 LiveCode. If not see . */ + +on TestLog + local tStackToRun + put the effective filename of me into tStackToRun + set the itemdelimiter to slash + if the environment is "server" then + put "_log.lc" into item -1 of tStackToRun + else + put "_log.livecodescript" into item -1 of tStackToRun + end if + + local tOptions + if the environment contains "command line" then + put "-ui" into tOptions + end if + set the itemdelimiter to comma + + local tArgs, tResult, tUUID + put uuid() into tUUID + repeat for each item tLogMessage in "logPublic,logPrivate" + put tLogMessage && tUUID into tArgs + TestRunStack tOptions, tStackToRun, tArgs + if the result is not empty then + TestDiagnostic the result + else + TestDiagnostic it + end if + TestAssert "log message" && tLogMessage, tUUID is in it + end repeat + + put quote & quote && tUUID into tArgs + TestRunStack tOptions, tStackToRun, tArgs + if the result is not empty then + TestDiagnostic the result + else + TestDiagnostic it + end if + TestAssert "log message" && tLogMessage, tUUID is not in it +end TestLog