summaryrefslogtreecommitdiff
path: root/src/backoffice.c
diff options
context:
space:
mode:
authorcashmere <cashmere@autistici.org>2026-09-04 09:05:24 +0200
committercashmere <cashmere@autistici.org>2026-09-04 09:05:24 +0200
commit4f49b16079b27d7ce633619e4cddfdf2bffbc952 (patch)
treec296c1e7f3c3e2f6859c6d3a1b1b208779b89679 /src/backoffice.c
downloadrelic-master.tar.gz
initial commitHEADmaster
Diffstat (limited to 'src/backoffice.c')
-rw-r--r--src/backoffice.c881
1 files changed, 881 insertions, 0 deletions
diff --git a/src/backoffice.c b/src/backoffice.c
new file mode 100644
index 0000000..696a2ff
--- /dev/null
+++ b/src/backoffice.c
@@ -0,0 +1,881 @@
+/*
+** Copyright (c) 2018 D. Richard Hipp
+**
+** This program is free software; you can redistribute it and/or
+** modify it under the terms of the Simplified BSD License (also
+** known as the "2-Clause License" or "FreeBSD License".)
+**
+** 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.
+**
+** Author contact information:
+** drh@hwaci.com
+** http://www.hwaci.com/drh/
+**
+*******************************************************************************
+**
+** This file contains code used to manage a background processes that
+** occur after user interaction with the repository. Examples of
+** backoffice processing includes:
+**
+** * Sending alerts and notifications
+** * Processing the email queue
+** * Handling post-receive hooks
+** * Automatically syncing to peer repositories
+**
+** Backoffice processing is automatically started whenever there are
+** changes to the repository. The backoffice process dies off after
+** a period of inactivity.
+**
+** Steps are taken to ensure that only a single backoffice process is
+** running at a time. Otherwise, there could be race conditions that
+** cause adverse effects such as multiple alerts for the same changes.
+**
+** At the same time, we do not want a backoffice process to run forever.
+** Backoffice processes should die off after doing whatever work they need
+** to do. In this way, we avoid having lots of idle processes in the
+** process table, doing nothing on rarely accessed repositories, and
+** if the Fossil binary is updated on a system, the backoffice processes
+** will restart using the new binary automatically.
+**
+** At any point in time there should be at most two backoffice processes.
+** There is a main process that is doing the actual work, and there is
+** a second stand-by process that is waiting for the main process to finish
+** and that will become the main process after a delay.
+**
+** After any successful web page reply, the backoffice_check_if_needed()
+** routine is called. That routine checks to see if both one or both of
+** the backoffice processes are already running. That routine remembers the
+** status in a global variable.
+**
+** Later, after the repository database is closed, the
+** backoffice_run_if_needed() routine is called. If the prior call
+** to backoffice_check_if_needed() indicated that backoffice processing
+** might be required, the run_if_needed() attempts to kick off a backoffice
+** process.
+**
+** All work performed by the backoffice is in the backoffice_work()
+** routine.
+*/
+#if defined(_WIN32)
+# if defined(_WIN32_WINNT)
+# undef _WIN32_WINNT
+# endif
+# define _WIN32_WINNT 0x501
+#endif
+#include "config.h"
+#include "backoffice.h"
+#include <time.h>
+#if defined(_WIN32)
+# include <windows.h>
+# include <stdio.h>
+# include <process.h>
+# if defined(__MINGW32__)
+# include <wchar.h>
+# endif
+# define GETPID (int)GetCurrentProcessId
+#else
+# include <unistd.h>
+# include <sys/types.h>
+# include <signal.h>
+# include <errno.h>
+# include <sys/time.h>
+# include <sys/resource.h>
+# include <fcntl.h>
+# define GETPID getpid
+#endif
+#include <time.h>
+
+/*
+** The BKOFCE_LEASE_TIME is the amount of time for which a single backoffice
+** processing run is valid. Each backoffice run monopolizes the lease for
+** at least this amount of time. Hopefully all backoffice processing is
+** finished much faster than this - usually in less than a second. But
+** regardless of how long each invocation lasts, successive backoffice runs
+** must be spaced out by at least this much time.
+*/
+#define BKOFCE_LEASE_TIME 60 /* Length of lease validity in seconds */
+
+#if LOCAL_INTERFACE
+/*
+** An instance of the following object describes a lease on the backoffice
+** processing timeslot. This lease is used to help ensure that no more than
+** one process is running backoffice at a time.
+*/
+struct Lease {
+ sqlite3_uint64 idCurrent; /* process ID for the current lease holder */
+ sqlite3_uint64 tmCurrent; /* Expiration of the current lease */
+ sqlite3_uint64 idNext; /* process ID for the next lease holder on queue */
+ sqlite3_uint64 tmNext; /* Expiration of the next lease */
+};
+#endif
+
+/***************************************************************************
+** Local state variables
+**
+** Set to prevent backoffice processing from ever entering sleep or
+** otherwise taking a long time to complete. Set this when a user-visible
+** process might need to wait for backoffice to complete.
+*/
+static int backofficeNoDelay = 0;
+
+/* This variable is set to the name of a database on which backoffice
+** should run if backoffice process is needed. It is set by the
+** backoffice_check_if_needed() routine which must be run while the database
+** file is open. Later, after the database is closed, the
+** backoffice_run_if_needed() will consult this variable to see if it
+** should be a no-op.
+**
+** The magic string "x" in this variable means "do not run the backoffice".
+*/
+static char *backofficeDb = 0;
+
+/*
+** Log backoffice activity to a file named here. If not NULL, this
+** overrides the "backoffice-logfile" setting of the database. If NULL,
+** the "backoffice-logfile" setting is used instead.
+*/
+static const char *backofficeLogfile = 0;
+
+/*
+** Write the log message into this open file.
+*/
+static FILE *backofficeFILE = 0;
+
+/*
+** Write backoffice log messages on this BLOB. to this connection:
+*/
+static Blob *backofficeBlob = 0;
+
+/*
+** Non-zero for extra logging detail.
+*/
+static int backofficeLogDetail = 0;
+
+/* End of state variables
+****************************************************************************/
+
+/*
+** This function emits a diagnostic message related to the processing in
+** this module.
+*/
+static void backofficeTrace(const char *zFormat, ...){
+ char *zMsg = 0;
+ if( g.fAnyTrace ){
+ va_list ap;
+ va_start(ap, zFormat);
+ zMsg = sqlite3_vmprintf(zFormat, ap);
+ va_end(ap);
+ }
+ if( g.fAnyTrace ) fprintf(stderr, "%s", zMsg);
+ if( zMsg ) sqlite3_free(zMsg);
+}
+
+/*
+** Do not allow backoffice processes to sleep waiting on a timeslot.
+** They must either do their work immediately or exit.
+**
+** In a perfect world, this interface would not exist, as there would
+** never be a problem with waiting backoffice threads. But in some cases
+** a backoffice will delay a UI thread, so we don't want them to run for
+** longer than needed.
+*/
+void backoffice_no_delay(void){
+ backofficeNoDelay = 1;
+}
+
+/*
+** Sleeps for the specified number of milliseconds -OR- until interrupted
+** by another thread (if supported by the underlying platform). Non-zero
+** will be returned if the sleep was interrupted.
+*/
+static int backofficeSleep(int milliseconds){
+#if defined(_WIN32)
+ assert( milliseconds>=0 );
+ if( SleepEx((DWORD)milliseconds, TRUE)==WAIT_IO_COMPLETION ){
+ return 1;
+ }
+#else
+ sqlite3_sleep(milliseconds);
+#endif
+ return 0;
+}
+
+/*
+** Parse a unsigned 64-bit integer from a string. Return a pointer
+** to the character of z[] that occurs after the integer.
+*/
+static const char *backofficeParseInt(const char *z, sqlite3_uint64 *pVal){
+ *pVal = 0;
+ if( z==0 ) return 0;
+ while( fossil_isspace(z[0]) ){ z++; }
+ while( fossil_isdigit(z[0]) ){
+ *pVal = (*pVal)*10 + z[0] - '0';
+ z++;
+ }
+ return z;
+}
+
+/*
+** Read the "backoffice" property and parse it into a Lease object.
+**
+** The backoffice property should consist of four integers:
+**
+** (1) Process ID for the active backoffice process.
+** (2) Time (seconds since 1970) for when the active backoffice
+** lease expires.
+** (3) Process ID for the on-deck backoffice process.
+** (4) Time when the on-deck process should expire.
+**
+** No other process should start active backoffice processing until
+** process (1) no longer exists and the current time exceeds (2).
+*/
+static void backofficeReadLease(Lease *pLease){
+ Stmt q;
+ memset(pLease, 0, sizeof(*pLease));
+ db_unprotect(PROTECT_CONFIG);
+ db_prepare(&q, "SELECT value FROM repository.config"
+ " WHERE name='backoffice'");
+ if( db_step(&q)==SQLITE_ROW ){
+ const char *z = db_column_text(&q,0);
+ z = backofficeParseInt(z, &pLease->idCurrent);
+ z = backofficeParseInt(z, &pLease->tmCurrent);
+ z = backofficeParseInt(z, &pLease->idNext);
+ backofficeParseInt(z, &pLease->tmNext);
+ }
+ db_finalize(&q);
+ db_protect_pop();
+}
+
+/*
+** Return a string that describes how long it has been since the
+** last backoffice run. The string is obtained from fossil_malloc().
+*/
+char *backoffice_last_run(void){
+ Lease x;
+ sqlite3_uint64 tmNow;
+ double rAge;
+ backofficeReadLease(&x);
+ tmNow = time(0);
+ if( x.tmCurrent==0 ){
+ return fossil_strdup("never");
+ }
+ if( tmNow<=(x.tmCurrent-BKOFCE_LEASE_TIME) ){
+ return fossil_strdup("moments ago");
+ }
+ rAge = (tmNow - (x.tmCurrent-BKOFCE_LEASE_TIME))/86400.0;
+ return mprintf("%z ago", human_readable_age(rAge));
+}
+
+/*
+** Write a lease to the backoffice property
+*/
+static void backofficeWriteLease(Lease *pLease){
+ db_unprotect(PROTECT_CONFIG);
+ db_multi_exec(
+ "REPLACE INTO repository.config(name,value,mtime)"
+ " VALUES('backoffice','%lld %lld %lld %lld',now())",
+ pLease->idCurrent, pLease->tmCurrent,
+ pLease->idNext, pLease->tmNext);
+ db_protect_pop();
+}
+
+/*
+** Check to see if the specified Win32 process is still alive. It
+** should be noted that even if this function returns non-zero, the
+** process may die before another operation on it can be completed.
+*/
+#if defined(_WIN32)
+#ifndef PROCESS_QUERY_LIMITED_INFORMATION
+# define PROCESS_QUERY_LIMITED_INFORMATION (0x1000)
+#endif
+static int backofficeWin32ProcessExists(DWORD dwProcessId){
+ HANDLE hProcess;
+ hProcess = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION,FALSE,dwProcessId);
+ if( hProcess==NULL ) return 0;
+ CloseHandle(hProcess);
+ return 1;
+}
+#endif
+
+/*
+** Check to see if the process identified by pid is alive. If
+** we cannot prove that the process is dead, return true.
+*/
+static int backofficeProcessExists(sqlite3_uint64 pid){
+#if defined(_WIN32)
+ return pid>0 && backofficeWin32ProcessExists((DWORD)pid)!=0;
+#else
+ return pid>0 && kill((pid_t)pid, 0)==0;
+#endif
+}
+
+/*
+** Check to see if the process identified by pid has finished. If
+** we cannot prove that the process is still running, return true.
+*/
+static int backofficeProcessDone(sqlite3_uint64 pid){
+#if defined(_WIN32)
+ return pid<=0 || backofficeWin32ProcessExists((DWORD)pid)==0;
+#else
+ return pid<=0 || kill((pid_t)pid, 0)!=0;
+#endif
+}
+
+/*
+** Return a process id number for the current process
+*/
+static sqlite3_uint64 backofficeProcessId(void){
+ return (sqlite3_uint64)GETPID();
+}
+
+
+/*
+** COMMAND: test-process-id
+**
+** Usage: %fossil [--sleep N] PROCESS-ID ...
+**
+** Show the current process id, and also tell whether or not all other
+** processes IDs on the command line are running or not. If the --sleep N
+** option is provide, then sleep for N seconds before exiting.
+*/
+void test_process_id_command(void){
+ const char *zSleep = find_option("sleep",0,1);
+ int i;
+ verify_all_options();
+ fossil_print("ProcessID for this process: %lld\n", backofficeProcessId());
+ if( zSleep ) sqlite3_sleep(1000*atoi(zSleep));
+ for(i=2; i<g.argc; i++){
+ sqlite3_uint64 x = (sqlite3_uint64)atoi(g.argv[i]);
+ fossil_print("ProcessId %lld: exists %d done %d\n",
+ x, backofficeProcessExists(x),
+ backofficeProcessDone(x));
+ }
+}
+
+/*
+** COMMAND: test-backoffice-lease
+**
+** Usage: %fossil test-backoffice-lease ?--reset?
+**
+** Print out information about the backoffice "lease" entry in the
+** config table that controls whether or not backoffice should run.
+**
+** If the --reset option is given, the backoffice lease is reset.
+** The use of the --reset option can be disruptive. It can cause two
+** or more backoffice processes to be run simultaneously. Use it with
+** caution.
+*/
+void test_backoffice_lease(void){
+ sqlite3_int64 tmNow = time(0);
+ Lease x;
+ const char *zLease;
+ db_find_and_open_repository(0,0);
+ if( find_option("reset",0,0)!=0 ){
+ db_unprotect(PROTECT_CONFIG);
+ db_multi_exec(
+ "DELETE FROM repository.config WHERE name='backoffice'"
+ );
+ db_protect_pop();
+ }
+ verify_all_options();
+ zLease = db_get("backoffice","");
+ fossil_print("now: %lld\n", tmNow);
+ fossil_print("lease: \"%s\"\n", zLease);
+ backofficeReadLease(&x);
+ fossil_print("idCurrent: %-20lld", x.idCurrent);
+ if( backofficeProcessExists(x.idCurrent) ) fossil_print(" (exists)");
+ if( backofficeProcessDone(x.idCurrent) ) fossil_print(" (done)");
+ fossil_print("\n");
+ fossil_print("tmCurrent: %-20lld", x.tmCurrent);
+ if( x.tmCurrent>0 ){
+ fossil_print(" (now%+d)\n",x.tmCurrent-tmNow);
+ }else{
+ fossil_print("\n");
+ }
+ fossil_print("idNext: %-20lld", x.idNext);
+ if( backofficeProcessExists(x.idNext) ) fossil_print(" (exists)");
+ if( backofficeProcessDone(x.idNext) ) fossil_print(" (done)");
+ fossil_print("\n");
+ fossil_print("tmNext: %-20lld", x.tmNext);
+ if( x.tmNext>0 ){
+ fossil_print(" (now%+d)\n",x.tmNext-tmNow);
+ }else{
+ fossil_print("\n");
+ }
+}
+
+/*
+** If backoffice processing is needed set the backofficeDb variable to the
+** name of the database file. If no backoffice processing is needed,
+** this routine makes no changes to state.
+*/
+void backoffice_check_if_needed(void){
+ Lease x;
+ sqlite3_uint64 tmNow;
+
+ if( backofficeDb ) return;
+ if( g.zRepositoryName==0 ) return;
+ if( g.db==0 ) return;
+ if( !db_table_exists("repository","config") ) return;
+ if( db_get_boolean("backoffice-disable",0) ) return;
+ tmNow = time(0);
+ backofficeReadLease(&x);
+ if( x.tmNext>=tmNow && backofficeProcessExists(x.idNext) ){
+ /* Another backoffice process is already queued up to run. This
+ ** process does not need to do any backoffice work. */
+ return;
+ }else{
+ /* We need to run backup to be (at a minimum) on-deck */
+ backofficeDb = fossil_strdup(g.zRepositoryName);
+ }
+}
+
+/*
+** Call this routine to disable backoffice
+*/
+void backoffice_disable(void){
+ backofficeDb = "x";
+}
+
+/*
+** Check for errors prior to running backoffice_thread() or backoffice_run().
+*/
+static void backoffice_error_check_one(int *pOnce){
+ if( *pOnce ){
+ fossil_panic("multiple calls to backoffice()");
+ }
+ *pOnce = 1;
+ if( g.db==0 ){
+ fossil_panic("database not open for backoffice processing");
+ }
+ if( db_transaction_nesting_depth()!=0 ){
+ fossil_panic("transaction %s not closed prior to backoffice processing",
+ db_transaction_start_point());
+ }
+}
+
+/* This is the main loop for backoffice processing.
+**
+** If another process is already working as the current backoffice and
+** the on-deck backoffice, then this routine returns very quickly
+** without doing any work.
+**
+** If no backoffice processes are running at all, this routine becomes
+** the main backoffice.
+**
+** If a primary backoffice is running, but an on-deck backoffice is
+** needed, this routine becomes that on-deck backoffice.
+*/
+static void backoffice_thread(void){
+ Lease x;
+ sqlite3_uint64 tmNow;
+ sqlite3_uint64 idSelf;
+ int lastWarning = 0;
+ int warningDelay = 30;
+ static int once = 0;
+
+ if( sqlite3_db_readonly(g.db, 0) ) return;
+ if( db_is_protected(PROTECT_READONLY) ) return;
+ g.zPhase = "backoffice-pending";
+ backoffice_error_check_one(&once);
+ idSelf = backofficeProcessId();
+ while(1){
+ tmNow = time(0);
+ db_begin_write();
+ backofficeReadLease(&x);
+ if( x.tmNext>=tmNow
+ && x.idNext!=idSelf
+ && backofficeProcessExists(x.idNext)
+ ){
+ /* Another backoffice process is already queued up to run. This
+ ** process does not need to do any backoffice work and can stop
+ ** immediately. */
+ db_end_transaction(0);
+ backofficeTrace("/***** Backoffice Processing Not Needed In %d *****/\n",
+ GETPID());
+ break;
+ }
+ if( x.tmCurrent<tmNow && backofficeProcessDone(x.idCurrent) ){
+ /* This process can start doing backoffice work immediately */
+ x.idCurrent = idSelf;
+ x.tmCurrent = tmNow + BKOFCE_LEASE_TIME;
+ x.idNext = 0;
+ x.tmNext = 0;
+ g.zPhase = "backoffice-work";
+ backofficeWriteLease(&x);
+ db_end_transaction(0);
+ backofficeTrace("/***** Begin Backoffice Processing %d *****/\n",
+ GETPID());
+ backoffice_work();
+ break;
+ }
+ if( backofficeNoDelay || db_get_boolean("backoffice-nodelay",0) ){
+ /* If the no-delay flag is set, exit immediately rather than queuing
+ ** up. Assume that some future request will come along and handle any
+ ** necessary backoffice work. */
+ db_end_transaction(0);
+ backofficeTrace(
+ "/***** Backoffice No-Delay Exit For %d *****/\n",
+ GETPID());
+ break;
+ }
+ /* This process needs to queue up and wait for the current lease
+ ** to expire before continuing. */
+ x.idNext = idSelf;
+ x.tmNext = (tmNow>x.tmCurrent ? tmNow : x.tmCurrent) + BKOFCE_LEASE_TIME;
+ backofficeWriteLease(&x);
+ db_end_transaction(0);
+ backofficeTrace("/***** Backoffice On-deck %d *****/\n", GETPID());
+ if( x.tmCurrent >= tmNow ){
+ if( backofficeSleep(1000*(x.tmCurrent - tmNow + 1)) ){
+ /* The sleep was interrupted by a signal from another thread. */
+ backofficeTrace("/***** Backoffice Interrupt %d *****/\n", GETPID());
+ db_end_transaction(0);
+ break;
+ }
+ }else{
+ if( (sqlite3_uint64)(lastWarning+warningDelay) < tmNow ){
+ sqlite3_int64 runningFor = BKOFCE_LEASE_TIME + tmNow - x.tmCurrent;
+ if( warningDelay>=240 && runningFor<1800 ){
+ fossil_warning(
+ "backoffice process %lld still running after %d seconds",
+ x.idCurrent, runningFor);
+ }
+ lastWarning = tmNow;
+ warningDelay *= 2;
+ }
+ if( backofficeSleep(1000) ){
+ /* The sleep was interrupted by a signal from another thread. */
+ backofficeTrace("/***** Backoffice Interrupt %d *****/\n", GETPID());
+ db_end_transaction(0);
+ break;
+ }
+ }
+ }
+ return;
+}
+
+/*
+** Append to a message to the backoffice log, if the log is open.
+*/
+void backoffice_log(const char *zFormat, ...){
+ va_list ap;
+ if( backofficeBlob==0 ) return;
+ blob_append_char(backofficeBlob, ' ');
+ va_start(ap, zFormat);
+ blob_vappendf(backofficeBlob, zFormat, ap);
+ va_end(ap);
+}
+
+#if !defined(_WIN32)
+/*
+** Capture routine for signals while running backoffice.
+*/
+static void backoffice_signal_handler(int sig){
+ const char *zSig = 0;
+ if( sig==SIGSEGV ) zSig = "SIGSEGV";
+ if( sig==SIGFPE ) zSig = "SIGFPE";
+ if( sig==SIGABRT ) zSig = "SIGABRT";
+ if( sig==SIGILL ) zSig = "SIGILL";
+ if( zSig==0 ){
+ backoffice_log("signal-%d", sig);
+ }else{
+ backoffice_log("%s", zSig);
+ }
+ fprintf(backofficeFILE, "%s\n", blob_str(backofficeBlob));
+ fflush(backofficeFILE);
+ exit(1);
+}
+#endif
+
+#if !defined(_WIN32)
+/*
+** Convert a struct timeval into an integer number of microseconds
+*/
+static long long int tvms(struct timeval *p){
+ return ((long long int)p->tv_sec)*1000000 + (long long int)p->tv_usec;
+}
+#endif
+
+
+/*
+** This routine runs to do the backoffice processing. When adding new
+** backoffice processing tasks, add them here.
+*/
+void backoffice_work(void){
+ /* Log the backoffice run for testing purposes. For production deployments
+ ** the "backoffice-logfile" property should be unset and the following code
+ ** should be a no-op. */
+ const char *zLog = backofficeLogfile;
+ Blob log;
+ int nThis;
+ int nTotal = 0;
+#if !defined(_WIN32)
+ struct timeval sStart, sEnd;
+#endif
+ if( zLog==0 ) zLog = db_get("backoffice-logfile",0);
+ if( zLog && zLog[0] && (backofficeFILE = fossil_fopen(zLog,"a"))!=0 ){
+ int i;
+ char *zName = db_get("project-name",0);
+#if !defined(_WIN32)
+ gettimeofday(&sStart, 0);
+ signal(SIGSEGV, backoffice_signal_handler);
+ signal(SIGABRT, backoffice_signal_handler);
+ signal(SIGFPE, backoffice_signal_handler);
+ signal(SIGILL, backoffice_signal_handler);
+#endif
+ if( zName==0 ){
+ zName = (char*)file_tail(g.zRepositoryName);
+ if( zName==0 ) zName = "(unnamed)";
+ }else{
+ /* Convert all spaces in the "project-name" into dashes */
+ for(i=0; zName[i]; i++){ if( zName[i]==' ' ) zName[i] = '-'; }
+ }
+ blob_init(&log, 0, 0);
+ backofficeBlob = &log;
+ blob_appendf(&log, "%s %s", db_text(0, "SELECT datetime('now')"), zName);
+ }
+
+ /* Here is where the actual work of the backoffice happens */
+ g.zPhase = "backoffice-alerts";
+ nThis = alert_backoffice(0);
+ if( nThis ){ backoffice_log("%d alerts", nThis); nTotal += nThis; }
+ g.zPhase = "backoffice-hooks";
+ nThis = hook_backoffice();
+ if( nThis ){ backoffice_log("%d hooks", nThis); nTotal += nThis; }
+ g.zPhase = "backoffice-close";
+
+ /* Close the log */
+ if( backofficeFILE ){
+ if( nTotal || backofficeLogDetail ){
+ if( nTotal==0 ) backoffice_log("no-op");
+#if !defined(_WIN32)
+ gettimeofday(&sEnd,0);
+ backoffice_log("elapse-time %d us", tvms(&sEnd) - tvms(&sStart));
+#endif
+ fprintf(backofficeFILE, "%s\n", blob_str(backofficeBlob));
+ }
+ fclose(backofficeFILE);
+ }
+}
+
+/*
+** COMMAND: backoffice*
+**
+** Usage: %fossil backoffice [OPTIONS...] [REPOSITORIES...]
+**
+** Run backoffice processing on the repositories listed. If no
+** repository is specified, run it on the repository of the local check-out.
+**
+** This might be done by a cron job or similar to make sure backoffice
+** processing happens periodically. Or, the --poll option can be used
+** to run this command as a daemon that will periodically invoke backoffice
+** on a collection of repositories.
+**
+** If only a single repository is named and --poll is omitted, then the
+** backoffice work is done in-process. But if there are multiple repositories
+** or if --poll is used, a separate sub-process is started for each poll of
+** each repository.
+**
+** Standard options:
+**
+** --debug Show what this command is doing
+**
+** --logfile FILE Append a log of backoffice actions onto FILE
+**
+** --min N When polling, invoke backoffice at least
+** once every N seconds even if the repository
+** never changes. 0 or negative means disable
+** this feature. Default: 3600 (once per hour).
+**
+** --poll N Repeat backoffice calls for repositories that
+** change in approximately N-second intervals.
+** N less than 1 turns polling off (the default).
+** Recommended polling interval: 60 seconds.
+**
+** --trace Enable debugging output on stderr
+**
+** Options intended for internal use only which may change or be
+** discontinued in future releases:
+**
+** --nodelay Do not queue up or wait for a backoffice job
+** to complete. If no work is available or if
+** backoffice has run recently, return immediately.
+**
+** --nolease Always run backoffice, even if there is a lease
+** conflict. This option implies --nodelay. This
+** option is added to secondary backoffice commands
+** that are invoked by the --poll option.
+*/
+void backoffice_command(void){
+ int nPoll;
+ int nMin;
+ const char *zPoll;
+ int bDebug = 0;
+ int bNoLease = 0;
+ unsigned int nCmd = 0;
+ if( find_option("trace",0,0)!=0 ) g.fAnyTrace = 1;
+ if( find_option("nodelay",0,0)!=0 ) backofficeNoDelay = 1;
+ backofficeLogfile = find_option("logfile",0,1);
+ zPoll = find_option("poll",0,1);
+ nPoll = zPoll ? atoi(zPoll) : 0;
+ zPoll = find_option("min",0,1);
+ nMin = zPoll ? atoi(zPoll) : 3600;
+ bDebug = find_option("debug",0,0)!=0;
+ bNoLease = find_option("nolease",0,0)!=0;
+
+ /* Silently consume the -R or --repository flag, leaving behind its
+ ** argument. This is for legacy compatibility. Older versions of the
+ ** backoffice command only ran on a single repository that was specified
+ ** using the -R option. */
+ (void)find_option("repository","R",0);
+
+ verify_all_options();
+ if( g.argc>3 || nPoll>0 ){
+ /* Either there are multiple repositories named on the command-line
+ ** or we are polling. In either case, each backoffice should be run
+ ** using a separate sub-process */
+ int i;
+ time_t iNow = 0;
+ time_t ix;
+ i64 *aLastRun = fossil_malloc( sizeof(i64)*g.argc );
+ memset(aLastRun, 0, sizeof(i64)*g.argc );
+ while( 1 /* exit via "break;" */){
+ time_t iNext = time(0);
+ for(i=2; i<g.argc; i++){
+ Blob cmd;
+ if( !file_isfile(g.argv[i], ExtFILE) ){
+ continue; /* Repo no longer exists. Ignore it. */
+ }
+ if( iNow
+ && iNow>file_mtime(g.argv[i], ExtFILE)
+ && (nMin<=0 || aLastRun[i]+nMin>iNow)
+ ){
+ continue; /* Not yet time to run this one */
+ }
+ blob_init(&cmd, 0, 0);
+ blob_append_escaped_arg(&cmd, g.nameOfExe, 1);
+ blob_append(&cmd, " backoffice --nodelay", -1);
+ if( g.fAnyTrace ){
+ blob_append(&cmd, " --trace", -1);
+ }
+ if( bDebug ){
+ blob_append(&cmd, " --debug", -1);
+ }
+ if( nPoll>0 ){
+ blob_append(&cmd, " --nolease", -1);
+ }
+ if( backofficeLogfile ){
+ blob_append(&cmd, " --logfile", -1);
+ blob_append_escaped_arg(&cmd, backofficeLogfile, 1);
+ }
+ blob_append_escaped_arg(&cmd, g.argv[i], 1);
+ nCmd++;
+ if( bDebug ){
+ fossil_print("COMMAND[%u]: %s\n", nCmd, blob_str(&cmd));
+ }
+ fossil_system(blob_str(&cmd));
+ aLastRun[i] = iNext;
+ blob_reset(&cmd);
+ }
+ if( nPoll<1 ) break;
+ iNow = iNext;
+ ix = time(0);
+ if( ix < iNow+nPoll ){
+ sqlite3_int64 nMS = (iNow + nPoll - ix)*1000;
+ if( bDebug )fossil_print("SLEEP: %lld\n", nMS);
+ sqlite3_sleep((int)nMS);
+ }
+ }
+ }else{
+ /* Not polling and only one repository named. Backoffice is run
+ ** once by this process, which then exits */
+ if( g.argc==3 ){
+ g.zRepositoryOption = g.argv[2];
+ g.argc--;
+ }
+ db_find_and_open_repository(0,0);
+ if( bDebug ){
+ backofficeLogDetail = 1;
+ }
+ if( bNoLease ){
+ backoffice_work();
+ }else{
+ backoffice_thread();
+ }
+ }
+}
+
+/*
+** This is the main interface to backoffice from the rest of the system.
+** This routine launches either backoffice_thread() directly or as a
+** subprocess.
+*/
+void backoffice_run_if_needed(void){
+ if( backofficeDb==0 ) return;
+ if( strcmp(backofficeDb,"x")==0 ) return;
+ if( g.db ) return;
+ if( g.repositoryOpen ) return;
+#if defined(_WIN32)
+ {
+ int i;
+ intptr_t x;
+ char *argv[4];
+ wchar_t *ax[5];
+ argv[0] = g.nameOfExe;
+ argv[1] = "backoffice";
+ argv[2] = "-R";
+ argv[3] = backofficeDb;
+ ax[4] = 0;
+ for(i=0; i<=3; i++) ax[i] = fossil_utf8_to_unicode(argv[i]);
+ x = _wspawnv(_P_NOWAIT, ax[0], (const wchar_t * const *)ax);
+ for(i=0; i<=3; i++) fossil_unicode_free(ax[i]);
+ backofficeTrace(
+ "/***** Subprocess %d creates backoffice child %lu *****/\n",
+ GETPID(), GetProcessId((HANDLE)x));
+ if( x>=0 ) return;
+ }
+#else /* unix */
+ {
+ pid_t pid = fork();
+ if( pid>0 ){
+ /* This is the parent in a successful fork(). Return immediately. */
+ backofficeTrace(
+ "/***** Subprocess %d creates backoffice child %d *****/\n",
+ GETPID(), (int)pid);
+ return;
+ }
+ if( pid==0 ){
+ /* This is the child of a successful fork(). Run backoffice. */
+ int i;
+ setsid();
+ for(i=0; i<=2; i++){
+ close(i);
+ open("/dev/null", O_RDWR);
+ }
+ for(i=3; i<100; i++){ close(i); }
+ g.fDebug = 0;
+ g.httpIn = 0;
+ g.httpOut = 0;
+ db_open_repository(backofficeDb);
+ backofficeDb = "x";
+ backoffice_thread();
+ db_close(1);
+ backofficeTrace("/***** Backoffice Child %d exits *****/\n", GETPID());
+ exit(0);
+ }
+ fossil_warning("backoffice process %d fork failed, errno %d", GETPID(),
+ errno);
+ }
+#endif
+ /* Fork() failed or is unavailable. Run backoffice in this process, but
+ ** do so with the no-delay setting.
+ */
+ backofficeNoDelay = 1;
+ db_open_repository(backofficeDb);
+ backofficeDb = "x";
+ backoffice_thread();
+ db_close(1);
+}