diff --git a/source/include/includes.h b/source/include/includes.h index 8aa34ff..7548a7d 100644 --- a/source/include/includes.h +++ b/source/include/includes.h @@ -623,6 +623,12 @@ typedef int BOOL; #define _UPPER_BOOL #endif +enum timestamp_set_resolution { + TIMESTAMP_SET_SECONDS = 0, + TIMESTAMP_SET_MSEC, + TIMESTAMP_SET_NT_OR_BETTER +}; + #ifdef HAVE_BROKEN_GETGROUPS #define GID_T int #else diff --git a/source/include/smb.h b/source/include/smb.h index 7484efd..95bef1d 100644 --- a/source/include/smb.h +++ b/source/include/smb.h @@ -652,6 +652,10 @@ typedef struct connection_struct { BOOL ipc; BOOL read_only; /* Attributes for the current user of the share. */ BOOL admin_user; /* Attributes for the current user of the share. */ + /* Does this filesystem honor + sub second timestamps on files + and directories when setting time ? */ + enum timestamp_set_resolution ts_res; char *dirpath; char *connectpath; char *origpath; diff --git a/source/lib/time.c b/source/lib/time.c index 8e28fcc..0f1089c 100644 --- a/source/lib/time.c +++ b/source/lib/time.c @@ -801,14 +801,30 @@ void srv_put_dos_date3(char *buf,int offset,time_t unixdate) put_dos_date3(buf, offset, unixdate, server_zone_offset); } +void round_timespec(enum timestamp_set_resolution res, struct timespec *ts) +{ + switch (res) { + case TIMESTAMP_SET_SECONDS: + round_timespec_to_sec(ts); + break; + case TIMESTAMP_SET_MSEC: + round_timespec_to_usec(ts); + break; + case TIMESTAMP_SET_NT_OR_BETTER: + /* No rounding needed. */ + break; + } +} + /**************************************************************************** Take a Unix time and convert to an NTTIME structure and place in buffer pointed to by p. ****************************************************************************/ -void put_long_date_timespec(char *p, struct timespec ts) +void put_long_date_timespec(enum timestamp_set_resolution res, char *p, struct timespec ts) { NTTIME nt; + round_timespec(res, &ts); unix_timespec_to_nt_time(&nt, ts); SIVAL(p, 0, nt & 0xFFFFFFFF); SIVAL(p, 4, nt >> 32); @@ -819,7 +835,7 @@ void put_long_date(char *p, time_t t) struct timespec ts; ts.tv_sec = t; ts.tv_nsec = 0; - put_long_date_timespec(p, ts); + put_long_date_timespec(TIMESTAMP_SET_SECONDS, p, ts); } /**************************************************************************** @@ -827,18 +843,23 @@ void put_long_date(char *p, time_t t) structure. ****************************************************************************/ -time_t get_create_time(const SMB_STRUCT_STAT *st,BOOL fake_dirs) +struct timespec get_create_timespec(const SMB_STRUCT_STAT *st,BOOL fake_dirs) { - time_t ret, ret1; + struct timespec ret, ret1; + struct timespec c_time = get_ctimespec(st); + struct timespec m_time = get_mtimespec(st); + struct timespec a_time = get_atimespec(st); if(S_ISDIR(st->st_mode) && fake_dirs) { - return (time_t)315493200L; /* 1/1/1980 */ + ret.tv_sec = (time_t)315493200L; /* 1/1/1980 */ + ret.tv_nsec = 0; + return ret; } - - ret = MIN(st->st_ctime, st->st_mtime); - ret1 = MIN(ret, st->st_atime); - if(ret1 != (time_t)0) { + ret = timespec_compare(&c_time, &m_time) < 0 ? c_time : m_time; + ret1 = timespec_compare(&ret, &a_time) < 0 ? ret : a_time; + + if(!null_timespec(ret1)) { return ret1; } @@ -849,14 +870,6 @@ time_t get_create_time(const SMB_STRUCT_STAT *st,BOOL fake_dirs) return ret; } -struct timespec get_create_timespec(const SMB_STRUCT_STAT *st,BOOL fake_dirs) -{ - struct timespec ts; - ts.tv_sec = get_create_time(st, fake_dirs); - ts.tv_nsec = 0; - return ts; -} - /**************************************************************************** Get/Set all the possible time fields from a stat struct as a timespec. ****************************************************************************/ @@ -1145,6 +1158,27 @@ int timespec_compare(const struct timespec *ts1, const struct timespec *ts2) } /**************************************************************************** + Round up a timespec if nsec > 500000000, round down if lower, + then zero nsec. +****************************************************************************/ + +void round_timespec_to_sec(struct timespec *ts) +{ + ts->tv_sec = convert_timespec_to_time_t(*ts); + ts->tv_nsec = 0; +} + +/**************************************************************************** + Round a timespec to usec value. +****************************************************************************/ + +void round_timespec_to_usec(struct timespec *ts) +{ + struct timeval tv = convert_timespec_to_timeval(*ts); + *ts = convert_timeval_to_timespec(tv); +} + +/**************************************************************************** Interprets an nt time into a unix struct timespec. Differs from nt_time_to_unix in that an 8 byte value of 0xffffffffffffffff will be returned as (time_t)-1, whereas nt_time_to_unix returns 0 in this case. diff --git a/source/smbd/dosmode.c b/source/smbd/dosmode.c index 28d8a77..b3feb3b 100644 --- a/source/smbd/dosmode.c +++ b/source/smbd/dosmode.c @@ -550,7 +550,7 @@ int file_set_dosmode(connection_struct *conn, const char *fname, than POSIX. *******************************************************************/ -int file_ntimes(connection_struct *conn, const char *fname, const struct timespec ts[2]) +int file_ntimes(connection_struct *conn, const char *fname, struct timespec ts[2]) { SMB_STRUCT_STAT sbuf; int ret = -1; @@ -569,6 +569,18 @@ int file_ntimes(connection_struct *conn, const char *fname, const struct timespe return 0; } + if (SMB_VFS_STAT(conn, fname, &sbuf) != 0) { + return 0; + } + + if (null_timespec(ts[0])) { + ts[0] = get_atimespec(&sbuf); + } + + if (null_timespec(ts[1])) { + ts[1] = get_mtimespec(&sbuf); + } + if(SMB_VFS_NTIMES(conn, fname, ts) == 0) { return 0; } @@ -611,8 +623,8 @@ BOOL set_filetime(connection_struct *conn, const char *fname, return(True); } + ZERO_STRUCT(ts); ts[1] = mtime; /* mtime. */ - ts[0] = ts[1]; /* atime. */ if (file_ntimes(conn, fname, ts)) { DEBUG(4,("set_filetime(%s) failed: %s\n",fname,strerror(errno))); diff --git a/source/smbd/nttrans.c b/source/smbd/nttrans.c index 2b9d5da..dd7aec5 100644 --- a/source/smbd/nttrans.c +++ b/source/smbd/nttrans.c @@ -906,13 +906,13 @@ int reply_ntcreate_and_X(connection_struct *conn, dos_filetime_timespec(&m_timespec); } - put_long_date_timespec(p, c_timespec); /* create time. */ + put_long_date_timespec(conn->ts_res, p, c_timespec); /* create time. */ p += 8; - put_long_date_timespec(p, a_timespec); /* access time */ + put_long_date_timespec(conn->ts_res, p, a_timespec); /* access time */ p += 8; - put_long_date_timespec(p, m_timespec); /* write time */ + put_long_date_timespec(conn->ts_res, p, m_timespec); /* write time */ p += 8; - put_long_date_timespec(p, m_timespec); /* change time */ + put_long_date_timespec(conn->ts_res, p, m_timespec); /* change time */ p += 8; SIVAL(p,0,fattr); /* File Attributes. */ p += 4; @@ -1584,13 +1584,13 @@ static int call_nt_transact_create(connection_struct *conn, char *inbuf, char *o dos_filetime_timespec(&m_timespec); } - put_long_date_timespec(p, c_timespec); /* create time. */ + put_long_date_timespec(conn->ts_res, p, c_timespec); /* create time. */ p += 8; - put_long_date_timespec(p, a_timespec); /* access time */ + put_long_date_timespec(conn->ts_res, p, a_timespec); /* access time */ p += 8; - put_long_date_timespec(p, m_timespec); /* write time */ + put_long_date_timespec(conn->ts_res, p, m_timespec); /* write time */ p += 8; - put_long_date_timespec(p, m_timespec); /* change time */ + put_long_date_timespec(conn->ts_res, p, m_timespec); /* change time */ p += 8; SIVAL(p,0,fattr); /* File Attributes. */ p += 4; diff --git a/source/smbd/reply.c b/source/smbd/reply.c index 69a9885..25f7ac5 100644 --- a/source/smbd/reply.c +++ b/source/smbd/reply.c @@ -6070,6 +6070,7 @@ int reply_getattrE(connection_struct *conn, char *inbuf,char *outbuf, int size, int outsize = 0; int mode; files_struct *fsp = file_fsp(inbuf,smb_vwv0); + struct timespec create_ts; START_PROFILE(SMBgetattrE); outsize = set_message(outbuf,11,0,True); @@ -6093,7 +6094,10 @@ int reply_getattrE(connection_struct *conn, char *inbuf,char *outbuf, int size, * this. */ - srv_put_dos_date2(outbuf,smb_vwv0,get_create_time(&sbuf,lp_fake_dir_create_times(SNUM(conn)))); + create_ts = get_create_timespec(&sbuf, lp_fake_dir_create_times(SNUM(conn))); + + srv_put_dos_date2(outbuf,smb_vwv0, + convert_timespec_to_time_t(create_ts)); srv_put_dos_date2(outbuf,smb_vwv2,sbuf.st_atime); /* Should we check pending modtime here ? JRA */ srv_put_dos_date2(outbuf,smb_vwv4,sbuf.st_mtime); diff --git a/source/smbd/service.c b/source/smbd/service.c index bfe9649..6aad9bb 100644 --- a/source/smbd/service.c +++ b/source/smbd/service.c @@ -548,6 +548,7 @@ static connection_struct *make_connection_snum(int snum, user_struct *vuser, fstring user; fstring dev; int ret; + struct timespec atime_ts, mtime_ts, ctime_ts; *user = 0; fstrcpy(dev, pdev); @@ -1010,9 +1011,43 @@ static connection_struct *make_connection_snum(int snum, user_struct *vuser, *status = NT_STATUS_BAD_NETWORK_NAME; return NULL; } - + string_set(&conn->origpath,conn->connectpath); - + + mtime_ts = get_mtimespec(&st); + ctime_ts = get_ctimespec(&st); + atime_ts = get_atimespec(&st); + + conn->ts_res = TIMESTAMP_SET_SECONDS; + + if (mtime_ts.tv_nsec || + atime_ts.tv_nsec || + ctime_ts.tv_nsec) { + /* If any of the normal UNIX directory timestamps + * have a non-zero tv_nsec component assume + * we might be able to set sub-second timestamps. + * See what filetime set primitives we have. + */ +#if defined(HAVE_UTIMES) + /* utimes allows msec timestamps to be set. */ + conn->ts_res = TIMESTAMP_SET_MSEC; +#elif defined(HAVE_UTIME) + /* utime only allows sec timestamps to be set. */ + conn->ts_res = TIMESTAMP_SET_SECONDS; +#endif + + /* TODO. Add a configure test for the Linux + * nsec timestamp set system call, and use it + * if available.... + */ + DEBUG(10,("make_connection_snum: timestamp " + "resolution of %s " + "available on share %s, directory %s\n", + conn->ts_res == TIMESTAMP_SET_MSEC ? "msec" : "sec", + lp_servicename(conn->cnum), + conn->connectpath )); + } + #if SOFTLINK_OPTIMISATION /* resolve any soft links early if possible */ if (vfs_ChDir(conn,conn->connectpath) == 0) { diff --git a/source/smbd/trans2.c b/source/smbd/trans2.c index d3b4fb9..382f9df 100644 --- a/source/smbd/trans2.c +++ b/source/smbd/trans2.c @@ -1184,6 +1184,8 @@ static BOOL get_lanman2_dir_entry(connection_struct *conn, } if(got_match) { + files_struct *fsp = NULL; + BOOL isdots = (strequal(fname,"..") || strequal(fname,".")); if (dont_descend && !isdots) continue; @@ -1236,7 +1238,16 @@ static BOOL get_lanman2_dir_entry(connection_struct *conn, file_size = get_file_size(sbuf); allocation_size = get_allocation_size(conn,NULL,&sbuf); - mdate_ts = get_mtimespec(&sbuf); + /* Check if we already have this file open + * with a pending modtime. */ + fsp = file_find_di_first(sbuf.st_dev, sbuf.st_ino); + if (fsp && !null_timespec(fsp->pending_modtime)) { + mdate_ts = fsp->pending_modtime; + set_mtimespec(&sbuf, mdate_ts); + } else { + mdate_ts = get_mtimespec(&sbuf); + } + adate_ts = get_atimespec(&sbuf); create_date_ts = get_create_timespec(&sbuf,lp_fake_dir_create_times(SNUM(conn))); @@ -1397,10 +1408,10 @@ static BOOL get_lanman2_dir_entry(connection_struct *conn, was_8_3 = mangle_is_8_3(fname, True, conn->params); p += 4; SIVAL(p,0,reskey); p += 4; - put_long_date_timespec(p,create_date_ts); p += 8; - put_long_date_timespec(p,adate_ts); p += 8; - put_long_date_timespec(p,mdate_ts); p += 8; - put_long_date_timespec(p,mdate_ts); p += 8; + put_long_date_timespec(conn->ts_res, p,create_date_ts); p += 8; + put_long_date_timespec(conn->ts_res, p,adate_ts); p += 8; + put_long_date_timespec(conn->ts_res, p,mdate_ts); p += 8; + put_long_date_timespec(conn->ts_res, p,mdate_ts); p += 8; SOFF_T(p,0,file_size); p += 8; SOFF_T(p,0,allocation_size); p += 8; SIVAL(p,0,nt_extmode); p += 4; @@ -1443,10 +1454,10 @@ static BOOL get_lanman2_dir_entry(connection_struct *conn, DEBUG(10,("get_lanman2_dir_entry: SMB_FIND_FILE_DIRECTORY_INFO\n")); p += 4; SIVAL(p,0,reskey); p += 4; - put_long_date_timespec(p,create_date_ts); p += 8; - put_long_date_timespec(p,adate_ts); p += 8; - put_long_date_timespec(p,mdate_ts); p += 8; - put_long_date_timespec(p,mdate_ts); p += 8; + put_long_date_timespec(conn->ts_res, p,create_date_ts); p += 8; + put_long_date_timespec(conn->ts_res, p,adate_ts); p += 8; + put_long_date_timespec(conn->ts_res, p,mdate_ts); p += 8; + put_long_date_timespec(conn->ts_res, p,mdate_ts); p += 8; SOFF_T(p,0,file_size); p += 8; SOFF_T(p,0,allocation_size); p += 8; SIVAL(p,0,nt_extmode); p += 4; @@ -1464,10 +1475,10 @@ static BOOL get_lanman2_dir_entry(connection_struct *conn, DEBUG(10,("get_lanman2_dir_entry: SMB_FIND_FILE_FULL_DIRECTORY_INFO\n")); p += 4; SIVAL(p,0,reskey); p += 4; - put_long_date_timespec(p,create_date_ts); p += 8; - put_long_date_timespec(p,adate_ts); p += 8; - put_long_date_timespec(p,mdate_ts); p += 8; - put_long_date_timespec(p,mdate_ts); p += 8; + put_long_date_timespec(conn->ts_res, p,create_date_ts); p += 8; + put_long_date_timespec(conn->ts_res, p,adate_ts); p += 8; + put_long_date_timespec(conn->ts_res, p,mdate_ts); p += 8; + put_long_date_timespec(conn->ts_res, p,mdate_ts); p += 8; SOFF_T(p,0,file_size); p += 8; SOFF_T(p,0,allocation_size); p += 8; SIVAL(p,0,nt_extmode); p += 4; @@ -1509,10 +1520,10 @@ static BOOL get_lanman2_dir_entry(connection_struct *conn, DEBUG(10,("get_lanman2_dir_entry: SMB_FIND_ID_FULL_DIRECTORY_INFO\n")); p += 4; SIVAL(p,0,reskey); p += 4; - put_long_date_timespec(p,create_date_ts); p += 8; - put_long_date_timespec(p,adate_ts); p += 8; - put_long_date_timespec(p,mdate_ts); p += 8; - put_long_date_timespec(p,mdate_ts); p += 8; + put_long_date_timespec(conn->ts_res, p,create_date_ts); p += 8; + put_long_date_timespec(conn->ts_res, p,adate_ts); p += 8; + put_long_date_timespec(conn->ts_res, p,mdate_ts); p += 8; + put_long_date_timespec(conn->ts_res, p,mdate_ts); p += 8; SOFF_T(p,0,file_size); p += 8; SOFF_T(p,0,allocation_size); p += 8; SIVAL(p,0,nt_extmode); p += 4; @@ -1540,10 +1551,10 @@ static BOOL get_lanman2_dir_entry(connection_struct *conn, was_8_3 = mangle_is_8_3(fname, True, conn->params); p += 4; SIVAL(p,0,reskey); p += 4; - put_long_date_timespec(p,create_date_ts); p += 8; - put_long_date_timespec(p,adate_ts); p += 8; - put_long_date_timespec(p,mdate_ts); p += 8; - put_long_date_timespec(p,mdate_ts); p += 8; + put_long_date_timespec(conn->ts_res, p,create_date_ts); p += 8; + put_long_date_timespec(conn->ts_res, p,adate_ts); p += 8; + put_long_date_timespec(conn->ts_res, p,mdate_ts); p += 8; + put_long_date_timespec(conn->ts_res, p,mdate_ts); p += 8; SOFF_T(p,0,file_size); p += 8; SOFF_T(p,0,allocation_size); p += 8; SIVAL(p,0,nt_extmode); p += 4; @@ -3038,9 +3049,9 @@ static char *store_file_unix_basic(connection_struct *conn, SOFF_T(pdata,0,get_allocation_size(conn,fsp,psbuf)); /* Number of bytes used on disk - 64 Bit */ pdata += 8; - put_long_date_timespec(pdata,get_ctimespec(psbuf)); /* Change Time 64 Bit */ - put_long_date_timespec(pdata+8,get_atimespec(psbuf)); /* Last access time 64 Bit */ - put_long_date_timespec(pdata+16,get_mtimespec(psbuf)); /* Last modification time 64 Bit */ + put_long_date_timespec(TIMESTAMP_SET_NT_OR_BETTER, pdata,get_ctimespec(psbuf)); /* Change Time 64 Bit */ + put_long_date_timespec(TIMESTAMP_SET_NT_OR_BETTER, pdata+8,get_atimespec(psbuf)); /* Last access time 64 Bit */ + put_long_date_timespec(TIMESTAMP_SET_NT_OR_BETTER, pdata+16,get_mtimespec(psbuf)); /* Last modification time 64 Bit */ pdata += 24; SIVAL(pdata,0,psbuf->st_uid); /* user id for the owner */ @@ -3180,7 +3191,7 @@ static char *store_file_unix_basic_info2(connection_struct *conn, pdata = store_file_unix_basic(conn, pdata, fsp, psbuf); /* Create (birth) time 64 bit */ - put_long_date_timespec(pdata, get_create_timespec(psbuf, False)); + put_long_date_timespec(TIMESTAMP_SET_NT_OR_BETTER, pdata, get_create_timespec(psbuf, False)); pdata += 8; map_info2_flags_from_sbuf(psbuf, &file_flags, &flags_mask); @@ -3589,10 +3600,10 @@ total_data=%u (should be %u)\n", (unsigned int)total_data, (unsigned int)IVAL(pd data_size = 40; SIVAL(pdata,36,0); } - put_long_date_timespec(pdata,create_time_ts); - put_long_date_timespec(pdata+8,atime_ts); - put_long_date_timespec(pdata+16,mtime_ts); /* write time */ - put_long_date_timespec(pdata+24,mtime_ts); /* change time */ + put_long_date_timespec(conn->ts_res, pdata,create_time_ts); + put_long_date_timespec(conn->ts_res, pdata+8,atime_ts); + put_long_date_timespec(conn->ts_res, pdata+16,mtime_ts); /* write time */ + put_long_date_timespec(conn->ts_res, pdata+24,mtime_ts); /* change time */ SIVAL(pdata,32,mode); DEBUG(5,("SMB_QFBI - ")); @@ -3673,10 +3684,10 @@ total_data=%u (should be %u)\n", (unsigned int)total_data, (unsigned int)IVAL(pd { unsigned int ea_size = estimate_ea_size(conn, fsp, fname); DEBUG(10,("call_trans2qfilepathinfo: SMB_FILE_ALL_INFORMATION\n")); - put_long_date_timespec(pdata,create_time_ts); - put_long_date_timespec(pdata+8,atime_ts); - put_long_date_timespec(pdata+16,mtime_ts); /* write time */ - put_long_date_timespec(pdata+24,mtime_ts); /* change time */ + put_long_date_timespec(conn->ts_res, pdata,create_time_ts); + put_long_date_timespec(conn->ts_res, pdata+8,atime_ts); + put_long_date_timespec(conn->ts_res, pdata+16,mtime_ts); /* write time */ + put_long_date_timespec(conn->ts_res, pdata+24,mtime_ts); /* change time */ SIVAL(pdata,32,mode); SIVAL(pdata,36,0); /* padding. */ pdata += 40; @@ -3782,10 +3793,10 @@ total_data=%u (should be %u)\n", (unsigned int)total_data, (unsigned int)IVAL(pd case SMB_FILE_NETWORK_OPEN_INFORMATION: DEBUG(10,("call_trans2qfilepathinfo: SMB_FILE_NETWORK_OPEN_INFORMATION\n")); - put_long_date_timespec(pdata,create_time_ts); - put_long_date_timespec(pdata+8,atime_ts); - put_long_date_timespec(pdata+16,mtime_ts); /* write time */ - put_long_date_timespec(pdata+24,mtime_ts); /* change time */ + put_long_date_timespec(conn->ts_res, pdata,create_time_ts); + put_long_date_timespec(conn->ts_res, pdata+8,atime_ts); + put_long_date_timespec(conn->ts_res, pdata+16,mtime_ts); /* write time */ + put_long_date_timespec(conn->ts_res, pdata+24,mtime_ts); /* change time */ SOFF_T(pdata,32,allocation_size); SOFF_T(pdata,40,file_size); SIVAL(pdata,48,mode); @@ -4097,6 +4108,7 @@ static NTSTATUS smb_set_file_time(connection_struct *conn, const SMB_STRUCT_STAT *psbuf, struct timespec ts[2]) { + struct timespec ts_stat[2]; uint32 action = FILE_NOTIFY_CHANGE_LAST_ACCESS |FILE_NOTIFY_CHANGE_LAST_WRITE; @@ -4117,21 +4129,14 @@ static NTSTATUS smb_set_file_time(connection_struct *conn, action &= ~FILE_NOTIFY_CHANGE_LAST_WRITE; } - DEBUG(6,("smb_set_file_time: actime: %s " , time_to_asc(convert_timespec_to_time_t(ts[0])) )); - DEBUG(6,("smb_set_file_time: modtime: %s ", time_to_asc(convert_timespec_to_time_t(ts[1])) )); + /* Ensure the resolution is the correct for + * what we can store on this filesystem. */ - /* - * Try and set the times of this file if - * they are different from the current values. - */ + round_timespec(conn->ts_res, &ts[0]); + round_timespec(conn->ts_res, &ts[1]); - { - struct timespec mts = get_mtimespec(psbuf); - struct timespec ats = get_atimespec(psbuf); - if ((timespec_compare(&ts[0], &ats) == 0) && (timespec_compare(&ts[1], &mts) == 0)) { - return NT_STATUS_OK; - } - } + DEBUG(6,("smb_set_file_time: actime: %s " , time_to_asc(convert_timespec_to_time_t(ts[0])) )); + DEBUG(6,("smb_set_file_time: modtime: %s ", time_to_asc(convert_timespec_to_time_t(ts[1])) )); if(fsp != NULL) { /* @@ -4153,9 +4158,18 @@ static NTSTATUS smb_set_file_time(connection_struct *conn, } DEBUG(10,("smb_set_file_time: setting utimes to modified values.\n")); - if(file_ntimes(conn, fname, ts)!=0) { - return map_nt_error_from_unix(errno); + ts_stat[0] = get_atimespec(psbuf); + ts_stat[1] = get_mtimespec(psbuf); + round_timespec(conn->ts_res, &ts_stat[0]); + round_timespec(conn->ts_res, &ts_stat[1]); + + if (timespec_compare(&ts[0], &ts_stat[0]) || + timespec_compare(&ts[1], &ts_stat[1])) { + if(file_ntimes(conn, fname, ts)!=0) { + return map_nt_error_from_unix(errno); + } } + if (action != 0) { notify_fname(conn, NOTIFY_ACTION_MODIFIED, action, fname); }