Index: src/cgi.c ================================================================== --- src/cgi.c +++ src/cgi.c @@ -1745,10 +1745,13 @@ char *z, *zToken; const char *zType = 0; int i, content_length = 0; char zLine[2000]; /* A single line of input. */ +#ifdef FOSSIL_ENABLE_JSON + if( nCycles==0 ){ json_main_bootstrap(); } +#endif if( zIpAddr ){ if( nCycles==0 ){ cgi_setenv("REMOTE_ADDR", zIpAddr); g.zIpAddr = mprintf("%s", zIpAddr); } Index: src/db.c ================================================================== --- src/db.c +++ src/db.c @@ -78,10 +78,16 @@ va_start(ap, zFormat); z = vmprintf(zFormat, ap); va_end(ap); #ifdef FOSSIL_ENABLE_JSON if( g.json.isJsonMode ){ + /* + ** Avoid calling into the JSON support subsystem if it + ** has not yet been initialized, e.g. early SQLite log + ** messages, etc. + */ + if( !json_is_main_boostrapped() ) json_main_bootstrap(); json_err( 0, z, 1 ); } else #endif /* FOSSIL_ENABLE_JSON */ if( g.xferPanic && g.cgiOutput==1 ){ Index: src/json.c ================================================================== --- src/json.c +++ src/json.c @@ -707,10 +707,21 @@ cson_value * json_req_payload_get(char const *pKey){ return g.json.reqPayload.o ? cson_object_get(g.json.reqPayload.o,pKey) : NULL; } + +/* +** Returns non-zero if the json_main_bootstrap() function has already +** been called. In general, this function should be used sparingly, +** e.g. from low-level support functions like fossil_warning() where +** there is genuine uncertainty about whether (or not) the JSON setup +** has already been called. +*/ +int json_is_main_boostrapped(){ + return ((g.json.gc.v != NULL) && (g.json.gc.a != NULL)); +} /* ** Initializes some JSON bits which need to be initialized relatively ** early on. It should only be called from cgi_init() or ** json_cmd_top() (early on in those functions). @@ -931,11 +942,11 @@ ** This must be called by the top-level JSON command dispatching code ** before they do any work. ** ** This must only be called once, or an assertion may be triggered. */ -static void json_mode_bootstrap(){ +void json_mode_bootstrap(){ static char once = 0 /* guard against multiple runs */; char const * zPath = P("PATH_INFO"); assert(g.json.gc.a && "json_main_bootstrap() was not called!"); assert( (0==once) && "json_mode_bootstrap() called too many times!"); if( once ){ @@ -2262,11 +2273,11 @@ ** json_cmd_top(). */ void json_page_top(void){ char const * zCommand; assert(g.json.gc.a && "json_main_bootstrap() was not called!"); - json_mode_bootstrap(); + assert(g.json.cmd.a && "json_mode_bootstrap() was not called!"); zCommand = json_command_arg(1); if(!zCommand || !*zCommand){ json_dispatch_missing_args_err( JsonPageDefs, "No command (sub-path) specified." " Try one of: "); Index: src/main.c ================================================================== --- src/main.c +++ src/main.c @@ -274,10 +274,13 @@ always output JSON-form error responses and always (in CGI mode) exit() with code 0 to avoid an HTTP 500 error. */ + int preserveRc; /* Do not convert error codes into 0. + * This is primarily intended for use + * by the test suite. */ int resultCode; /* used for passing back specific codes ** from /json callbacks. */ int errorDetailParanoia; /* 0=full error codes, 1=%10, 2=%100, 3=%1000 */ cson_output_opt outOpt; /* formatting options for JSON mode. */ cson_value *authToken; /* authentication token */ @@ -746,10 +749,13 @@ g.fSshTrace = find_option("sshtrace", 0, 0)!=0; g.fCgiTrace = find_option("cgitrace", 0, 0)!=0; g.fSshClient = 0; g.zSshCmd = 0; if( g.fSqlTrace ) g.fSqlStats = 1; +#ifdef FOSSIL_ENABLE_JSON + g.json.preserveRc = find_option("json-preserve-rc", 0, 0)!=0; +#endif g.fHttpTrace = find_option("httptrace", 0, 0)!=0; #ifdef FOSSIL_ENABLE_TH1_HOOKS g.fNoThHook = find_option("no-th-hook", 0, 0)!=0; #endif g.fAnyTrace = g.fSqlTrace|g.fSystemTrace|g.fSshTrace| @@ -1909,10 +1915,17 @@ @
The database schema on the server is out-of-date. Please ask @ the administrator to run fossil rebuild.
} }else{ +#ifdef FOSSIL_ENABLE_JSON + static int jsonOnce = 0; + if( !jsonOnce && g.json.isJsonMode ){ + json_mode_bootstrap(); + jsonOnce = 1; + } +#endif if( (pCmd->eCmdFlags & CMDFLAG_RAWCONTENT)==0 ){ cgi_decode_post_parameters(); } if( g.fCgiTrace ){ fossil_trace("######## Calling %s #########\n", pCmd->zName); Index: src/printf.c ================================================================== --- src/printf.c +++ src/printf.c @@ -1076,13 +1076,23 @@ ** Write error message output */ static int fossil_print_error(int rc, const char *z){ #ifdef FOSSIL_ENABLE_JSON if( g.json.isJsonMode ){ + /* + ** Avoid calling into the JSON support subsystem if it + ** has not yet been initialized, e.g. early SQLite log + ** messages, etc. + */ + if( !json_is_main_boostrapped() ) json_main_bootstrap(); json_err( 0, z, 1 ); - if( g.isHTTP ){ + if( g.isHTTP && !g.json.preserveRc ){ rc = 0 /* avoid HTTP 500 */; + } + if( g.cgiOutput==1 ){ + g.cgiOutput = 2; + cgi_reply(); } } else #endif if( g.cgiOutput==1 && g.db ){ @@ -1191,10 +1201,16 @@ z = vmprintf(zFormat, ap); va_end(ap); fossil_errorlog("warning: %s", z); #ifdef FOSSIL_ENABLE_JSON if(g.json.isJsonMode){ + /* + ** Avoid calling into the JSON support subsystem if it + ** has not yet been initialized, e.g. early SQLite log + ** messages, etc. + */ + if( !json_is_main_boostrapped() ) json_main_bootstrap(); json_warn( FSL_JSON_W_UNKNOWN, "%s", z ); }else #endif { if( g.cgiOutput==1 ){ Index: src/th_main.c ================================================================== --- src/th_main.c +++ src/th_main.c @@ -128,10 +128,11 @@ ** it for use. */ void Th_InitTraceLog(){ g.thTrace = find_option("th-trace", 0, 0)!=0; if( g.thTrace ){ + g.fAnyTrace = 1; blob_zero(&g.thLog); } } /* Index: test/json.test ================================================================== --- test/json.test +++ test/json.test @@ -33,11 +33,14 @@ # We need a JSON parser to effectively test the JSON produced by # fossil. It looks like the one from tcllib is exactly what we need. # On ActiveTcl, add it with teacup. On other platforms, YMMV. # teacup install json # teacup install json::write -package require json +if {[catch {package require json}] != 0} then { + puts "The \"json\" package is not available." + test_cleanup_then_return +} proc json2dict {txt} { set rc [catch {::json::json2dict $txt} result options] if {$rc != 0} { protOut "JSON ERROR: $result" @@ -74,11 +77,12 @@ # # Returns the status code from the HTTP header. proc fossil_http_json {url {cookie "Muppet=Monster"} args} { global RESULT JR set request "GET $url HTTP/1.1\r\nHost: localhost\r\nUser-Agent: Fossil-http-json\r\nCookie: $cookie" - set RESULT [fossil_maybe_answer $request http {*}$args] + set RESULT [fossil_maybe_answer $request http {*}$args --ipaddr 127.0.0.1] + set head ""; set body ""; set status "--NO_MATCH--" regexp {(?w)(.*)^\s*$(.*)} $RESULT dummy head body regexp {^HTTP\S+\s+(\d\d\d)\s+(.*)$} $head dummy status msg if {$status eq "200"} { set JR [json2dict $body] } @@ -115,13 +119,14 @@ } # handle the actual request flush stdout #exec $fossilexe - set RESULT [fossil_maybe_answer $request http {*}$args] + set RESULT [fossil_maybe_answer $request http {*}$args --ipaddr 127.0.0.1] # separate HTTP headers from body + set head ""; set body ""; set status "--NO_MATCH--" regexp {(?w)(.*)^\s*$(.*)} $RESULT dummy head body regexp {^HTTP\S+\s+(\d\d\d)\s+(.*)$} $head dummy status msg if {$status eq "200"} { if {[string length $body] > 0} { set JR [json2dict $body] @@ -170,10 +175,13 @@ #### VERSION AKA HAI # The JSON API generally assumes we have a respository, so let it have one. test_setup + +# Stop backoffice from running during this test as it can cause hangs. +fossil settings backoffice-disable 1 # Check for basic envelope fields in the result with an error fossil_json -expectError test_json_envelope json-enverr [concat resultCode fossil timestamp \ resultText command procTimeUs procTimeMs] {} @@ -307,28 +315,40 @@ # json cap via POST with authToken in request envelope set anon2 [read_file anon-2] fossil_post_json "/json/cap" $anon2 test json-cap-POSTenv-env-0 {[string length $JR] > 0} test_json_envelope_ok json-cap-POSTenv-env -test json-cap-POSTenv-name {[dict get $JR payload name] eq "anonymous"} knownBug +if {[catch {test json-cap-POSTenv-name \ + {[dict get $JR payload name] eq "anonymous"} knownBug} jerr]} then { + test json-cap-POSTenv-name-threw 0 + protOut "CAUGHT: $jerr" +} test json-cap-POSTenv-notsetup {![dict get $JR payload permissionFlags setup]} # json cap via GET with authToken in Cookie header fossil_post_json "/json/cap" {} $AnonCookie test json-cap-GETcookie-env-0 {[string length $JR] > 0} -test_json_envelope_ok json-cap-GETcookie-env -test json-cap-GETcookie-name {[dict get $JR payload name] eq "anonymous"} -test json-cap-GETcookie-notsetup {![dict get $JR payload permissionFlags setup]} +test_json_envelope_ok json-cap-GETcookie-env-0 +if {[catch {test json-cap-GETcookie-name-0 \ + {[dict get $JR payload name] eq "anonymous"}} jerr]} then { + test json-cap-GETcookie-name-0-threw 0 + protOut "CAUGHT: $jerr" +} +test json-cap-GETcookie-notsetup-0 {![dict get $JR payload permissionFlags setup]} # json cap via GET with authToken in a parameter fossil_post_json "/json/cap?authToken=[dict get $AuthAnon authToken]" {} -test json-cap-GETcookie-env-0 {[string length $JR] > 0} -test_json_envelope_ok json-cap-GETcookie-env -test json-cap-GETcookie-name {[dict get $JR payload name] eq "anonymous"} -test json-cap-GETcookie-notsetup {![dict get $JR payload permissionFlags setup]} +test json-cap-GETcookie-env-1 {[string length $JR] > 0} +test_json_envelope_ok json-cap-GETcookie-env-1 +if {[catch {test json-cap-GETcookie-name-1 \ + {[dict get $JR payload name] eq "anonymous"}} jerr]} then { + test json-cap-GETcookie-name-1-threw 0 + protOut "CAUGHT: $jerr" +} +test json-cap-GETcookie-notsetup-1 {![dict get $JR payload permissionFlags setup]} # whoami # via CLI with no auth token supplied fossil_json whoami @@ -675,27 +695,36 @@ # error happens before we have made the determination that the app is # in JSON mode or if the error handling is incorrectly not # recognizing JSON mode. # #test_setup x.fossil -#catch {exec chmod 444 .rep.fossil}; # Unix. What about Win? -fossil_http_json /json/timeline/checkin $U1Cookie +fossil_http_json /json/query?sql=PRAGMA%20repository.journal_mode%3Dwal $U1Cookie test json-ROrepo-1-1 {$CODE == 0} test json-ROrepo-1-2 {[regexp {\}\s*$} $RESULT]} test json-ROrepo-1-3 {![regexp {SQLITE_[A-Z]+:} $RESULT]} test_json_envelope_ok json-http-timeline1 +if {$is_windows} then { + catch {exec attrib +r .rep.fossil}; # Windows +} else { + catch {exec chmod 444 .rep.fossil}; # Unix +} protOut "chmod 444 repo" -catch {exec chmod 444 .rep.fossil}; # Unix -catch {exec attrib +r .rep.fossil}; # Windows -fossil_http_json /json/timeline/checkin $U1Cookie -expectError +fossil_http_json /json/query?sql=PRAGMA%20repository.journal_mode%3Ddelete $U1Cookie -expectError --json-preserve-rc test json-ROrepo-2-1 {$CODE != 0} -test json-ROrepo-2-2 {[regexp {\}\s*$} $RESULT]} knownBug -test json-ROrepo-2-3 {![regexp {SQLITE_[A-Z]+:} $RESULT]} knownBug +test json-ROrepo-2-2 {[regexp {\}\s*$} $RESULT]} +test json-ROrepo-2-3 {![regexp {SQLITE_[A-Z]+:} $RESULT]} #test_json_envelope_ok json-http-timeline2 -catch {exec attrib -r .rep.fossil}; # Windows -catch {exec chmod 666 .rep.fossil}; # Unix - +if {$is_windows} then { + catch {exec attrib -r .rep.fossil}; # Windows + catch {exec attrib -r .rep.fossil-shm} + catch {exec attrib -r .rep.fossil-wal} +} else { + catch {exec chmod 666 .rep.fossil}; # Unix + catch {exec chmod 666 .rep.fossil-shm} + catch {exec chmod 666 .rep.fossil-wal} +} +protOut "chmod 666 repo" #### Result Codes # Test cases designed to stimulate each (documented) error code. # FOSSIL-0000 Index: test/tester.tcl ================================================================== --- test/tester.tcl +++ test/tester.tcl @@ -170,39 +170,57 @@ set index [lsearch -exact $args -keepNewline] if {$index != -1} { set keepNewline 1 set args [lreplace $args $index $index] } + set whatIf 0 + set index [lsearch -exact $args -whatIf] + if {$index != -1} { + set whatIf 1 + set args [lreplace $args $index $index] + } foreach a $args { lappend cmd $a } protOut $cmd flush stdout - if {[string length $answer] > 0} { - protOut $answer - set prompt_file [file join $::tempPath fossil_prompt_answer] - write_file $prompt_file $answer\n - if {$keepNewline} { - set rc [catch {eval exec -keepnewline $cmd <$prompt_file} result] - } else { - set rc [catch {eval exec $cmd <$prompt_file} result] - } - file delete $prompt_file - } else { - if {$keepNewline} { - set rc [catch {eval exec -keepnewline $cmd} result] - } else { - set rc [catch {eval exec $cmd} result] - } + if {$whatIf} { + protOut [pwd]; protOut $answer + set result WHAT-IF-MODE; set rc 42 + } else { + if {[string length $answer] > 0} { + protOut $answer + set prompt_file [file join $::tempPath fossil_prompt_answer] + write_file $prompt_file $answer\n + set execCmd [list eval exec] + if {$keepNewline} {lappend execCmd -keepnewline} + lappend execCmd $cmd <$prompt_file + set rc [catch $execCmd result] + file delete $prompt_file + } else { + set execCmd [list eval exec] + if {$keepNewline} {lappend execCmd -keepnewline} + lappend execCmd $cmd + set rc [catch $execCmd result] + } + } + set ab(str) {child process exited abnormally} + set ab(len) [string length $ab(str)] + set ab(off) [expr {$ab(len) - 1}] + if {$rc && $expectError && \ + [string range $result end-$ab(off) end] eq $ab(str)} { + set result [string range $result 0 end-$ab(len)] } global RESULT CODE set CODE $rc - if {($rc && !$expectError) || (!$rc && $expectError)} { - protOut "ERROR: $result" 1 - } elseif {$::VERBOSE} { - protOut "RESULT: $result" + if {!$whatIf} { + if {($rc && !$expectError) || (!$rc && $expectError)} { + protOut "ERROR ($rc): $result" 1 + } elseif {$::VERBOSE} { + protOut "RESULT ($rc): $result" + } } set RESULT $result } # Read a file into memory.