From: larrybr Date: Mon, 28 Mar 2022 21:42:16 +0000 (+0000) Subject: TCL extension polished and more smoothly integrated. A couple of bugs related to... X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=c3a9ba08c1cf336f80b022e61691a0aafc4bdb92;p=thirdparty%2Fsqlite.git TCL extension polished and more smoothly integrated. A couple of bugs related to takedown fixed. Help for added features. Code cleaned up and commented. FossilOrigin-Name: 4ee7df118033af368854992763d96f860f8bd218b28b9ad27dd7ad4291cabfd7 --- diff --git a/ext/misc/tclshext.c.in b/ext/misc/tclshext.c.in index 1e700d1bcd..f9ac21b123 100644 --- a/ext/misc/tclshext.c.in +++ b/ext/misc/tclshext.c.in @@ -16,29 +16,81 @@ -o tclshext.so -ltcl8.6 ** Later TCL versions can be used if desired. ** -** This extension adds two features to the host shell: -** 1. The .tcl dot command is added. -** 2. TCL scripting support is added. -** -** The .tcl command can be run with 0 or more arguments. -** With no arguments, it does a REPL loop until the end of input is seen. -** The end of input is either an EOF condition or a lone '.' on a line. -** With more arguments, files they name are interpreted as TCL script. -** In either case, the TCL command return code is tranlated to a DotCmdRC. +** This extension adds these features to the host shell: +** 1. TCL scripting support is added. +** 2. TCL commands added: udb shdb now_interactive get_tcl_group .. +** 3. The .tcl and .eval dot commands are added. ** ** TCL scripting support is added with a ShellExtensionAPI hookScripting() ** call in the manner documented for it and the ScriptHooks struct. This ** support lasts until the extension destructor is called. Until then, -** shell input groups beginning with ".." are treated as TCL input, one -** complete TCL command at a time. +** shell input groups beginning with ".." are treated as TCL input, in +** various ways, specifically: (1) When a bare ".." is entered, a TCL +** REPL loop is run until the end of input is seen; (2) When "..X ..." +** is entered, (where "X" is a dot-command name), the X dot command will +** be run in its normal fashion, but its arguments will be collected +** according to TCL parsing rules and will be expanded as is usual for +** TCL commands; and (3) when ".. T ..." is entered, (where "T" is a TCL +** command name), that TCL command and its arguments will be collected +** and expanded according to TCL parsing rules, and the command will be +** run in the TCL execution environment (in the root namespace), but the +** shell execution environment remains in effect afterward. (Note that +** cases 2 and 3 differ in whether space occurs after the leading "..".) +** +** The phrase "end of input" means either: end-of-file is seen on a file, +** pipe or string stream input, or a lone "." on the first and only line +** of an input line group is seen. This convention is useful in scripting +** when it is expedient to switch execution environments from within the +** same input stream. For example: + # From shell, enter TCL REPL: + .. + # Initialize some variables and insert into the DB: + set var1 [compute_however ...] + set var2 [derive_somehow ...] + udb eval { INSERT INTO SomeTable(col1, col2) VALUES($var1, var2) } + # Leave REPL + . + # Generate and keep pretty output: + .mode box -ww + .header on + .once prettified.txt + SELECT * FROM SomeTable; + # Alternatively, the query can be run from the TCL environment: + .. + set tstamp [clock format [clock seconds] -format %Y-%m-%d] + .once "backup of prettified.txt made $tstamp" + .eval {SELECT col1, col2 FROM SomeTable} + # Return to shell environment: + . ** ** For any of these ways of providing TCL input, the same TCL interpreter ** is used, with its state maintained from one input to the next. In this ** way, .sqliterc or other preparatory shell scripts (or typing) can be -** made to provide a useful set of user-defined shell enhancements. +** made to provide useful, user-defined shell enhancements or specialized +** procedures (aka "TCL commands") for oft-repeated tasks. +** +** The added TCL commands are: +** udb shdb ; # which expose the user DB and shell DB for access via TCL +** now_interactive ; # which indicates whether input is interactive +** get_tcl_group ; # which gets a single TCL input line group +** .. ; # which does nothing, silently and without error +** The .. command exists so that a lone ".." on an input line suffices +** to ensure the TCL REPL is running. This is symmetric with a lone "." +** input to the TCL REPL because it either terminates the loop or, if +** entered in the shell environment, quietly does nothing without error. +** +** The added .tcl dot command may be used to enter a TCL REPL, or with +** arguments, it will read files as TCL. (This is somewhat extraneous, +** as the same can be done with TCL commands. This is more easily done +** from the shell invocation, and the .tcl command's integration into +** the .help facility provides a way for users to get help for "..".) +** +** The added .eval dot command may be used from the TCL environment to +** run a SQL query or statement and obtain the shell's nice display or +** other disposition of results. While the udb and shdb command allow +** convenient access to the DB(s), they are not setup for display. */ -#include #include "shext_linkage.h" static struct ShExtAPI *pShExtApi = 0; @@ -63,6 +115,7 @@ INCLUDE tclsqlite.c #endif typedef struct TclCmd TclCmd; +typedef struct EvalCmd EvalCmd; static void TclCmd_Takedown(TclCmd *ptc); @@ -72,19 +125,47 @@ static void TclCmd_Takedown(TclCmd *ptc); DERIVED_METHOD(void, destruct, MetaCommand,TclCmd, 0, ()){ TclCmd_Takedown((TclCmd *)pThis); } +DERIVED_METHOD(void, destruct, MetaCommand,EvalCmd, 0, ()){ + (void)(pThis); +} DERIVED_METHOD(const char *, name, MetaCommand,TclCmd, 0,()){ return "tcl"; } +DERIVED_METHOD(const char *, name, MetaCommand,EvalCmd, 0,()){ + return "eval"; +} DERIVED_METHOD(const char *, help, MetaCommand,TclCmd, 1,(int more)){ switch( more ){ case 0: return - ".tcl ?FILES? Run a TCL REPL or interpret files as TCL\n"; + ".tcl ?FILES? Run a TCL REPL or interpret files as TCL.\n"; + case 1: return + " If FILES are provided, they name files to be read in as TCL.\n" + " Otherwise, a read/evaluate/print loop is run until a lone \".\" is\n" + " entered as complete TCL input or input end-of-stream is encountered.\n" + "\n" + " The same REPL can be run with a lone \"..\". Or the \"..\" prefix\n" + " may be used thusly, \"..dotcmd ...\" or \".. tclcmd ...\", to run a\n" + " single dot command or TCL command, respectively, whereupon it will\n" + " be run in its respective execution environment after its arguments\n" + " are collected using TCL parsing rules and expanded as for TCL, in\n" + " the TCL base namespace. In this way, arguments may be \"computed\".\n" + ; + default: return 0; + } +} +DERIVED_METHOD(const char *, help, MetaCommand,EvalCmd, 1,(int more)){ + switch( more ){ + case 0: return + ".eval SQL ... Evaluate given SQL statements as shell does.\n"; case 1: return - " If FILES are provided, they name files to be read in as TCL.\n" - " Otherwise, a read/evaluate/print loop is run until a lone \".\"\n" - " is entered on an input line or end-of-stream is encountered.\n"; + " There is little use for this dot command without the TCL extension,\n" + " as it merely does what can be done in the shell by direct entry of\n" + " the same statements. Its utility is in the TCL environment, where\n" + " statements may be computed or derived in a variety of ways, and\n" + " one wishes to use the shell's result output rendering capability.\n" + ; default: return 0; } } @@ -93,6 +174,10 @@ DERIVED_METHOD(DotCmdRC, argsCheck, MetaCommand,TclCmd, 3, (char **pzErrMsg, int nArgs, char *azArgs[])){ return DCR_Ok; } +DERIVED_METHOD(DotCmdRC, argsCheck, MetaCommand,EvalCmd, 3, + (char **pzErrMsg, int nArgs, char *azArgs[])){ + return DCR_Ok; +} static Tcl_Interp *getInterp(TclCmd *ptc); @@ -103,7 +188,7 @@ static void copy_complaint(char **pzErr, Tcl_Interp *pi){ } } -/* The .tcl REPL script is one of the 3 following string literals, +/* The .tcl/.. REPL script is one of the 3 following string literals, * selected at build time for these different purposes: * 1st: a simple input collection, reading only stdin, which may * be (handily) used as a fallback for debugging purposes. @@ -114,6 +199,7 @@ static void copy_complaint(char **pzErr, Tcl_Interp *pi){ * input collection. It has higher shell dependency, and for * that it gains the shell's line editing and history recall, * in addition to working with the shell's input switching. + * It also supports recursive REPLs when return is caught. */ #ifdef TCL_REPL_STDIN_ONLY # define TCL_REPL 1 @@ -196,18 +282,19 @@ static const char * const zREPL = "namespace delete ::REPL\n" "read stdin 0\n" #elif TCL_REPL==3 +# define SHELL_REPL_CMDNAME "sqlite_shell_REPL" /* using shell's input collection with line editing (if configured) */ - "sqlite_shell_REPL" + SHELL_REPL_CMDNAME #else "throw {NONE} {not built for REPL}\n" #endif ; /* zREPL */ static const char * const zDefineREPL = - "proc sqlite_shell_REPL {} {\n" + "proc "SHELL_REPL_CMDNAME" {} {\n" "set interactive [now_interactive]\n" "while {1} {\n" - "foreach {group ready} [get_input_line_group] {}\n" + "foreach {group ready} [get_tcl_group] {}\n" "set trimmed [string trim $group]\n" "if {$group eq \"\" && !$ready} break\n" "if {$trimmed eq \"\"} continue\n" @@ -231,39 +318,62 @@ static const char * const zDefineREPL = "}\n" ; +/* Enter the preferred REPL */ +static DotCmdRC runTclREPL(Tcl_Interp *interp, char **pzErrMsg){ + int rc = Tcl_Eval(interp, zREPL); + clearerr(stdin); /* Cure issue where stdin gets stuck after keyboard EOF. */ + if( rc!=TCL_OK ){ + copy_complaint(pzErrMsg, interp); + return DCR_Error; + } + return DCR_Ok; +} + DERIVED_METHOD(DotCmdRC, execute, MetaCommand,TclCmd, 4, (ShellExState *psx, char **pzErrMsg, int nArgs, char *azArgs[])){ FILE *out = pExtHelpers->currentOutputFile(psx); TclCmd *ptc = (TclCmd *)pThis; - DotCmdRC rv = DCR_Ok; - int rc = TCL_OK; if( nArgs>1 ){ /* Read named files into the interpreter. */ + int rc = TCL_OK; int aix; for( aix=0; aix<(nArgs-1) && rc==TCL_OK; ++aix ){ rc = Tcl_EvalFile(getInterp(ptc), azArgs[aix+1]); } + if( rc!=TCL_OK ){ + copy_complaint(pzErrMsg, getInterp(ptc)); + return DCR_Error; + } + return DCR_Ok; }else{ /* Enter a REPL */ - rc = Tcl_Eval(getInterp(ptc), zREPL); - clearerr(stdin); /* Cure issue where stdin gets stuck after keyboard EOF. */ + return runTclREPL(getInterp(ptc), pzErrMsg); } - if( rc!=TCL_OK ){ - copy_complaint(pzErrMsg, getInterp(ptc)); - rv = DCR_Error; - } - return rv; +} +DERIVED_METHOD(DotCmdRC, execute, MetaCommand,EvalCmd, 4, + (ShellExState *psx, char **pzErrMsg, int nArgs, char *azArgs[])){ + FILE *out = pExtHelpers->currentOutputFile(psx); + fprintf(out, "The .eval command does nothing, yet.\n"); + return DCR_Ok; } -/* Define a MetaCommand v-table initialized to reference above methods. */ +/* Define MetaCommand v-tables initialized to reference above methods. */ MetaCommand_IMPLEMENT_VTABLE(TclCmd, tclcmd_methods); +MetaCommand_IMPLEMENT_VTABLE(EvalCmd, evalcmd_methods); +/* Static instances are used because that suffices and makes the + * interpreter easy to reference without going through pointers. */ INSTANCE_BEGIN(TclCmd); Tcl_Interp *interp; INSTANCE_END(TclCmd) tclcmd = { &tclcmd_methods , 0 /* interp pointer */ }; +INSTANCE_BEGIN(EvalCmd); + /* no instance data */ +INSTANCE_END(EvalCmd) evalcmd = { + &evalcmd_methods +}; static Tcl_Interp *getInterp(TclCmd *ptc){ return ptc->interp; @@ -275,7 +385,11 @@ static void TclCmd_Takedown(TclCmd *ptc){ Tcl_Finalize(); } -/* Say line is script lead-in iff its first dark is "..". */ +/* Say line is script lead-in iff its first dark is "..". + * In combination with dot commands also being TCL commands and the + * special handling in the next three functions, this effects what is + * promised in this file's header text and by .tcl's help text. + */ static int tclIsScriptLead(void *pvState, const char *zLineLead){ char c; (void)(pvState); @@ -283,11 +397,13 @@ static int tclIsScriptLead(void *pvState, const char *zLineLead){ return (c=='.' && *zLineLead=='.'); } +/* Say line group is complete if it passes muster as ready-to-go TCL. */ static int tclIsComplete(void *pvState, const char *zScript){ (void)(pvState); return Tcl_CommandComplete(zScript); } +/* Run as TCL after some jiggering with the leading dots. */ static DotCmdRC tclRunScript(void *pvState, const char *zScript, ShellExState *p, char **pzErrMsg){ char c; @@ -295,11 +411,40 @@ static DotCmdRC tclRunScript(void *pvState, const char *zScript, while( (c=*zScript++) && (c==' '||c=='\t') ) {} if( c=='.' && *zScript++=='.' ){ int rc, nc = strlen30(zScript); - rc = Tcl_EvalEx(ptc->interp, zScript, nc, TCL_EVAL_DIRECT|TCL_EVAL_GLOBAL); - if( rc!=TCL_OK ) copy_complaint(pzErrMsg, getInterp(ptc)); + /* At this point, *zScript should fall into one of these cases: */ + switch( *zScript ){ + case '.': + /* Three dots, assume user meant to run a dot command. */ + one_shot_tcl: + rc = Tcl_EvalEx(ptc->interp, zScript, /* needs no adjustment */ + nc, TCL_EVAL_DIRECT|TCL_EVAL_GLOBAL); + if( rc!=TCL_OK ) copy_complaint(pzErrMsg, getInterp(ptc)); + break; + case ' ': case '\t': + /* Two dots then whitespace, it's a TCL one-shot command. */ + while( (c = *zScript)!=0 && c==' ' || c=='\t' ) ++zScript, --nc; + if ( c!=0 ) goto one_shot_tcl; + /* It looks like "..", so run it that way via fall-thru. */ + case 0: + /* Two lone dots, user wants to run TCL REPL. */ + return runTclREPL(ptc->interp, pzErrMsg); + default: + /* Two dots then dark not dot, may be a dot command. */ + if( *zScript>='a' && *zScript<='z' ){ + --zScript, ++nc; + goto one_shot_tcl; + } + /* It cannot be a dot command; a user tip is apparently needed. */ + if( pzErrMsg ){ + *pzErrMsg = sqlite3_mprintf("Nothing valid begins with ..%c\n" + "Run .help tcl to see what is valid.\n", + *zScript); + return DCR_SayUsage; + } + } return DCR_Ok|(rc!=TCL_OK); } - return DCR_Error; + return DCR_Error; /* Silent error because it should not happen. */ } #define GETLINE_MAXLEN 1000 @@ -326,7 +471,7 @@ static int getInputLine(void *pvSS, Tcl_Interp *interp, #endif #if TCL_REPL==3 -/* C implementation of TCL proc, get_input_line_group +/* C implementation of TCL proc, get_tcl_group * This routine returns a 2 element list consisting of: * the collected input lines, joined with "\n", as a string * and @@ -565,9 +710,9 @@ static int udbEventHandle(void *pv, NoticeKind nk, void *pvSubject, if( nk==NK_ShutdownImminent ){ udbCleanup(pudb); }else if( nk==NK_Unsubscribe ){ - assert(pudb==0 || (pudb->nRef==0 && pudb->ppSdb==0)); + assert(pudb==0 || pudb->numSdb==0); }else if( nk==NK_DbUserAppeared || nk==NK_DbUserVanishing - || nk==NK_DbAboutToClose ){ + || nk==NK_DbAboutToClose || nk==NK_ExtensionUnload ){ sqlite3 *dbSubject = (sqlite3*)pvSubject; int ix = udbIndexOfDb(pudb, dbSubject); switch( nk ){ @@ -578,6 +723,10 @@ static int udbEventHandle(void *pv, NoticeKind nk, void *pvSubject, case NK_DbUserVanishing: if( ix>=0 ) pudb->ixuSdb = -1; break; + case NK_ExtensionUnload: + pShExtApi->subscribeEvents(psx, sqlite3_tclshext_init, 0, + NK_Unsubscribe, udbEventHandle); + /* fall thru */ case NK_DbAboutToClose: if( ix>=0 ) udbRemove(pudb, ix); break; @@ -703,6 +852,7 @@ int sqlite3_tclshext_init( if( pShExtLink && pShExtLink->pShellExtensionAPI->numRegistrars>=1 ){ ShellExState *psx = pShExtLink->pSXS; MetaCommand *pmc = (MetaCommand *)&tclcmd; + MetaCommand *pec = (MetaCommand *)&evalcmd; const char *zShellName, *zShellDir; int rc; @@ -729,6 +879,7 @@ int sqlite3_tclshext_init( TclCmd_Takedown(&tclcmd); return SQLITE_ERROR; } + rc = pShExtApi->registerMetaCommand(psx, sqlite3_tclshext_init, pec); rc = pShExtApi->registerMetaCommand(psx, sqlite3_tclshext_init, pmc); if( rc==SQLITE_OK ){ ScriptHooks sh = { pmc, tclIsScriptLead, tclIsComplete, tclRunScript }; @@ -746,14 +897,18 @@ int sqlite3_tclshext_init( #endif #if TCL_REPL==3 Tcl_CreateObjCommand(tclcmd.interp, - "get_input_line_group", getInputLineGroup, psx, 0); + "get_tcl_group", getInputLineGroup, psx, 0); Tcl_Eval(tclcmd.interp, zDefineREPL); #endif Tcl_CreateCommand(tclcmd.interp, "now_interactive", nowInteractive, psx, 0); + /* Rename unknown so that calls to it can be intercepted. */ Tcl_Eval(tclcmd.interp, "rename unknown "UNKNOWN_RENAME); Tcl_CreateCommand(tclcmd.interp, "unknown", unknownDotDelegate, psx, 0); + /* Define this proc so that ".." either gets to the TCL REPL loop + * or does nothing (if already in it), as a user convenience. */ + Tcl_Eval(tclcmd.interp, "proc .. {} {}"); pShExtLink->eid = sqlite3_tclshext_init; }else{ TclCmd_Takedown(&tclcmd); diff --git a/manifest b/manifest index 33baad8e45..a213c86ef4 100644 --- a/manifest +++ b/manifest @@ -1,5 +1,5 @@ -C In\sTCL\sshell\sextension,\swrap\sshell\sDB\sfor\suse\sfrom\sTCL. -D 2022-03-28T05:57:07.930 +C TCL\sextension\spolished\sand\smore\ssmoothly\sintegrated.\sA\scouple\sof\sbugs\srelated\sto\stakedown\sfixed.\sHelp\sfor\sadded\sfeatures.\sCode\scleaned\sup\sand\scommented. +D 2022-03-28T21:42:16.234 F .fossil-settings/empty-dirs dbb81e8fc0401ac46a1491ab34a7f2c7c0452f2f06b54ebb845d024ca8283ef1 F .fossil-settings/ignore-glob 35175cdfcf539b2318cb04a9901442804be81cd677d8b889fcc9149c21f239ea F LICENSE.md df5091916dbb40e6e9686186587125e1b2ff51f022cc334e886c19a0e9982724 @@ -329,7 +329,7 @@ F ext/misc/showauth.c 732578f0fe4ce42d577e1c86dc89dd14a006ab52 F ext/misc/spellfix.c 94df9bbfa514a563c1484f684a2df3d128a2f7209a84ca3ca100c68a0163e29f F ext/misc/sqlar.c 0ace5d3c10fe736dc584bf1159a36b8e2e60fab309d310cd8a0eecd9036621b6 F ext/misc/stmt.c 35063044a388ead95557e4b84b89c1b93accc2f1c6ddea3f9710e8486a7af94a -F ext/misc/tclshext.c.in c78715b514667807c152c4cd1bccc054f848ca43f3c3367047e1a0a78cb1c5ce +F ext/misc/tclshext.c.in db6ec20db5c11f8ca808232d569ed9cd6173ae4d5de49ddee891b980d1a181ff F ext/misc/templatevtab.c 8a16a91a5ceaccfcbd6aaaa56d46828806e460dd194965b3f77bf38f14b942c4 F ext/misc/totype.c fa4aedeb07f66169005dffa8de3b0a2b621779fd44f85c103228a42afa71853b F ext/misc/uint.c 053fed3bce2e89583afcd4bf804d75d659879bbcedac74d0fa9ed548839a030b @@ -556,8 +556,8 @@ F src/random.c 097dc8b31b8fba5a9aca1697aeb9fd82078ec91be734c16bffda620ced7ab83c F src/resolve.c ea935b87d6fb36c78b70cdc7b28561dc8f33f2ef37048389549c7b5ef9b0ba5e F src/rowset.c ba9515a922af32abe1f7d39406b9d35730ed65efab9443dc5702693b60854c92 F src/select.c c366c05e48e836ea04f8ecefb9c1225745dc250c3f01bdb39e9cbb0dc25e3610 -F src/shell.c.in c95182f95dc2b1278214812e02fb5131a847851c93b95c548a8a825b87f47aa1 x -F src/shext_linkage.h 68aca955c86a0ef7d1f007a4e776a7e9c17542cfde66195db1d7333695fcef7d +F src/shell.c.in 15c883304f6a757ec9b6c4e2f6720a5d9a2066ed0b88348a36bcf5c1b11c1f7c x +F src/shext_linkage.h c70f95dd0738c2cd8452ab5c47c245d7fa3b99ec26cd160c67dfb4489d5dacf9 F src/sqlite.h.in 2a35f62185eb5e7ecc64a2f68442b538ce9be74f80f28a00abc24837edcf1c17 F src/sqlite3.rc 5121c9e10c3964d5755191c80dd1180c122fc3a8 F src/sqlite3ext.h f49e28c25bd941e79794db5415fdf7b202deb3bc072ed6f1ed273d578703684e @@ -1951,8 +1951,8 @@ F vsixtest/vsixtest.tcl 6a9a6ab600c25a91a7acc6293828957a386a8a93 F vsixtest/vsixtest.vcxproj.data 2ed517e100c66dc455b492e1a33350c1b20fbcdc F vsixtest/vsixtest.vcxproj.filters 37e51ffedcdb064aad6ff33b6148725226cd608e F vsixtest/vsixtest_TemporaryKey.pfx e5b1b036facdb453873e7084e1cae9102ccc67a0 -P fbf0eb0d12932513bba4c6a6ef31d9972d704ab38690c806098504a4cd67786d -R d234ea22bba902ea03fc077d174e9a9f +P 41cc84336bbf7bc64492c24e1bf5be0fccbb8a3db57498070b624af8818d0075 +R a6fcd98d9f55917ef71d90740fb27226 U larrybr -Z cd9ddf3319fb6a9468c36fb31583d848 +Z 385f3d338e553fbadafb70a790af574d # Remove this line to create a well-formed Fossil manifest. diff --git a/manifest.uuid b/manifest.uuid index 4b8f0e7cf7..10d82be7d2 100644 --- a/manifest.uuid +++ b/manifest.uuid @@ -1 +1 @@ -41cc84336bbf7bc64492c24e1bf5be0fccbb8a3db57498070b624af8818d0075 \ No newline at end of file +4ee7df118033af368854992763d96f860f8bd218b28b9ad27dd7ad4291cabfd7 \ No newline at end of file diff --git a/src/shell.c.in b/src/shell.c.in index 961a380e70..99c1a7b4dd 100755 --- a/src/shell.c.in +++ b/src/shell.c.in @@ -7785,6 +7785,7 @@ static int register_meta_command(ShellExState *p, if( rc==SQLITE_DONE ){ psei->ppMetaCommands[nc++] = pMC; psei->numMetaCommands = nc; + notify_subscribers(psi, NK_NewDotCommand, pMC); return SQLITE_OK; }else{ psei->ppMetaCommands[nc] = 0; @@ -7840,7 +7841,7 @@ static int subscribe_events(ShellExState *p, ExtensionId eid, void *pvUserData, if( (eventHandler==0 || eventHandler==pes->eventHandler) && (pes->eid==0 || eid==eid) && (eid!=0 ||eventHandler!=0 ||/* for shell use */ pvUserData==p ) ){ - int nLeft = pesLim - pesLim; + int nLeft = pesLim - pes; assert(pes->eventHandler!=0); pes->eventHandler(pes->pvUserData, NK_Unsubscribe, pes->eid, p); if( nLeft>1 ) memmove(pes, pes+1, (nLeft-1)*sizeof(*pes)); @@ -7877,6 +7878,26 @@ static int subscribe_events(ShellExState *p, ExtensionId eid, void *pvUserData, return SQLITE_OK; } } + +/* + * Unsubscribe all event listeners having an ExtensionId > 0. This is + * done just prior closing the shell DB (when dynamic extensions will + * be unloaded and accessing them in any way is good for a crash.) + */ +static void unsubscribe_extensions(ShellInState *psi){ + ShellExState *psx = XSS(psi); + int esix = 0; + + if( psi->numExtLoaded<=1 ) return; /* Ignore shell pseudo-extension. */ + while( esixnumSubscriptions ){ + struct EventSubscription *pes = psi->pSubscriptions+esix; + if( pes->eid > 0 ){ + int nsin = psi->numSubscriptions; + subscribe_events(psx, pes->eid, psx, NK_Unsubscribe, 0); + esix = esix + 1 + (psi->numSubscriptions - nsin); + }else ++esix; + } +} #endif static FILE *currentOutputFile(ShellExState *p){ @@ -13439,8 +13460,7 @@ static DotCmdRC meta_command_errors(char *zErr, char *azArg[], int nArg, pArg1st = azArg[0]; break; default: - assert(0); - z = "?"; + z = "? Bizarre return from %s"; break; } if( !zErr ){ @@ -15146,9 +15166,20 @@ int SQLITE_CDECL SHELL_MAIN(int argc, wchar_t **wargv){ sqlite3_free(data.zEditor); #if SHELL_DYNAMIC_EXTENSION notify_subscribers(&data, NK_DbAboutToClose, datax.dbShell); + /* It is necessary that the shell DB be closed after the user DBs. + * This is because loaded extensions are held by the shell DB and + * are therefor (automatically) unloaded when it is closed. */ + notify_subscribers(&data, NK_ExtensionUnload, datax.dbShell); + /* Forcefully unsubscribe any extension which ignored above or did + * not unsubscribe upon getting above event. */ + unsubscribe_extensions(&data); + /* This must be done before any extensions unload. */ + free_all_shext_tracking(&data); + /* Unload extensions and free the DB used for dealing with them. */ sqlite3_close(datax.dbShell); + /* This notification can only reach statically linked extensions. */ notify_subscribers(&data, NK_ShutdownImminent, 0); - free_all_shext_tracking(&data); + /* Forcefull unsubscribe static extension event listeners. */ subscribe_events(&datax, 0, &datax, NK_Unsubscribe, 0); #endif #if !SQLITE_SHELL_IS_UTF8 diff --git a/src/shext_linkage.h b/src/shext_linkage.h index bc67988811..4793992af5 100644 --- a/src/shext_linkage.h +++ b/src/shext_linkage.h @@ -277,8 +277,13 @@ typedef enum { * pvSubject is the newly set .dbUser value. */ NK_DbUserVanishing, /* Current ShellExState .dbUser will soon vanish, * pvSubject is the vanishing .dbUser value. */ - NK_DbAboutToClose, /* A ShellExState-visible DB will soon be closed, - * pvSubject is the sqlite3 pointer soon to close. */ + NK_DbAboutToClose, /* A possibly ShellExState-visible DB will soon be + * closed, pvSubject is the DB's sqlite3 pointer. */ + NK_ExtensionUnload, /* The ShellExState .dbShell DB will soon be closed, + * soon to be followed by unloading of all dynamic + * extensions; pvSubject is the DB's sqlite3 pointer. */ + NK_NewDotCommand, /* A new MetaCommand has been registered, pvSubject + * is the just-added MetaCommand object (pointer). */ NK_CountOf /* Present count of preceding members (evolves) */ } NoticeKind;