root/src/system/kernel/real_time_clock.cpp
/*
 * Copyright 2004-2009, Axel Dörfler, axeld@pinc-software.de. All rights reserved.
 * Copyright 2003, Jeff Ward, jeff@r2d2.stcloudstate.edu. All rights reserved.
 *
 * Distributed under the terms of the MIT License.
 */


#include <KernelExport.h>

#include <arch/real_time_clock.h>
#include <commpage.h>
#ifdef _COMPAT_MODE
#       include <commpage_compat.h>
#endif
#include <real_time_clock.h>
#include <real_time_data.h>
#include <syscalls.h>
#include <thread.h>

#include <stdlib.h>

//#define TRACE_TIME
#ifdef TRACE_TIME
#       define TRACE(x) dprintf x
#else
#       define TRACE(x)
#endif


#define RTC_SECONDS_DAY 86400
#define RTC_EPOCH_JULIAN_DAY 2440588
        // January 1st, 1970

static struct real_time_data *sRealTimeData;
#ifdef _COMPAT_MODE
static struct real_time_data *sRealTimeDataCompat;
#endif
static bool sIsGMT = false;
static bigtime_t sTimezoneOffset = 0;
static char sTimezoneName[B_FILE_NAME_LENGTH] = "GMT";


static void
real_time_clock_changed()
{
        timer_real_time_clock_changed();
        user_timer_real_time_clock_changed();
}


/*! Write the system time to CMOS. */
static void
rtc_system_to_hw(void)
{
        uint64 seconds;

        seconds = (arch_rtc_get_system_time_offset(sRealTimeData) + system_time()
                + (sIsGMT ? 0 : sTimezoneOffset)) / 1000000;

        arch_rtc_set_hw_time(seconds);
}


/*! Read the CMOS clock and update the system time accordingly. */
static void
rtc_hw_to_system(void)
{
        uint64 current_time;

        current_time = arch_rtc_get_hw_time();
        set_real_time_clock(current_time + (sIsGMT ? 0 : sTimezoneOffset));
}


bigtime_t
rtc_boot_time(void)
{
        return arch_rtc_get_system_time_offset(sRealTimeData);
}


static int
rtc_debug(int argc, char **argv)
{
        if (argc < 2) {
                // If no arguments were given, output all useful data.
                uint32 currentTime;
                bigtime_t systemTimeOffset
                        = arch_rtc_get_system_time_offset(sRealTimeData);

                currentTime = (systemTimeOffset + system_time()) / 1000000;
                dprintf("system_time:  %" B_PRId64 "\n", system_time());
                dprintf("system_time_offset:    %" B_PRId64 "\n", systemTimeOffset);
                dprintf("current_time: %" B_PRIu32 "\n", currentTime);
        } else {
                // If there was an argument, reset the system and hw time.
                set_real_time_clock(strtoul(argv[1], NULL, 10));
        }

        return 0;
}


status_t
rtc_init(kernel_args *args)
{
        sRealTimeData = (struct real_time_data*)allocate_commpage_entry(
                COMMPAGE_ENTRY_REAL_TIME_DATA, sizeof(struct real_time_data));
        arch_rtc_init(args, sRealTimeData);

#ifdef _COMPAT_MODE
        sRealTimeDataCompat = (struct real_time_data*)
                allocate_commpage_compat_entry(COMMPAGE_ENTRY_REAL_TIME_DATA,
                sizeof(struct real_time_data));
        arch_rtc_init(args, sRealTimeDataCompat);
#endif

        rtc_hw_to_system();

        add_debugger_command("rtc", &rtc_debug, "Set and test the real-time clock");
        return B_OK;
}


//      #pragma mark - public kernel API


void
set_real_time_clock_usecs(bigtime_t currentTime)
{
        arch_rtc_set_system_time_offset(sRealTimeData, currentTime
                - system_time());
#ifdef _COMPAT_MODE
        arch_rtc_set_system_time_offset(sRealTimeDataCompat, currentTime
                - system_time());
#endif
        rtc_system_to_hw();
        real_time_clock_changed();
}


void
set_real_time_clock(unsigned long currentTime)
{
        set_real_time_clock_usecs((bigtime_t)currentTime * 1000000);
}


unsigned long
real_time_clock(void)
{
        return (arch_rtc_get_system_time_offset(sRealTimeData) + system_time())
                / 1000000;
}


bigtime_t
real_time_clock_usecs(void)
{
        return arch_rtc_get_system_time_offset(sRealTimeData) + system_time();
}


uint32
get_timezone_offset(void)
{
        return (time_t)(sTimezoneOffset / 1000000LL);
}


// #pragma mark -


/*!     Converts the \a tm data to seconds. Note that the base year is not
        1900 as in POSIX, but 1970.
*/
uint64
rtc_tm_to_secs(const struct tm *tm)
{
        uint32 days;
        int year, month;

        month = tm->tm_mon + 1;
        year = tm->tm_year + RTC_EPOCH_BASE_YEAR;

        // Reference: Fliegel, H. F. and van Flandern, T. C. (1968).
        // Communications of the ACM, Vol. 11, No. 10 (October, 1968).
        days = tm->tm_mday - 32075 - RTC_EPOCH_JULIAN_DAY
                + 1461 * (year + 4800 + (month - 14) / 12) / 4
                + 367 * (month - 2 - 12 * ((month - 14) / 12)) / 12
                - 3 * ((year + 4900 + (month - 14) / 12) / 100) / 4;

        return (uint64)days * RTC_SECONDS_DAY + tm->tm_hour * 3600 + tm->tm_min * 60
                + tm->tm_sec;
}


