diff --git a/apps/gdallocationinfo.cpp b/apps/gdallocationinfo.cpp index b5c3c42117da..bcd6e5225141 100644 --- a/apps/gdallocationinfo.cpp +++ b/apps/gdallocationinfo.cpp @@ -26,12 +26,6 @@ #include -#ifdef _WIN32 -#include -#else -#include -#endif - /************************************************************************/ /* GetSRSAsWKT */ /************************************************************************/ @@ -300,7 +294,7 @@ MAIN_START(argc, argv) if (std::isnan(dfGeoX)) { // Is it an interactive terminal ? - if (isatty(static_cast(fileno(stdin)))) + if (CPLIsInteractive(stdin)) { if (!osSourceSRS.empty()) { diff --git a/apps/gdaltransform.cpp b/apps/gdaltransform.cpp index 6455a060c5d5..7b77da3dd10e 100644 --- a/apps/gdaltransform.cpp +++ b/apps/gdaltransform.cpp @@ -30,12 +30,6 @@ #include "ogr_srs_api.h" #include "commonutils.h" -#ifdef _WIN32 -#include -#else -#include -#endif - /************************************************************************/ /* Usage() */ /************************************************************************/ @@ -359,7 +353,7 @@ MAIN_START(argc, argv) if (!bCoordOnCommandLine) { // Is it an interactive terminal ? - if (isatty(static_cast(fileno(stdin)))) + if (CPLIsInteractive(stdin)) { if (pszSrcFilename != nullptr) { diff --git a/apps/gdalwarp_lib.cpp b/apps/gdalwarp_lib.cpp index 6b0a6bdcee69..737a161f96b7 100644 --- a/apps/gdalwarp_lib.cpp +++ b/apps/gdalwarp_lib.cpp @@ -2552,8 +2552,8 @@ static GDALDatasetH GDALWarpDirect(const char *pszDest, GDALDatasetH hDstDS, { CPLString osMsg; osMsg.Printf("Processing %s [%d/%d]", - GDALGetDescription(pahSrcDS[iSrc]), iSrc + 1, - nSrcCount); + CPLGetFilename(GDALGetDescription(pahSrcDS[iSrc])), + iSrc + 1, nSrcCount); return pfnExternalProgress((iSrc + dfComplete) / nSrcCount, osMsg.c_str(), pExternalProgressData); } diff --git a/frmts/grib/gribdataset.cpp b/frmts/grib/gribdataset.cpp index 9e506e379760..d5b3bc750a4c 100644 --- a/frmts/grib/gribdataset.cpp +++ b/frmts/grib/gribdataset.cpp @@ -27,12 +27,6 @@ #include #endif -#ifndef _WIN32 -#include // isatty() -#else -#include // _isatty() -#endif - #include #include #include @@ -901,11 +895,7 @@ static bool IsGdalinfoInteractive() { static const bool bIsGdalinfoInteractive = []() { -#ifndef _WIN32 - if (isatty(static_cast(fileno(stdout)))) -#else - if (_isatty(_fileno(stdout))) -#endif + if (CPLIsInteractive(stdout)) { std::string osPath; osPath.resize(1024); diff --git a/port/cpl_conv.cpp b/port/cpl_conv.cpp index 3325fbb7bb99..f1f68b4e8885 100644 --- a/port/cpl_conv.cpp +++ b/port/cpl_conv.cpp @@ -62,6 +62,12 @@ #include // for LC_NUMERIC_MASK on MacOS #endif +#ifdef _WIN32 +#include // _isatty +#else +#include // isatty +#endif + #ifdef DEBUG_CONFIG_OPTIONS #include #endif @@ -3493,3 +3499,24 @@ CPLConfigOptionSetter::~CPLConfigOptionSetter() } //! @endcond + +/************************************************************************/ +/* CPLIsInteractive() */ +/************************************************************************/ + +/** Returns whether the provided file refers to a terminal. + * + * This function is a wrapper of the ``isatty()`` POSIX function. + * + * @param f File to test. Typically stdin, stdout or stderr + * @return true if it is an open file referring to a terminal. + * @since GDAL 3.11 + */ +bool CPLIsInteractive(FILE *f) +{ +#ifndef _WIN32 + return isatty(static_cast(fileno(f))); +#else + return _isatty(_fileno(f)); +#endif +} diff --git a/port/cpl_conv.h b/port/cpl_conv.h index 724ffc8aef33..f1d11eb06300 100644 --- a/port/cpl_conv.h +++ b/port/cpl_conv.h @@ -309,6 +309,12 @@ void CPLCleanupSetlocaleMutex(void); */ int CPL_DLL CPLIsPowerOfTwo(unsigned int i); +/* -------------------------------------------------------------------- */ +/* Terminal related */ +/* -------------------------------------------------------------------- */ + +bool CPL_DLL CPLIsInteractive(FILE *f); + CPL_C_END /* -------------------------------------------------------------------- */ diff --git a/port/cpl_error.cpp b/port/cpl_error.cpp index 62ef19fccf06..f751837377b0 100644 --- a/port/cpl_error.cpp +++ b/port/cpl_error.cpp @@ -15,10 +15,6 @@ #include "cpl_error.h" -#ifndef _WIN32 -#include // isatty() -#endif - #include #include #include @@ -1052,7 +1048,7 @@ void CPL_STDCALL CPLDefaultErrorHandler(CPLErr eErrClass, CPLErrorNum nError, #ifndef _WIN32 CPLErrorContext *psCtx = CPLGetErrorContext(); if (psCtx != nullptr && !IS_PREFEFINED_ERROR_CTX(psCtx) && - fpLog == stderr && isatty(static_cast(fileno(stderr)))) + fpLog == stderr && CPLIsInteractive(stderr)) { if (psCtx->bProgressMode) { diff --git a/port/cpl_progress.cpp b/port/cpl_progress.cpp index 4edf83b187ad..71bb78a597d1 100644 --- a/port/cpl_progress.cpp +++ b/port/cpl_progress.cpp @@ -14,6 +14,7 @@ #include #include +#include #include @@ -163,6 +164,31 @@ void CPL_STDCALL GDALDestroyScaledProgress(void *pData) CPLFree(pData); } +/************************************************************************/ +/* GDALTermProgressWidth() */ +/************************************************************************/ + +static constexpr int GDALTermProgressWidth(int nMaxTicks, int nMajorTickSpacing) +{ + int nWidth = 0; + for (int i = 0; i <= nMaxTicks; i++) + { + if (i % nMajorTickSpacing == 0) + { + int nPercent = (i * 100) / nMaxTicks; + do + { + nWidth++; + } while (nPercent /= 10); + } + else + { + nWidth += 1; + } + } + return nWidth; +} + /************************************************************************/ /* GDALTermProgress() */ /************************************************************************/ @@ -179,6 +205,11 @@ void CPL_STDCALL GDALDestroyScaledProgress(void *pData) 0...10...20...30...40...50...60...70...80...90...100 - done. \endverbatim + * Starting with GDAL 3.11, for tasks estimated to take more than 10 seconds, + * an estimated remaining time is also displayed at the end. And for tasks + * taking more than 5 seconds to complete, the total time is displayed upon + * completion. + * * Every 2.5% of progress another number or period is emitted. Note that * GDALTermProgress() uses internal static data to keep track of the last * percentage reported and will get confused if two terminal based progress @@ -200,30 +231,112 @@ int CPL_STDCALL GDALTermProgress(double dfComplete, CPL_UNUSED const char *pszMessage, CPL_UNUSED void *pProgressArg) { - const int nThisTick = - std::min(40, std::max(0, static_cast(dfComplete * 40.0))); + constexpr int MAX_TICKS = 40; + constexpr int MAJOR_TICK_SPACING = 4; + constexpr int LENGTH_OF_0_TO_100_PROGRESS = + GDALTermProgressWidth(MAX_TICKS, MAJOR_TICK_SPACING); + + const int nThisTick = std::min( + MAX_TICKS, std::max(0, static_cast(dfComplete * MAX_TICKS))); // Have we started a new progress run? static int nLastTick = -1; - if (nThisTick < nLastTick && nLastTick >= 39) + static time_t nStartTime = 0; + // whether estimated remaining time is displayed + static bool bETADisplayed = false; + // number of characters displayed during last progress call + static int nCharacterCountLastTime = 0; + // maximum number of characters displayed during previous calls + static int nCharacterCountMax = 0; + if (nThisTick < nLastTick && nLastTick >= MAX_TICKS - 1) + { + bETADisplayed = false; nLastTick = -1; + nCharacterCountLastTime = 0; + nCharacterCountMax = 0; + } if (nThisTick <= nLastTick) return TRUE; + const time_t nCurTime = time(nullptr); + if (nLastTick < 0) + nStartTime = nCurTime; + + constexpr int MIN_DELAY_FOR_ETA = 5; // in seconds + if (nCurTime - nStartTime >= MIN_DELAY_FOR_ETA && dfComplete > 0 && + dfComplete < 0.5) + { + static bool bIsTTY = CPLIsInteractive(stdout); + bETADisplayed = bIsTTY; + } + if (bETADisplayed) + { + for (int i = 0; i < nCharacterCountLastTime; ++i) + fprintf(stdout, "\b"); + nLastTick = -1; + nCharacterCountLastTime = 0; + } + while (nThisTick > nLastTick) { ++nLastTick; - if (nLastTick % 4 == 0) - fprintf(stdout, "%d", (nLastTick / 4) * 10); + if (nLastTick % MAJOR_TICK_SPACING == 0) + { + const int nPercent = (nLastTick * 100) / MAX_TICKS; + nCharacterCountLastTime += fprintf(stdout, "%d", nPercent); + } else - fprintf(stdout, "."); + { + nCharacterCountLastTime += fprintf(stdout, "."); + } } - if (nThisTick == 40) - fprintf(stdout, " - done.\n"); + if (nThisTick == MAX_TICKS) + { + nCharacterCountLastTime += fprintf(stdout, " - done"); + if (nCurTime - nStartTime >= MIN_DELAY_FOR_ETA) + { + const int nEllapsed = static_cast(nCurTime - nStartTime); + const int nHours = nEllapsed / 3600; + const int nMins = (nEllapsed % 3600) / 60; + const int nSecs = nEllapsed % 60; + nCharacterCountLastTime += + fprintf(stdout, " in %02d:%02d:%02d.", nHours, nMins, nSecs); + for (int i = nCharacterCountLastTime; i < nCharacterCountMax; ++i) + nCharacterCountLastTime += fprintf(stdout, " "); + } + else + { + fprintf(stdout, "."); + } + fprintf(stdout, "\n"); + } else + { + if (bETADisplayed) + { + for (int i = nCharacterCountLastTime; + i < LENGTH_OF_0_TO_100_PROGRESS; ++i) + nCharacterCountLastTime += fprintf(stdout, " "); + + const double dfETA = + (nCurTime - nStartTime) * (1.0 / dfComplete - 1); + const int nETA = static_cast(dfETA + 0.5); + const int nHours = nETA / 3600; + const int nMins = (nETA % 3600) / 60; + const int nSecs = nETA % 60; + nCharacterCountLastTime += + fprintf(stdout, " - estimated remaining time: %02d:%02d:%02d", + nHours, nMins, nSecs); + for (int i = nCharacterCountLastTime; i < nCharacterCountMax; ++i) + nCharacterCountLastTime += fprintf(stdout, " "); + } fflush(stdout); + } + + if (nCharacterCountLastTime > nCharacterCountMax) + nCharacterCountMax = nCharacterCountLastTime; return TRUE; }