2018-05-03 21:45:43 +02:00
#include "Utils.hpp"
2018-06-20 13:57:37 +02:00
#include "I18N.hpp"
2018-05-03 21:45:43 +02:00
2017-08-03 17:31:31 +02:00
#include <locale>
2017-10-30 18:15:41 +01:00
#include <ctime>
2018-11-19 11:07:39 +01:00
#include <cstdarg>
#include <stdio.h>
2017-08-03 17:31:31 +02:00
2021-03-15 16:19:22 +01:00
#include "Platform.hpp"
2019-09-11 12:13:59 +02:00
#include "Time.hpp"
2018-04-20 11:05:00 +02:00
#ifdef WIN32
2019-07-17 15:48:53 +02:00
#include <windows.h>
#include <psapi.h>
2018-04-20 11:05:00 +02:00
#else
2019-07-17 15:48:53 +02:00
#include <unistd.h>
#include <sys/types.h>
#include <sys/param.h>
2019-08-05 13:01:23 +02:00
#include <sys/resource.h>
2019-07-17 15:48:53 +02:00
#ifdef BSD
#include <sys/sysctl.h>
#endif
2021-03-15 16:19:22 +01:00
#ifdef __APPLE__
2019-08-05 13:01:23 +02:00
#include <mach/mach.h>
#endif
2021-03-15 16:19:22 +01:00
#ifdef __linux__
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/sendfile.h>
#endif
2018-04-20 11:05:00 +02:00
#endif
2016-12-12 19:13:33 +01:00
#include <boost/log/core.hpp>
#include <boost/log/trivial.hpp>
#include <boost/log/expressions.hpp>
2017-08-03 17:31:31 +02:00
#include <boost/locale.hpp>
2017-10-25 12:53:31 +02:00
#include <boost/algorithm/string/predicate.hpp>
2017-10-17 20:00:15 +02:00
#include <boost/filesystem.hpp>
2017-12-21 16:56:33 +01:00
#include <boost/filesystem/path.hpp>
#include <boost/nowide/fstream.hpp>
2017-08-03 17:31:31 +02:00
#include <boost/nowide/convert.hpp>
2018-09-14 09:28:00 +02:00
#include <boost/nowide/cstdio.hpp>
2017-08-03 17:31:31 +02:00
2018-09-17 15:12:13 +02:00
#include <tbb/task_scheduler_init.h>
2017-08-03 17:31:31 +02:00
2021-01-11 13:31:30 +01:00
#if defined(__linux__) || defined(__GNUC__ )
2019-02-03 15:30:37 +01:00
#include <strings.h>
2021-01-11 13:31:30 +01:00
#endif /* __linux__ */
2019-02-03 15:30:37 +01:00
#ifdef _MSC_VER
#define strcasecmp _stricmp
#endif
2018-08-09 21:15:49 +02:00
2016-12-12 19:13:33 +01:00
namespace Slic3r {
2017-03-03 14:38:25 +01:00
static boost :: log :: trivial :: severity_level logSeverity = boost :: log :: trivial :: error ;
2016-12-12 19:13:33 +01:00
2019-02-15 13:40:23 +01:00
static boost :: log :: trivial :: severity_level level_to_boost ( unsigned level )
2016-12-12 19:13:33 +01:00
{
switch ( level ) {
2017-03-03 12:53:05 +01:00
// Report fatal errors only.
2019-02-15 13:40:23 +01:00
case 0 : return boost :: log :: trivial :: fatal ;
2017-03-03 12:53:05 +01:00
// Report fatal errors and errors.
2019-02-15 13:40:23 +01:00
case 1 : return boost :: log :: trivial :: error ;
2017-03-03 12:53:05 +01:00
// Report fatal errors, errors and warnings.
2019-02-15 13:40:23 +01:00
case 2 : return boost :: log :: trivial :: warning ;
2017-03-03 12:53:05 +01:00
// Report all errors, warnings and infos.
2019-02-15 13:40:23 +01:00
case 3 : return boost :: log :: trivial :: info ;
2017-03-03 12:53:05 +01:00
// Report all errors, warnings, infos and debugging.
2019-02-15 13:40:23 +01:00
case 4 : return boost :: log :: trivial :: debug ;
2017-03-03 12:53:05 +01:00
// Report everyting including fine level tracing information.
2019-02-15 13:40:23 +01:00
default : return boost :: log :: trivial :: trace ;
2016-12-12 19:13:33 +01:00
}
2019-02-15 13:40:23 +01:00
}
void set_logging_level ( unsigned int level )
{
logSeverity = level_to_boost ( level );
2016-12-12 19:13:33 +01:00
boost :: log :: core :: get () -> set_filter
(
boost :: log :: trivial :: severity >= logSeverity
);
}
2018-12-18 18:40:35 +01:00
unsigned get_logging_level ()
{
switch ( logSeverity ) {
case boost :: log :: trivial :: fatal : return 0 ;
case boost :: log :: trivial :: error : return 1 ;
case boost :: log :: trivial :: warning : return 2 ;
case boost :: log :: trivial :: info : return 3 ;
case boost :: log :: trivial :: debug : return 4 ;
2019-02-15 13:40:23 +01:00
case boost :: log :: trivial :: trace : return 5 ;
2018-12-18 18:40:35 +01:00
default : return 1 ;
}
}
2019-08-08 15:24:23 +02:00
// Force set_logging_level(<=error) after loading of the DLL.
// This is currently only needed if libslic3r is loaded as a shared library into Perl interpreter
// to perform unit and integration tests.
static struct RunOnInit {
RunOnInit () {
set_logging_level ( 1 );
}
} g_RunOnInit ;
2017-03-03 12:53:05 +01:00
void trace ( unsigned int level , const char * message )
{
2019-02-15 13:40:23 +01:00
boost :: log :: trivial :: severity_level severity = level_to_boost ( level );
2017-03-03 12:53:05 +01:00
BOOST_LOG_STREAM_WITH_PARAMS ( :: boost :: log :: trivial :: logger :: get (), \
( :: boost :: log :: keywords :: severity = severity )) << message ;
}
2018-08-09 21:15:49 +02:00
void disable_multi_threading ()
{
// Disable parallelization so the Shiny profiler works
static tbb :: task_scheduler_init * tbb_init = nullptr ;
if ( tbb_init == nullptr )
tbb_init = new tbb :: task_scheduler_init ( 1 );
}
2017-10-17 20:00:15 +02:00
static std :: string g_var_dir ;
void set_var_dir ( const std :: string & dir )
{
g_var_dir = dir ;
}
const std :: string & var_dir ()
{
return g_var_dir ;
}
std :: string var ( const std :: string & file_name )
{
2018-05-23 13:19:25 +02:00
auto file = ( boost :: filesystem :: path ( g_var_dir ) / file_name ). make_preferred ();
2017-10-17 20:00:15 +02:00
return file . string ();
}
2017-12-10 13:19:44 +01:00
static std :: string g_resources_dir ;
void set_resources_dir ( const std :: string & dir )
{
g_resources_dir = dir ;
}
const std :: string & resources_dir ()
{
return g_resources_dir ;
}
2018-02-12 08:57:32 +01:00
static std :: string g_local_dir ;
void set_local_dir ( const std :: string & dir )
2018-02-07 17:13:52 +01:00
{
2018-02-12 08:57:32 +01:00
g_local_dir = dir ;
}
const std :: string & localization_dir ()
{
return g_local_dir ;
2018-02-07 17:13:52 +01:00
}
2018-06-20 13:57:37 +02:00
// Translate function callback, to call wxWidgets translate function to convert non-localized UTF8 string to a localized one.
2018-06-20 18:33:46 +02:00
Slic3r :: I18N :: translate_fn_type Slic3r :: I18N :: translate_fn = nullptr ;
2018-06-20 13:57:37 +02:00
2017-10-25 12:53:31 +02:00
static std :: string g_data_dir ;
void set_data_dir ( const std :: string & dir )
{
2018-04-13 15:08:58 +02:00
g_data_dir = dir ;
2017-10-25 12:53:31 +02:00
}
const std :: string & data_dir ()
{
return g_data_dir ;
}
2019-08-20 16:19:30 +02:00
#ifdef _WIN32
// The following helpers are borrowed from the LLVM project https://github.com/llvm
namespace WindowsSupport
{
template < typename HandleTraits >
class ScopedHandle {
typedef typename HandleTraits :: handle_type handle_type ;
handle_type Handle ;
ScopedHandle ( const ScopedHandle & other ) = delete ;
void operator = ( const ScopedHandle & other ) = delete ;
public :
ScopedHandle () : Handle ( HandleTraits :: GetInvalid ()) {}
explicit ScopedHandle ( handle_type h ) : Handle ( h ) {}
~ ScopedHandle () { if ( HandleTraits :: IsValid ( Handle )) HandleTraits :: Close ( Handle ); }
handle_type take () {
handle_type t = Handle ;
Handle = HandleTraits :: GetInvalid ();
return t ;
}
ScopedHandle & operator = ( handle_type h ) {
if ( HandleTraits :: IsValid ( Handle ))
HandleTraits :: Close ( Handle );
Handle = h ;
return * this ;
}
// True if Handle is valid.
explicit operator bool () const { return HandleTraits :: IsValid ( Handle ) ? true : false ; }
operator handle_type () const { return Handle ; }
};
struct CommonHandleTraits {
typedef HANDLE handle_type ;
static handle_type GetInvalid () { return INVALID_HANDLE_VALUE ; }
static void Close ( handle_type h ) { :: CloseHandle ( h ); }
static bool IsValid ( handle_type h ) { return h != GetInvalid (); }
};
typedef ScopedHandle < CommonHandleTraits > ScopedFileHandle ;
std :: error_code map_windows_error ( unsigned windows_error_code )
{
#define MAP_ERR_TO_COND(x, y) case x: return std::make_error_code(std::errc::y)
switch ( windows_error_code ) {
MAP_ERR_TO_COND ( ERROR_ACCESS_DENIED , permission_denied );
MAP_ERR_TO_COND ( ERROR_ALREADY_EXISTS , file_exists );
MAP_ERR_TO_COND ( ERROR_BAD_UNIT , no_such_device );
MAP_ERR_TO_COND ( ERROR_BUFFER_OVERFLOW , filename_too_long );
MAP_ERR_TO_COND ( ERROR_BUSY , device_or_resource_busy );
MAP_ERR_TO_COND ( ERROR_BUSY_DRIVE , device_or_resource_busy );
MAP_ERR_TO_COND ( ERROR_CANNOT_MAKE , permission_denied );
MAP_ERR_TO_COND ( ERROR_CANTOPEN , io_error );
MAP_ERR_TO_COND ( ERROR_CANTREAD , io_error );
MAP_ERR_TO_COND ( ERROR_CANTWRITE , io_error );
MAP_ERR_TO_COND ( ERROR_CURRENT_DIRECTORY , permission_denied );
MAP_ERR_TO_COND ( ERROR_DEV_NOT_EXIST , no_such_device );
MAP_ERR_TO_COND ( ERROR_DEVICE_IN_USE , device_or_resource_busy );
MAP_ERR_TO_COND ( ERROR_DIR_NOT_EMPTY , directory_not_empty );
MAP_ERR_TO_COND ( ERROR_DIRECTORY , invalid_argument );
MAP_ERR_TO_COND ( ERROR_DISK_FULL , no_space_on_device );
MAP_ERR_TO_COND ( ERROR_FILE_EXISTS , file_exists );
MAP_ERR_TO_COND ( ERROR_FILE_NOT_FOUND , no_such_file_or_directory );
MAP_ERR_TO_COND ( ERROR_HANDLE_DISK_FULL , no_space_on_device );
MAP_ERR_TO_COND ( ERROR_INVALID_ACCESS , permission_denied );
MAP_ERR_TO_COND ( ERROR_INVALID_DRIVE , no_such_device );
MAP_ERR_TO_COND ( ERROR_INVALID_FUNCTION , function_not_supported );
MAP_ERR_TO_COND ( ERROR_INVALID_HANDLE , invalid_argument );
MAP_ERR_TO_COND ( ERROR_INVALID_NAME , invalid_argument );
MAP_ERR_TO_COND ( ERROR_LOCK_VIOLATION , no_lock_available );
MAP_ERR_TO_COND ( ERROR_LOCKED , no_lock_available );
MAP_ERR_TO_COND ( ERROR_NEGATIVE_SEEK , invalid_argument );
MAP_ERR_TO_COND ( ERROR_NOACCESS , permission_denied );
MAP_ERR_TO_COND ( ERROR_NOT_ENOUGH_MEMORY , not_enough_memory );
MAP_ERR_TO_COND ( ERROR_NOT_READY , resource_unavailable_try_again );
MAP_ERR_TO_COND ( ERROR_OPEN_FAILED , io_error );
MAP_ERR_TO_COND ( ERROR_OPEN_FILES , device_or_resource_busy );
MAP_ERR_TO_COND ( ERROR_OUTOFMEMORY , not_enough_memory );
MAP_ERR_TO_COND ( ERROR_PATH_NOT_FOUND , no_such_file_or_directory );
MAP_ERR_TO_COND ( ERROR_BAD_NETPATH , no_such_file_or_directory );
MAP_ERR_TO_COND ( ERROR_READ_FAULT , io_error );
MAP_ERR_TO_COND ( ERROR_RETRY , resource_unavailable_try_again );
MAP_ERR_TO_COND ( ERROR_SEEK , io_error );
MAP_ERR_TO_COND ( ERROR_SHARING_VIOLATION , permission_denied );
MAP_ERR_TO_COND ( ERROR_TOO_MANY_OPEN_FILES , too_many_files_open );
MAP_ERR_TO_COND ( ERROR_WRITE_FAULT , io_error );
MAP_ERR_TO_COND ( ERROR_WRITE_PROTECT , permission_denied );
MAP_ERR_TO_COND ( WSAEACCES , permission_denied );
MAP_ERR_TO_COND ( WSAEBADF , bad_file_descriptor );
MAP_ERR_TO_COND ( WSAEFAULT , bad_address );
MAP_ERR_TO_COND ( WSAEINTR , interrupted );
MAP_ERR_TO_COND ( WSAEINVAL , invalid_argument );
MAP_ERR_TO_COND ( WSAEMFILE , too_many_files_open );
MAP_ERR_TO_COND ( WSAENAMETOOLONG , filename_too_long );
default :
return std :: error_code ( windows_error_code , std :: system_category ());
}
#undef MAP_ERR_TO_COND
}
static std :: error_code rename_internal ( HANDLE from_handle , const std :: wstring & wide_to , bool replace_if_exists )
{
std :: vector < char > rename_info_buf ( sizeof ( FILE_RENAME_INFO ) - sizeof ( wchar_t ) + ( wide_to . size () * sizeof ( wchar_t )));
FILE_RENAME_INFO & rename_info = * reinterpret_cast < FILE_RENAME_INFO *> ( rename_info_buf . data ());
rename_info . ReplaceIfExists = replace_if_exists ;
rename_info . RootDirectory = 0 ;
rename_info . FileNameLength = DWORD ( wide_to . size () * sizeof ( wchar_t ));
std :: copy ( wide_to . begin (), wide_to . end (), & rename_info . FileName [ 0 ]);
:: SetLastError ( ERROR_SUCCESS );
if ( ! :: SetFileInformationByHandle ( from_handle , FileRenameInfo , & rename_info , ( DWORD ) rename_info_buf . size ())) {
unsigned Error = GetLastError ();
if ( Error == ERROR_SUCCESS )
Error = ERROR_CALL_NOT_IMPLEMENTED ; // Wine doesn't always set error code.
return map_windows_error ( Error );
}
return std :: error_code ();
}
static std :: error_code real_path_from_handle ( HANDLE H , std :: wstring & buffer )
{
buffer . resize ( MAX_PATH + 1 );
DWORD CountChars = :: GetFinalPathNameByHandleW ( H , ( LPWSTR ) buffer . data (), ( DWORD ) buffer . size () - 1 , FILE_NAME_NORMALIZED );
if ( CountChars > buffer . size ()) {
// The buffer wasn't big enough, try again. In this case the return value *does* indicate the size of the null terminator.
buffer . resize (( size_t ) CountChars );
CountChars = :: GetFinalPathNameByHandleW ( H , ( LPWSTR ) buffer . data (), ( DWORD ) buffer . size () - 1 , FILE_NAME_NORMALIZED );
}
if ( CountChars == 0 )
return map_windows_error ( GetLastError ());
buffer . resize ( CountChars );
return std :: error_code ();
}
std :: error_code rename ( const std :: string & from , const std :: string & to )
{
// Convert to utf-16.
std :: wstring wide_from = boost :: nowide :: widen ( from );
std :: wstring wide_to = boost :: nowide :: widen ( to );
ScopedFileHandle from_handle ;
// Retry this a few times to defeat badly behaved file system scanners.
for ( unsigned retry = 0 ; retry != 200 ; ++ retry ) {
if ( retry != 0 )
:: Sleep ( 10 );
from_handle = :: CreateFileW (( LPWSTR ) wide_from . data (), GENERIC_READ | DELETE , FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE , NULL , OPEN_EXISTING , FILE_ATTRIBUTE_NORMAL , NULL );
if ( from_handle )
break ;
}
if ( ! from_handle )
return map_windows_error ( GetLastError ());
// We normally expect this loop to succeed after a few iterations. If it
// requires more than 200 tries, it's more likely that the failures are due to
// a true error, so stop trying.
for ( unsigned retry = 0 ; retry != 200 ; ++ retry ) {
auto errcode = rename_internal ( from_handle , wide_to , true );
if ( errcode == std :: error_code ( ERROR_CALL_NOT_IMPLEMENTED , std :: system_category ())) {
// Wine doesn't support SetFileInformationByHandle in rename_internal.
// Fall back to MoveFileEx.
if ( std :: error_code errcode2 = real_path_from_handle ( from_handle , wide_from ))
return errcode2 ;
if ( :: MoveFileExW (( LPCWSTR ) wide_from . data (), ( LPCWSTR ) wide_to . data (), MOVEFILE_REPLACE_EXISTING ))
return std :: error_code ();
return map_windows_error ( GetLastError ());
}
if ( ! errcode || errcode != std :: errc :: permission_denied )
return errcode ;
// The destination file probably exists and is currently open in another
// process, either because the file was opened without FILE_SHARE_DELETE or
// it is mapped into memory (e.g. using MemoryBuffer). Rename it in order to
// move it out of the way of the source file. Use FILE_FLAG_DELETE_ON_CLOSE
// to arrange for the destination file to be deleted when the other process
// closes it.
ScopedFileHandle to_handle ( :: CreateFileW (( LPCWSTR ) wide_to . data (), GENERIC_READ | DELETE , FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE , NULL , OPEN_EXISTING , FILE_ATTRIBUTE_NORMAL | FILE_FLAG_DELETE_ON_CLOSE , NULL ));
if ( ! to_handle ) {
auto errcode = map_windows_error ( GetLastError ());
// Another process might have raced with us and moved the existing file
// out of the way before we had a chance to open it. If that happens, try
// to rename the source file again.
if ( errcode == std :: errc :: no_such_file_or_directory )
continue ;
return errcode ;
}
BY_HANDLE_FILE_INFORMATION FI ;
if ( ! :: GetFileInformationByHandle ( to_handle , & FI ))
return map_windows_error ( GetLastError ());
// Try to find a unique new name for the destination file.
for ( unsigned unique_id = 0 ; unique_id != 200 ; ++ unique_id ) {
std :: wstring tmp_filename = wide_to + L ".tmp" + std :: to_wstring ( unique_id );
std :: error_code errcode = rename_internal ( to_handle , tmp_filename , false );
if ( errcode ) {
if ( errcode == std :: make_error_code ( std :: errc :: file_exists ) || errcode == std :: make_error_code ( std :: errc :: permission_denied )) {
// Again, another process might have raced with us and moved the file
// before we could move it. Check whether this is the case, as it
// might have caused the permission denied error. If that was the
// case, we don't need to move it ourselves.
ScopedFileHandle to_handle2 ( :: CreateFileW (( LPCWSTR ) wide_to . data (), 0 , FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE , NULL , OPEN_EXISTING , FILE_ATTRIBUTE_NORMAL , NULL ));
if ( ! to_handle2 ) {
auto errcode = map_windows_error ( GetLastError ());
if ( errcode == std :: errc :: no_such_file_or_directory )
break ;
return errcode ;
}
BY_HANDLE_FILE_INFORMATION FI2 ;
if ( ! :: GetFileInformationByHandle ( to_handle2 , & FI2 ))
return map_windows_error ( GetLastError ());
if ( FI . nFileIndexHigh != FI2 . nFileIndexHigh || FI . nFileIndexLow != FI2 . nFileIndexLow || FI . dwVolumeSerialNumber != FI2 . dwVolumeSerialNumber )
break ;
continue ;
}
return errcode ;
}
break ;
}
// Okay, the old destination file has probably been moved out of the way at
// this point, so try to rename the source file again. Still, another
// process might have raced with us to create and open the destination
// file, so we need to keep doing this until we succeed.
}
// The most likely root cause.
return std :: make_error_code ( std :: errc :: permission_denied );
}
} // namespace WindowsSupport
#endif /* _WIN32 */
2016-12-12 19:13:33 +01:00
2018-09-14 09:28:00 +02:00
// borrowed from LVVM lib/Support/Windows/Path.inc
2019-08-20 16:19:30 +02:00
std :: error_code rename_file ( const std :: string & from , const std :: string & to )
2013-09-09 21:40:57 +02:00
{
2018-09-14 09:28:00 +02:00
#ifdef _WIN32
2019-08-20 16:19:30 +02:00
return WindowsSupport :: rename ( from , to );
2018-09-14 09:28:00 +02:00
#else
2018-09-17 17:17:38 +02:00
boost :: nowide :: remove ( to . c_str ());
2019-08-20 16:19:30 +02:00
return std :: make_error_code ( static_cast < std :: errc > ( boost :: nowide :: rename ( from . c_str (), to . c_str ())));
2018-09-14 09:28:00 +02:00
#endif
2018-05-03 21:45:43 +02:00
}
2017-08-03 19:49:41 +02:00
2021-03-15 16:19:22 +01:00
#ifdef __linux__
// Copied from boost::filesystem, to support copying a file to a weird filesystem, which does not support changing file attributes,
// for example ChromeOS Linux integration or FlashAIR WebDAV.
// Copied and simplified from boost::filesystem::detail::copy_file() with option = overwrite_if_exists and with just the Linux path kept,
// and only features supported by Linux 3.10 (on our build server with CentOS 7) are kept, namely sendfile with ranges and statx() are not supported.
bool copy_file_linux ( const boost :: filesystem :: path & from , const boost :: filesystem :: path & to , boost :: system :: error_code & ec )
{
using namespace boost :: filesystem ;
struct fd_wrapper
{
int fd { - 1 };
fd_wrapper () = default ;
explicit fd_wrapper ( int fd ) throw () : fd ( fd ) {}
~ fd_wrapper () throw () { if ( fd >= 0 ) :: close ( fd ); }
};
ec . clear ();
int err = 0 ;
// Note: Declare fd_wrappers here so that errno is not clobbered by close() that may be called in fd_wrapper destructors
fd_wrapper infile , outfile ;
while ( true ) {
infile . fd = :: open ( from . c_str (), O_RDONLY | O_CLOEXEC );
if ( infile . fd < 0 ) {
err = errno ;
if ( err == EINTR )
continue ;
fail :
ec . assign ( err , boost :: system :: system_category ());
return false ;
}
break ;
}
struct :: stat from_stat ;
if ( :: fstat ( infile . fd , & from_stat ) != 0 ) {
fail_errno :
err = errno ;
goto fail ;
}
const mode_t from_mode = from_stat . st_mode ;
if ( ! S_ISREG ( from_mode )) {
err = ENOSYS ;
goto fail ;
}
// Enable writing for the newly created files. Having write permission set is important e.g. for NFS,
// which checks the file permission on the server, even if the client's file descriptor supports writing.
mode_t to_mode = from_mode | S_IWUSR ;
int oflag = O_WRONLY | O_CLOEXEC | O_CREAT | O_TRUNC ;
while ( true ) {
outfile . fd = :: open ( to . c_str (), oflag , to_mode );
if ( outfile . fd < 0 ) {
err = errno ;
if ( err == EINTR )
continue ;
goto fail ;
}
break ;
}
struct :: stat to_stat ;
if ( :: fstat ( outfile . fd , & to_stat ) != 0 )
goto fail_errno ;
to_mode = to_stat . st_mode ;
if ( ! S_ISREG ( to_mode )) {
err = ENOSYS ;
goto fail ;
}
if ( from_stat . st_dev == to_stat . st_dev && from_stat . st_ino == to_stat . st_ino ) {
err = EEXIST ;
goto fail ;
}
//! copy_file implementation that uses sendfile loop. Requires sendfile to support file descriptors.
//FIXME Vojtech: This is a copy loop valid for Linux 2.6.33 and newer.
// copy_file_data_copy_file_range() supports cross-filesystem copying since 5.3, but Vojtech did not want to polute this
// function with that, we don't think the performance gain is worth it for the types of files we are copying,
// and our build server based on CentOS 7 with Linux 3.10 does not support that anyways.
{
// sendfile will not send more than this amount of data in one call
constexpr std :: size_t max_send_size = 0x7ffff000u ;
uintmax_t offset = 0u ;
while ( off_t ( offset ) < from_stat . st_size ) {
uintmax_t size_left = from_stat . st_size - offset ;
std :: size_t size_to_copy = max_send_size ;
if ( size_left < static_cast < uintmax_t > ( max_send_size ))
size_to_copy = static_cast < std :: size_t > ( size_left );
ssize_t sz = :: sendfile ( outfile . fd , infile . fd , nullptr , size_to_copy );
if ( sz < 0 ) {
err = errno ;
if ( err == EINTR )
continue ;
if ( err == 0 )
break ;
goto fail ; // err already contains the error code
}
offset += sz ;
}
}
// If we created a new file with an explicitly added S_IWUSR permission,
// we may need to update its mode bits to match the source file.
if ( to_mode != from_mode && :: fchmod ( outfile . fd , from_mode ) != 0 ) {
if ( platform_flavor () == PlatformFlavor :: LinuxOnChromium ) {
// Ignore that. 9p filesystem does not allow fmod().
BOOST_LOG_TRIVIAL ( info ) << "copy_file_linux() failed to fchmod() the output file \" " << to . string () << " \" to " << from_mode << ": " << ec . message () <<
" This may be expected when writing to a 9p filesystem." ;
} else {
// Generic linux. Write out an error to console. At least we may get some feedback.
BOOST_LOG_TRIVIAL ( error ) << "copy_file_linux() failed to fchmod() the output file \" " << to . string () << " \" to " << from_mode << ": " << ec . message ();
}
}
// Note: Use fsync/fdatasync followed by close to avoid dealing with the possibility of close failing with EINTR.
// Even if close fails, including with EINTR, most operating systems (presumably, except HP-UX) will close the
// file descriptor upon its return. This means that if an error happens later, when the OS flushes data to the
// underlying media, this error will go unnoticed and we have no way to receive it from close. Calling fsync/fdatasync
// ensures that all data have been written, and even if close fails for some unfathomable reason, we don't really
// care at that point.
err = :: fdatasync ( outfile . fd );
if ( err != 0 )
goto fail_errno ;
return true ;
}
#endif // __linux__
2020-10-23 18:34:19 +02:00
CopyFileResult copy_file_inner ( const std :: string & from , const std :: string & to , std :: string & error_message )
2019-12-19 15:36:00 +01:00
{
const boost :: filesystem :: path source ( from );
const boost :: filesystem :: path target ( to );
static const auto perms = boost :: filesystem :: owner_read | boost :: filesystem :: owner_write | boost :: filesystem :: group_read | boost :: filesystem :: others_read ; // aka 644
// Make sure the file has correct permission both before and after we copy over it.
// NOTE: error_code variants are used here to supress expception throwing.
// Error code of permission() calls is ignored on purpose - if they fail,
// the copy_file() function will fail appropriately and we don't want the permission()
// calls to cause needless failures on permissionless filesystems (ie. FATs on SD cards etc.)
// or when the target file doesn't exist.
boost :: system :: error_code ec ;
2020-10-23 15:36:14 +02:00
boost :: filesystem :: permissions ( target , perms , ec );
2020-11-04 09:23:47 +01:00
if ( ec )
2020-11-26 17:15:26 +01:00
BOOST_LOG_TRIVIAL ( debug ) << "boost::filesystem::permisions before copy error message (this could be irrelevant message based on file system): " << ec . message ();
2020-10-23 18:34:19 +02:00
ec . clear ();
2021-03-15 16:19:22 +01:00
#ifdef __linux__
// We want to allow copying files on Linux to succeed even if changing the file attributes fails.
// That may happen when copying on some exotic file system, for example Linux on Chrome.
copy_file_linux ( source , target , ec );
#else // __linux__
2020-10-23 18:34:19 +02:00
boost :: filesystem :: copy_file ( source , target , boost :: filesystem :: copy_option :: overwrite_if_exists , ec );
2021-03-15 16:19:22 +01:00
#endif // __linux__
2020-10-23 18:34:19 +02:00
if ( ec ) {
2020-12-01 12:01:01 +01:00
error_message = ec . message ();
2020-02-27 10:44:25 +01:00
return FAIL_COPY_FILE ;
2019-12-19 15:36:00 +01:00
}
2020-11-04 09:23:47 +01:00
ec . clear ();
2020-10-23 15:36:14 +02:00
boost :: filesystem :: permissions ( target , perms , ec );
2020-11-04 09:23:47 +01:00
if ( ec )
2020-11-26 17:15:26 +01:00
BOOST_LOG_TRIVIAL ( debug ) << "boost::filesystem::permisions after copy error message (this could be irrelevant message based on file system): " << ec . message ();
2020-02-27 10:44:25 +01:00
return SUCCESS ;
2019-12-19 15:36:00 +01:00
}
2020-10-23 18:34:19 +02:00
CopyFileResult copy_file ( const std :: string & from , const std :: string & to , std :: string & error_message , const bool with_check )
2018-06-18 15:07:17 +02:00
{
2019-12-19 15:36:00 +01:00
std :: string to_temp = to + ".tmp" ;
2020-10-23 18:34:19 +02:00
CopyFileResult ret_val = copy_file_inner ( from , to_temp , error_message );
2020-02-27 10:44:25 +01:00
if ( ret_val == SUCCESS )
2019-12-19 15:36:00 +01:00
{
2019-12-21 12:31:32 +01:00
if ( with_check )
ret_val = check_copy ( from , to_temp );
2019-12-22 11:11:48 +01:00
if ( ret_val == 0 && rename_file ( to_temp , to ))
2020-02-27 10:44:25 +01:00
ret_val = FAIL_RENAMING ;
2019-12-19 15:36:00 +01:00
}
return ret_val ;
2019-12-18 13:13:40 +01:00
}
2019-11-01 10:51:26 +01:00
2020-02-27 10:44:25 +01:00
CopyFileResult check_copy ( const std :: string & origin , const std :: string & copy )
2019-12-18 13:13:40 +01:00
{
2020-02-19 15:59:40 +01:00
boost :: nowide :: ifstream f1 ( origin , std :: ifstream :: in | std :: ifstream :: binary | std :: ifstream :: ate );
boost :: nowide :: ifstream f2 ( copy , std :: ifstream :: in | std :: ifstream :: binary | std :: ifstream :: ate );
2019-12-18 13:13:40 +01:00
2020-02-19 15:59:40 +01:00
if ( f1 . fail ())
2020-02-27 10:44:25 +01:00
return FAIL_CHECK_ORIGIN_NOT_OPENED ;
2020-02-19 15:59:40 +01:00
if ( f2 . fail ())
2020-02-27 10:44:25 +01:00
return FAIL_CHECK_TARGET_NOT_OPENED ;
2019-12-18 13:13:40 +01:00
2019-12-22 11:11:48 +01:00
std :: streampos fsize = f1 . tellg ();
if ( fsize != f2 . tellg ())
2020-02-27 10:44:25 +01:00
return FAIL_FILES_DIFFERENT ;
2019-12-18 13:13:40 +01:00
f1 . seekg ( 0 , std :: ifstream :: beg );
f2 . seekg ( 0 , std :: ifstream :: beg );
2019-12-22 11:11:48 +01:00
// Compare by reading 8 MiB buffers one at a time.
size_t buffer_size = 8 * 1024 * 1024 ;
std :: vector < char > buffer_origin ( buffer_size , 0 );
std :: vector < char > buffer_copy ( buffer_size , 0 );
do {
f1 . read ( buffer_origin . data (), buffer_size );
f2 . read ( buffer_copy . data (), buffer_size );
std :: streampos origin_cnt = f1 . gcount ();
std :: streampos copy_cnt = f2 . gcount ();
if ( origin_cnt != copy_cnt ||
( origin_cnt > 0 && std :: memcmp ( buffer_origin . data (), buffer_copy . data (), origin_cnt ) != 0 ))
// Files are different.
2020-02-27 10:44:25 +01:00
return FAIL_FILES_DIFFERENT ;
2019-12-22 11:11:48 +01:00
fsize -= origin_cnt ;
} while ( f1 . good () && f2 . good ());
// All data has been read and compared equal.
2020-02-27 10:44:25 +01:00
return ( f1 . eof () && f2 . eof () && fsize == 0 ) ? SUCCESS : FAIL_FILES_DIFFERENT ;
2018-06-18 15:07:17 +02:00
}
2019-02-03 15:30:37 +01:00
// Ignore system and hidden files, which may be created by the DropBox synchronisation process.
2019-05-14 19:46:01 +02:00
// https://github.com/prusa3d/PrusaSlicer/issues/1298
2019-02-03 15:30:37 +01:00
bool is_plain_file ( const boost :: filesystem :: directory_entry & dir_entry )
{
if ( ! boost :: filesystem :: is_regular_file ( dir_entry . status ()))
return false ;
#ifdef _MSC_VER
DWORD attributes = GetFileAttributesW ( boost :: nowide :: widen ( dir_entry . path (). string ()). c_str ());
return ( attributes & ( FILE_ATTRIBUTE_HIDDEN | FILE_ATTRIBUTE_SYSTEM )) == 0 ;
#else
return true ;
#endif
}
bool is_ini_file ( const boost :: filesystem :: directory_entry & dir_entry )
{
return is_plain_file ( dir_entry ) && strcasecmp ( dir_entry . path (). extension (). string (). c_str (), ".ini" ) == 0 ;
}
bool is_idx_file ( const boost :: filesystem :: directory_entry & dir_entry )
{
return is_plain_file ( dir_entry ) && strcasecmp ( dir_entry . path (). extension (). string (). c_str (), ".idx" ) == 0 ;
}
2021-01-05 12:26:05 +01:00
bool is_gcode_file ( const std :: string & path )
{
return boost :: iends_with ( path , ".gcode" ) || boost :: iends_with ( path , ".gco" ) ||
boost :: iends_with ( path , ".g" ) || boost :: iends_with ( path , ".ngc" );
}
2016-12-12 19:13:33 +01:00
} // namespace Slic3r
2018-06-07 11:18:28 +02:00
2017-08-03 19:49:41 +02:00
#ifdef WIN32
#ifndef NOMINMAX
# define NOMINMAX
#endif
#include <windows.h>
#endif /* WIN32 */
namespace Slic3r {
2017-12-14 13:47:22 +01:00
// Encode an UTF-8 string to the local code page.
2017-08-03 19:49:41 +02:00
std :: string encode_path ( const char * src )
{
#ifdef WIN32
// Convert the source utf8 encoded string to a wide string.
std :: wstring wstr_src = boost :: nowide :: widen ( src );
if ( wstr_src . length () == 0 )
return std :: string ();
// Convert a wide string to a local code page.
int size_needed = :: WideCharToMultiByte ( 0 , 0 , wstr_src . data (), ( int ) wstr_src . size (), nullptr , 0 , nullptr , nullptr );
std :: string str_dst ( size_needed , 0 );
2020-01-03 16:32:56 +01:00
:: WideCharToMultiByte ( 0 , 0 , wstr_src . data (), ( int ) wstr_src . size (), str_dst . data (), size_needed , nullptr , nullptr );
2017-08-03 19:49:41 +02:00
return str_dst ;
#else /* WIN32 */
return src ;
#endif /* WIN32 */
}
2017-12-14 13:47:22 +01:00
// Encode an 8-bit string from a local code page to UTF-8.
2020-12-01 11:59:03 +01:00
// Multibyte to utf8
2017-08-03 19:49:41 +02:00
std :: string decode_path ( const char * src )
{
#ifdef WIN32
int len = int ( strlen ( src ));
if ( len == 0 )
return std :: string ();
// Convert the string encoded using the local code page to a wide string.
int size_needed = :: MultiByteToWideChar ( 0 , 0 , src , len , nullptr , 0 );
std :: wstring wstr_dst ( size_needed , 0 );
2020-01-03 16:32:56 +01:00
:: MultiByteToWideChar ( 0 , 0 , src , len , wstr_dst . data (), size_needed );
2017-08-03 19:49:41 +02:00
// Convert a wide string to utf8.
return boost :: nowide :: narrow ( wstr_dst . c_str ());
#else /* WIN32 */
return src ;
#endif /* WIN32 */
}
std :: string normalize_utf8_nfc ( const char * src )
{
2017-11-02 16:21:34 +01:00
static std :: locale locale_utf8 ( boost :: locale :: generator (). generate ( "" ));
2017-08-03 19:49:41 +02:00
return boost :: locale :: normalize ( src , boost :: locale :: norm_nfc , locale_utf8 );
}
2017-12-21 16:56:33 +01:00
namespace PerlUtils {
// Get a file name including the extension.
std :: string path_to_filename ( const char * src ) { return boost :: filesystem :: path ( src ). filename (). string (); }
// Get a file name without the extension.
std :: string path_to_stem ( const char * src ) { return boost :: filesystem :: path ( src ). stem (). string (); }
// Get just the extension.
std :: string path_to_extension ( const char * src ) { return boost :: filesystem :: path ( src ). extension (). string (); }
// Get a directory without the trailing slash.
std :: string path_to_parent_path ( const char * src ) { return boost :: filesystem :: path ( src ). parent_path (). string (); }
};
2020-02-03 11:18:33 +01:00
std :: string string_printf ( const char * format , ...)
{
va_list args1 ;
va_start ( args1 , format );
va_list args2 ;
va_copy ( args2 , args1 );
2020-02-03 17:11:24 +01:00
static const size_t INITIAL_LEN = 200 ;
std :: string buffer ( INITIAL_LEN , '\0' );
2020-02-03 11:18:33 +01:00
int bufflen = :: vsnprintf ( buffer . data (), INITIAL_LEN - 1 , format , args1 );
if ( bufflen >= int ( INITIAL_LEN )) {
buffer . resize ( size_t ( bufflen ) + 1 );
:: vsnprintf ( buffer . data (), buffer . size (), format , args2 );
}
2020-02-03 17:11:24 +01:00
buffer . resize ( bufflen );
return buffer ;
2020-02-03 11:18:33 +01:00
}
2019-09-11 12:13:59 +02:00
std :: string header_slic3r_generated ()
2017-10-30 18:15:41 +01:00
{
2020-10-05 15:42:35 +02:00
return std :: string ( "generated by " SLIC3R_APP_NAME " " SLIC3R_VERSION " on " ) + Utils :: utc_timestamp ();
2017-10-30 18:15:41 +01:00
}
2020-10-05 15:42:35 +02:00
std :: string header_gcodeviewer_generated ()
{
return std :: string ( "generated by " GCODEVIEWER_APP_NAME " " SLIC3R_VERSION " on " ) + Utils :: utc_timestamp ();
}
2018-04-20 11:05:00 +02:00
unsigned get_current_pid ()
{
#ifdef WIN32
return GetCurrentProcessId ();
#else
return :: getpid ();
#endif
}
2018-07-23 14:39:50 +02:00
std :: string xml_escape ( std :: string text )
{
std :: string :: size_type pos = 0 ;
for (;;)
{
pos = text . find_first_of ( " \"\' &<>" , pos );
if ( pos == std :: string :: npos )
break ;
std :: string replacement ;
switch ( text [ pos ])
{
case '\"' : replacement = """ ; break ;
case '\'' : replacement = "'" ; break ;
case '&' : replacement = "&" ; break ;
case '<' : replacement = "<" ; break ;
case '>' : replacement = ">" ; break ;
default : break ;
}
text . replace ( pos , 1 , replacement );
pos += replacement . size ();
}
return text ;
}
2018-12-18 16:32:11 +01:00
std :: string format_memsize_MB ( size_t n )
{
std :: string out ;
size_t n2 = 0 ;
size_t scale = 1 ;
// Round to MB
n += 500000 ;
n /= 1000000 ;
while ( n >= 1000 ) {
n2 = n2 + scale * ( n % 1000 );
n /= 1000 ;
scale *= 1000 ;
}
char buf [ 8 ];
2019-06-25 13:06:04 +02:00
sprintf ( buf , "%d" , ( int ) n );
2018-12-18 16:32:11 +01:00
out = buf ;
while ( scale != 1 ) {
scale /= 1000 ;
n = n2 / scale ;
n2 = n2 % scale ;
2019-06-25 13:06:04 +02:00
sprintf ( buf , ",%03d" , ( int ) n );
2018-12-18 16:32:11 +01:00
out += buf ;
}
return out + "MB" ;
}
2019-08-05 19:57:57 +02:00
// Returns platform-specific string to be used as log output or parsed in SysInfoDialog.
// The latter parses the string with (semi)colons as separators, it should look about as
// "desc1: value1; desc2: value2" or similar (spaces should not matter).
std :: string log_memory_info ( bool ignore_loglevel )
2018-12-18 11:31:41 +01:00
{
std :: string out ;
2019-08-05 19:57:57 +02:00
if ( ignore_loglevel || logSeverity <= boost :: log :: trivial :: info ) {
#ifdef WIN32
#ifndef PROCESS_MEMORY_COUNTERS_EX
// MingW32 doesn't have this struct in psapi.h
typedef struct _PROCESS_MEMORY_COUNTERS_EX {
DWORD cb ;
DWORD PageFaultCount ;
SIZE_T PeakWorkingSetSize ;
SIZE_T WorkingSetSize ;
SIZE_T QuotaPeakPagedPoolUsage ;
SIZE_T QuotaPagedPoolUsage ;
SIZE_T QuotaPeakNonPagedPoolUsage ;
SIZE_T QuotaNonPagedPoolUsage ;
SIZE_T PagefileUsage ;
SIZE_T PeakPagefileUsage ;
SIZE_T PrivateUsage ;
} PROCESS_MEMORY_COUNTERS_EX , * PPROCESS_MEMORY_COUNTERS_EX ;
#endif /* PROCESS_MEMORY_COUNTERS_EX */
2018-12-18 11:31:41 +01:00
HANDLE hProcess = :: OpenProcess ( PROCESS_QUERY_INFORMATION | PROCESS_VM_READ , FALSE , :: GetCurrentProcessId ());
if ( hProcess != nullptr ) {
PROCESS_MEMORY_COUNTERS_EX pmc ;
if ( GetProcessMemoryInfo ( hProcess , ( PROCESS_MEMORY_COUNTERS * ) & pmc , sizeof ( pmc )))
2019-08-05 19:57:57 +02:00
out = " WorkingSet: " + format_memsize_MB ( pmc . WorkingSetSize ) + "; PrivateBytes: " + format_memsize_MB ( pmc . PrivateUsage ) + "; Pagefile(peak): " + format_memsize_MB ( pmc . PagefileUsage ) + "(" + format_memsize_MB ( pmc . PeakPagefileUsage ) + ")" ;
else
out += " Used memory: N/A" ;
2018-12-18 11:31:41 +01:00
CloseHandle ( hProcess );
}
2019-08-05 13:01:23 +02:00
#elif defined(__linux__) or defined(__APPLE__)
2019-08-05 19:57:57 +02:00
// Get current memory usage.
#ifdef __APPLE__
struct mach_task_basic_info info ;
mach_msg_type_number_t infoCount = MACH_TASK_BASIC_INFO_COUNT ;
out += " Resident memory: " ;
if ( task_info ( mach_task_self ( ), MACH_TASK_BASIC_INFO , ( task_info_t ) & info , & infoCount ) == KERN_SUCCESS )
out += format_memsize_MB (( size_t ) info . resident_size );
else
out += "N/A" ;
#else // i.e. __linux__
size_t tSize = 0 , resident = 0 , share = 0 ;
std :: ifstream buffer ( "/proc/self/statm" );
if ( buffer && ( buffer >> tSize >> resident >> share )) {
2019-08-05 13:01:23 +02:00
size_t page_size = ( size_t ) sysconf ( _SC_PAGE_SIZE ); // in case x86-64 is configured to use 2MB pages
size_t rss = resident * page_size ;
2019-08-05 19:57:57 +02:00
out += " Resident memory: " + format_memsize_MB ( rss );
out += "; Shared memory: " + format_memsize_MB ( share * page_size );
out += "; Private memory: " + format_memsize_MB ( rss - share * page_size );
2019-08-05 13:01:23 +02:00
}
2019-08-05 19:57:57 +02:00
else
out += " Used memory: N/A" ;
#endif
// Now get peak memory usage.
out += "; Peak memory usage: " ;
rusage memory_info ;
if ( getrusage ( RUSAGE_SELF , & memory_info ) == 0 )
{
size_t peak_mem_usage = ( size_t ) memory_info . ru_maxrss ;
#ifdef __linux__
peak_mem_usage *= 1024 ; // getrusage returns the value in kB on linux
#endif
out += format_memsize_MB ( peak_mem_usage );
}
else
out += "N/A" ;
2019-08-05 13:01:23 +02:00
#endif
}
return out ;
}
2018-12-18 11:31:41 +01:00
2019-07-17 15:48:53 +02:00
// Returns the size of physical memory (RAM) in bytes.
// http://nadeausoftware.com/articles/2012/09/c_c_tip_how_get_physical_memory_size_system
size_t total_physical_memory ()
{
#if defined(_WIN32) && (defined(__CYGWIN__) || defined(__CYGWIN32__))
// Cygwin under Windows. ------------------------------------
// New 64-bit MEMORYSTATUSEX isn't available. Use old 32.bit
MEMORYSTATUS status ;
status . dwLength = sizeof ( status );
GlobalMemoryStatus ( & status );
return ( size_t ) status . dwTotalPhys ;
#elif defined(_WIN32)
// Windows. -------------------------------------------------
// Use new 64-bit MEMORYSTATUSEX, not old 32-bit MEMORYSTATUS
MEMORYSTATUSEX status ;
status . dwLength = sizeof ( status );
GlobalMemoryStatusEx ( & status );
return ( size_t ) status . ullTotalPhys ;
#elif defined(__unix__) || defined(__unix) || defined(unix) || (defined(__APPLE__) && defined(__MACH__))
// UNIX variants. -------------------------------------------
// Prefer sysctl() over sysconf() except sysctl() HW_REALMEM and HW_PHYSMEM
#if defined(CTL_HW) && (defined(HW_MEMSIZE) || defined(HW_PHYSMEM64))
int mib [ 2 ];
mib [ 0 ] = CTL_HW ;
#if defined(HW_MEMSIZE)
mib [ 1 ] = HW_MEMSIZE ; // OSX. ---------------------
#elif defined(HW_PHYSMEM64)
mib [ 1 ] = HW_PHYSMEM64 ; // NetBSD, OpenBSD. ---------
#endif
int64_t size = 0 ; // 64-bit
size_t len = sizeof ( size );
if ( sysctl ( mib , 2 , & size , & len , NULL , 0 ) == 0 )
return ( size_t ) size ;
return 0L ; // Failed?
#elif defined(_SC_AIX_REALMEM)
// AIX. -----------------------------------------------------
return ( size_t ) sysconf ( _SC_AIX_REALMEM ) * ( size_t ) 1024L ;
#elif defined(_SC_PHYS_PAGES) && defined(_SC_PAGESIZE)
// FreeBSD, Linux, OpenBSD, and Solaris. --------------------
return ( size_t ) sysconf ( _SC_PHYS_PAGES ) *
( size_t ) sysconf ( _SC_PAGESIZE );
#elif defined(_SC_PHYS_PAGES) && defined(_SC_PAGE_SIZE)
// Legacy. --------------------------------------------------
return ( size_t ) sysconf ( _SC_PHYS_PAGES ) *
( size_t ) sysconf ( _SC_PAGE_SIZE );
#elif defined(CTL_HW) && (defined(HW_PHYSMEM) || defined(HW_REALMEM))
// DragonFly BSD, FreeBSD, NetBSD, OpenBSD, and OSX. --------
int mib [ 2 ];
mib [ 0 ] = CTL_HW ;
#if defined(HW_REALMEM)
mib [ 1 ] = HW_REALMEM ; // FreeBSD. -----------------
#elif defined(HW_PYSMEM)
mib [ 1 ] = HW_PHYSMEM ; // Others. ------------------
#endif
unsigned int size = 0 ; // 32-bit
size_t len = sizeof ( size );
if ( sysctl ( mib , 2 , & size , & len , NULL , 0 ) == 0 )
return ( size_t ) size ;
return 0L ; // Failed?
#endif // sysctl and sysconf variants
#else
return 0L ; // Unknown OS.
#endif
}
2017-08-03 19:49:41 +02:00
}; // namespace Slic3r