void
rtc_secs_to_tm(uint64 seconds, struct tm *t)
{
        uint32 year, month, day, l, n;

        // Reference: Fliegel, H. F. and van Flandern, T. C. (1968).
        // Communications of the ACM, Vol. 11, No. 10 (October, 1968).
        l = seconds / 86400 + 68569 + RTC_EPOCH_JULIAN_DAY;
        n = 4 * l / 146097;
        l = l - (146097 * n + 3) / 4;
        year = 4000 * (l + 1) / 1461001;
        l = l - 1461 * year / 4 + 31;
        month = 80 * l / 2447;
        day = l - 2447 * month / 80;
        l = month / 11;
        month = month + 2 - 12 * l;
        year = 100 * (n - 49) + year + l;

        t->tm_mday = day;
        t->tm_mon = month - 1;
        t->tm_year = year - RTC_EPOCH_BASE_YEAR;

        seconds = seconds % RTC_SECONDS_DAY;
        t->tm_hour = seconds / 3600;

        seconds = seconds % 3600;
        t->tm_min = seconds / 60;
        t->tm_sec = seconds % 60;
}


//      #pragma mark - syscalls


bigtime_t
_user_system_time(void)
{
        syscall_64_bit_return_value();

        return system_time();
}


status_t
_user_set_real_time_clock(bigtime_t time)
{
        if (geteuid() != 0)
                return B_NOT_ALLOWED;

        set_real_time_clock_usecs(time);
        return B_OK;
}


status_t
_user_set_timezone(int32 timezoneOffset, const char *name, size_t nameLength)
{
        bigtime_t offset = (bigtime_t)timezoneOffset * 1000000LL;

        if (geteuid() != 0)
                return B_NOT_ALLOWED;

        TRACE(("old system_time_offset %lld old %lld new %lld gmt %d\n",
                arch_rtc_get_system_time_offset(sRealTimeData), sTimezoneOffset,
                offset, sIsGMT));

        if (name != NULL && nameLength > 0) {
                if (!IS_USER_ADDRESS(name)
                        || user_strlcpy(sTimezoneName, name, sizeof(sTimezoneName)) < 0)
                        return B_BAD_ADDRESS;
        }

        // We only need to update our time offset if the hardware clock
        // does not run in the local timezone.
        // Since this is shared data, we need to update it atomically.
        if (!sIsGMT) {
                arch_rtc_set_system_time_offset(sRealTimeData,
                        arch_rtc_get_system_time_offset(sRealTimeData) + sTimezoneOffset
                                - offset);
#ifdef _COMPAT_MODE
                arch_rtc_set_system_time_offset(sRealTimeDataCompat,
                        arch_rtc_get_system_time_offset(sRealTimeDataCompat)
                                + sTimezoneOffset - offset);
#endif
                real_time_clock_changed();
        }

        sTimezoneOffset = offset;

        TRACE(("new system_time_offset %lld\n",
                arch_rtc_get_system_time_offset(sRealTimeData)));

        return B_OK;
}


status_t
_user_get_timezone(int32 *_timezoneOffset, char *userName, size_t nameLength)
{
        int32 offset = (int32)(sTimezoneOffset / 1000000LL);

        if (_timezoneOffset != NULL
                && (!IS_USER_ADDRESS(_timezoneOffset)
                        || user_memcpy(_timezoneOffset, &offset, sizeof(offset)) < B_OK))
                return B_BAD_ADDRESS;

        if (userName != NULL
                && (!IS_USER_ADDRESS(userName)
                        || user_strlcpy(userName, sTimezoneName, nameLength) < 0))
                return B_BAD_ADDRESS;

        return B_OK;
}


status_t
_user_set_real_time_clock_is_gmt(bool isGMT)
{
        // store previous value
        bool wasGMT = sIsGMT;
        if (geteuid() != 0)
                return B_NOT_ALLOWED;

        sIsGMT = isGMT;

        if (wasGMT != sIsGMT) {
                arch_rtc_set_system_time_offset(sRealTimeData,
                        arch_rtc_get_system_time_offset(sRealTimeData)
                                + (sIsGMT ? 1 : -1) * sTimezoneOffset);
#ifdef _COMPAT_MODE
                arch_rtc_set_system_time_offset(sRealTimeDataCompat,
                        arch_rtc_get_system_time_offset(sRealTimeDataCompat)
                                + (sIsGMT ? 1 : -1) * sTimezoneOffset);
#endif
                real_time_clock_changed();
        }

        return B_OK;
}


status_t
_user_get_real_time_clock_is_gmt(bool *_userIsGMT)
{
        if (_userIsGMT == NULL)
                return B_BAD_VALUE;

        if (_userIsGMT != NULL
                && (!IS_USER_ADDRESS(_userIsGMT)
                        || user_memcpy(_userIsGMT, &sIsGMT, sizeof(bool)) != B_OK))
                return B_BAD_ADDRESS;

        return B_OK;
}