SDA-3589 (Auto update for mac and windows) (#1447)

* SDA-3588 - Auto update for macOS and Windows

* SDA-3588 - bump version to 19.0.1

* SDA-3588 - fix run-script-os version

* SDA-3588 - Remove old auto update and update setAppUserModelId

* SDA-3588 - Update app ID to com.symphony.electron_desktop

* SDA-3588 - Update app ID to com.symphony.electron_desktop in Symphony.cs

* SDA-3588 - Add autoUpdateUrl field in config file

* SDA-3588 - Validate url

* SDA-3588 - Include installer nsis script

* SDA-3589 - Remove os path from update url and rename artifacts

* SDA-3589 - set empty value for autoUpdateUrl

* SDA-3589 - Add validation for autoUpdateUrl field

* SDA-3589 - change auto update manual workflow
This commit is contained in:
Kiran Niranjan 2022-07-12 07:04:05 +05:30 committed by GitHub
parent b94712685c
commit 094a68f99a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
44 changed files with 589 additions and 3589 deletions

View File

@ -1,51 +0,0 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 16
VisualStudioVersion = 16.0.31112.23
MinimumVisualStudioVersion = 10.0.40219.1
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "auto_update_helper", "auto_update_helper.vcxproj", "{9A61CB06-1A97-440C-A931-508C9019F0FB}"
EndProject
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "auto_update_service", "auto_update_service.vcxproj", "{7E157BD0-86F0-4325-848C-BA1C87D9C99C}"
EndProject
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "tests", "tests.vcxproj", "{98A68237-C5F6-487D-8D49-346D6991F2C5}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|x64 = Debug|x64
Debug|x86 = Debug|x86
Release|x64 = Release|x64
Release|x86 = Release|x86
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{9A61CB06-1A97-440C-A931-508C9019F0FB}.Debug|x64.ActiveCfg = Debug|x64
{9A61CB06-1A97-440C-A931-508C9019F0FB}.Debug|x64.Build.0 = Debug|x64
{9A61CB06-1A97-440C-A931-508C9019F0FB}.Debug|x86.ActiveCfg = Debug|Win32
{9A61CB06-1A97-440C-A931-508C9019F0FB}.Debug|x86.Build.0 = Debug|Win32
{9A61CB06-1A97-440C-A931-508C9019F0FB}.Release|x64.ActiveCfg = Release|x64
{9A61CB06-1A97-440C-A931-508C9019F0FB}.Release|x64.Build.0 = Release|x64
{9A61CB06-1A97-440C-A931-508C9019F0FB}.Release|x86.ActiveCfg = Release|Win32
{9A61CB06-1A97-440C-A931-508C9019F0FB}.Release|x86.Build.0 = Release|Win32
{7E157BD0-86F0-4325-848C-BA1C87D9C99C}.Debug|x64.ActiveCfg = Debug|x64
{7E157BD0-86F0-4325-848C-BA1C87D9C99C}.Debug|x64.Build.0 = Debug|x64
{7E157BD0-86F0-4325-848C-BA1C87D9C99C}.Debug|x86.ActiveCfg = Debug|Win32
{7E157BD0-86F0-4325-848C-BA1C87D9C99C}.Debug|x86.Build.0 = Debug|Win32
{7E157BD0-86F0-4325-848C-BA1C87D9C99C}.Release|x64.ActiveCfg = Release|x64
{7E157BD0-86F0-4325-848C-BA1C87D9C99C}.Release|x64.Build.0 = Release|x64
{7E157BD0-86F0-4325-848C-BA1C87D9C99C}.Release|x86.ActiveCfg = Release|Win32
{7E157BD0-86F0-4325-848C-BA1C87D9C99C}.Release|x86.Build.0 = Release|Win32
{98A68237-C5F6-487D-8D49-346D6991F2C5}.Debug|x64.ActiveCfg = Debug|x64
{98A68237-C5F6-487D-8D49-346D6991F2C5}.Debug|x64.Build.0 = Debug|x64
{98A68237-C5F6-487D-8D49-346D6991F2C5}.Debug|x86.ActiveCfg = Debug|Win32
{98A68237-C5F6-487D-8D49-346D6991F2C5}.Debug|x86.Build.0 = Debug|Win32
{98A68237-C5F6-487D-8D49-346D6991F2C5}.Release|x64.ActiveCfg = Release|x64
{98A68237-C5F6-487D-8D49-346D6991F2C5}.Release|x64.Build.0 = Release|x64
{98A68237-C5F6-487D-8D49-346D6991F2C5}.Release|x86.ActiveCfg = Release|Win32
{98A68237-C5F6-487D-8D49-346D6991F2C5}.Release|x86.Build.0 = Release|Win32
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {ACFD70FC-B270-4021-9FB7-2FB6C2EB1C43}
EndGlobalSection
EndGlobal

View File

@ -1,302 +0,0 @@
#define _CRT_SECURE_NO_WARNINGS
#define _CRT_NONSTDC_NO_WARNINGS
//#define LOG_TO_CONSOLE_FOR_DEBUGGING
#include <windows.h>
#include <aclapi.h>
#pragma comment(lib, "advapi32.lib")
#pragma comment( lib, "shell32.lib" )
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#define IPC_LOG_INFO LOG_INFO
#define IPC_LOG_ERROR LOG_ERROR
#define IPC_LOG_LAST_ERROR LOG_LAST_ERROR
#include "ipc.h"
#define PIPE_NAME "symphony_sda_auto_update_ipc"
struct log_t {
CRITICAL_SECTION mutex;
FILE* file;
int time_offset;
} g_log;
void internal_log( char const* file, int line, char const* func, char const* level, char const* format, ... ) {
EnterCriticalSection( &g_log.mutex );
if( g_log.file ) {
char const* lastbackslash = strrchr( file, '\\' );
if( lastbackslash ) {
file = lastbackslash + 1;
}
time_t rawtime;
struct tm* info;
time( &rawtime );
info = localtime( &rawtime );
int offset = g_log.time_offset;
int offs_s = offset % 60;
offset -= offs_s;
int offs_m = ( offset % (60 * 60) ) / 60;
offset -= offs_m * 60;
int offs_h = offset / ( 60 * 60 );
fprintf( g_log.file, "%d-%02d-%02d %02d:%02d:%02d:025 %+02d:%02d | %s | %s(%d) | %s: ", info->tm_year + 1900, info->tm_mon + 1,
info->tm_mday, info->tm_hour, info->tm_min, info->tm_sec, offs_h, offs_m, level, file, line, func );
va_list args;
va_start( args, format );
vfprintf( g_log.file, format, args );
va_end( args );
fflush( g_log.file );
}
LeaveCriticalSection( &g_log.mutex );
}
void internal_log_last_error( char const* file, int line, char const* func, char const* level, char const* message ) {
EnterCriticalSection( &g_log.mutex );
DWORD error = GetLastError();
LPSTR buffer = NULL;
size_t size = FormatMessageA(
FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS,
NULL, error, MAKELANGID( LANG_NEUTRAL, SUBLANG_DEFAULT ), (LPSTR)&buffer, 0, NULL);
internal_log( file, line, func, level, "%s: LastError == %u \"%s\"", message, error, buffer ? buffer : "" );
if( buffer ) {
LocalFree( buffer );
}
LeaveCriticalSection( &g_log.mutex );
}
#define LOG_INFO( format, ... ) internal_log( __FILE__, __LINE__, __func__, "info", format "\n", __VA_ARGS__ )
#define LOG_ERROR( format, ... ) internal_log( __FILE__, __LINE__, __func__, "error", format "\n", __VA_ARGS__ )
#define LOG_LAST_ERROR( message ) internal_log_last_error( __FILE__, __LINE__, __func__, "error", message )
void log_init( char const* filename ) {
InitializeCriticalSection( &g_log.mutex );
if( filename ) {
#ifndef LOG_TO_CONSOLE_FOR_DEBUGGING
g_log.file = fopen( filename, "w" );
#else
g_log.file = stdout;
#endif
time_t rawtime = time( NULL );
struct tm* ptm = gmtime( &rawtime );
time_t gmt = mktime( ptm );
g_log.time_offset = (int)difftime( rawtime, gmt );
LOG_INFO( "Log file created: %s", filename );
}
}
int main( int argc, char* argv[] ) {
// Find argument ending with .log, if any
char const* log_filename = NULL;
for( int i = 0; i < argc; ++i ) {
char const* ext = strrchr( argv[ i ], '.' );
if( stricmp( ext, ".log" ) == 0 ) {
log_filename = argv[ i ];
break;
}
}
if( !log_filename ) {
return EXIT_FAILURE;
}
// Initialize logging
log_init( log_filename );
LOG_INFO( "argc: %d", argc );
for( int i = 0; i < argc; ++i ) {
LOG_INFO( "argv[%d]: %s", i, argv[ i ] );
}
if( argc != 4 ) {
LOG_ERROR( "Not enough arguments: %d provided, expected 4", argc );
return EXIT_FAILURE;
}
char const* installer_filename = argv[ 1 ];
char const* application_filename = argv[ 2 ];
LOG_INFO( "installer_filename: %s", installer_filename );
LOG_INFO( "application_filename: %s", application_filename );
BOOL installation_successful = FALSE;
LOG_INFO( "Connecting to IPC server" );
ipc_client_t* client = ipc_client_connect( PIPE_NAME );
if( client ) {
BOOL installation_in_progress = FALSE;
LOG_INFO( "Connected" );
char command[ 512 ];
sprintf( command, "msi %s", installer_filename );
LOG_INFO( "Sending command: \"%s\"", command );
if( ipc_client_send( client, command ) ) {
LOG_INFO( "Command sent" );
char response[ 256 ] = { 0 };
int size = 0;
int temp_size = 0;
LOG_INFO( "Receiving response" );
ipc_receive_status_t status = IPC_RECEIVE_STATUS_MORE_DATA;
while( size < sizeof( response ) - 1 && status == IPC_RECEIVE_STATUS_MORE_DATA ) {
status = ipc_client_receive( client, response + size,
sizeof( response ) - size - 1, &temp_size );
if( status == IPC_RECEIVE_STATUS_ERROR ) {
LOG_ERROR( "Receiving response failed" );
break;
}
size += temp_size;
}
response[ size ] = '\0';
LOG_INFO( "Response received: \"%s\"", response );
if( strcmp( response, "OK" ) == 0 ) {
LOG_INFO( "Installation in progress" );
installation_in_progress = TRUE;
} else {
LOG_ERROR( "Installation failed" );
}
} else {
LOG_ERROR( "Failed to send command" );
}
// Service will shut down while installation is in progress, so we need to reconnect
if( installation_in_progress ) {
ipc_client_disconnect( client );
client = NULL;
LOG_INFO( "Attempting to reconnect" );
int time_elapsed_ms = 0;
while( !client ) {
Sleep( 5000 );
client = ipc_client_connect( PIPE_NAME );
if( !client ) {
time_elapsed_ms += 5000;
if( time_elapsed_ms > 10*60*1000 ) {
LOG_ERROR( "Unable to reconnect to service after 10 minutes, things have gone very wrong" );
break;
}
}
}
}
if( client ) {
// retrieve status
LOG_INFO( "Retrieving installation status from service" );
LOG_INFO( "Sending command: \"status\"" );
if( ipc_client_send( client, "status" ) ) {
LOG_INFO( "Command sent" );
char response[ 256 ] = { 0 };
int size = 0;
int temp_size = 0;
LOG_INFO( "Receiving response" );
ipc_receive_status_t status = IPC_RECEIVE_STATUS_MORE_DATA;
while( size < sizeof( response ) - 1 && status == IPC_RECEIVE_STATUS_MORE_DATA ) {
status = ipc_client_receive( client, response + size,
sizeof( response ) - size - 1, &temp_size );
if( status == IPC_RECEIVE_STATUS_ERROR ) {
LOG_ERROR( "Receiving response failed" );
break;
}
size += temp_size;
}
response[ size ] = '\0';
if( *response ) {
if( stricmp( response, "FINISHED" ) == 0 ) {
LOG_INFO( "INSTALLATION SUCCESSFUL" );
installation_successful = TRUE;
} else if( stricmp( response, "FAILED" ) == 0 ) {
LOG_ERROR( "FAILED TO LAUNCH INSTALLER" );
} else if( stricmp( response, "INVALID" ) == 0 ) {
LOG_ERROR( "NO RECENT INSTALLATION" );
}
} else {
LOG_ERROR( "Failed to get a valid status response" );
ipc_client_disconnect( client );
client = NULL;
}
} else {
LOG_ERROR( "Failed to send command, disconnecting and aborting. Logs not collected." );
ipc_client_disconnect( client );
client = NULL;
}
// retrieve logs
if( client ) {
LOG_INFO( "Retrieving logs from service" );
bool logs_finished = false;
while( !logs_finished ) {
LOG_INFO( "Sending command: \"log\"" );
if( ipc_client_send( client, "log" ) ) {
LOG_INFO( "Command sent" );
char response[ 4096 ] = { 0 };
int size = 0;
int temp_size = 0;
LOG_INFO( "Receiving response" );
ipc_receive_status_t status = IPC_RECEIVE_STATUS_MORE_DATA;
while( size < sizeof( response ) - 1 && status == IPC_RECEIVE_STATUS_MORE_DATA ) {
status = ipc_client_receive( client, response + size,
sizeof( response ) - size - 1, &temp_size );
if( status == IPC_RECEIVE_STATUS_ERROR ) {
LOG_ERROR( "Receiving response failed" );
break;
}
size += temp_size;
}
response[ size ] = '\0';
if( *response ) {
size_t len = strlen( response );
if( len > 0 && response[ len - 1 ] == '\n' ) {
response[ len - 1 ] = '\0';
}
LOG_INFO( "SERVER LOG | %s", response );
} else {
LOG_INFO( "All logs retrieved" );
logs_finished = true;
}
} else {
LOG_ERROR( "Failed to send command, disconnecting and aborting. Logs not collected." );
ipc_client_disconnect( client );
client = NULL;
}
}
LOG_INFO( "All done, disconnecting from client" );
ipc_client_disconnect( client );
}
}
}
LOG_INFO( "Launching SDA after installation: \"%s\"", application_filename );
int result = (int)(uintptr_t) ShellExecute( NULL, NULL, application_filename,
NULL, NULL, SW_SHOWNORMAL );
if( result <= 32 ) {
LOG_LAST_ERROR( "Failed to launch SDA after installation" );
}
if( !installation_successful ) {
LOG_ERROR( "Installation failed." );
return EXIT_FAILURE;
}
LOG_INFO( "Installation successful." );
return EXIT_SUCCESS;
}
#define IPC_IMPLEMENTATION
#include "ipc.h"

View File

@ -1,34 +0,0 @@
#define IDR_ICON 101
IDR_ICON ICON "icon.ico"
#define VER_MAJOR 1
#define VER_MINOR 0
#define VER_REVISION 0
#define INNER_TO_STR(x) #x
#define TO_STR(x) INNER_TO_STR(x)
#define VER_STRING TO_STR(VER_MAJOR) "." TO_STR(VER_MINOR) "." TO_STR(VER_REVISION) ".0"
#define VS_VERSION_INFO 0x1L
VS_VERSION_INFO VERSIONINFO
FILEVERSION VER_MAJOR,VER_MINOR,VER_REVISION
BEGIN
BLOCK "StringFileInfo"
BEGIN
BLOCK "040904E4"
BEGIN
VALUE "FileDescription", "Symphony Auto Update Helper"
VALUE "ProductName", "Symphony Auto Update Helper"
VALUE "ProductVersion", VER_STRING
VALUE "LegalCopyright", "Copyright 2021 Symphony OSS"
END
END
BLOCK "VarFileInfo"
BEGIN
VALUE "Translation", 0x409, 1252
END
END

View File

@ -1,146 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup Label="ProjectConfigurations">
<ProjectConfiguration Include="Debug|Win32">
<Configuration>Debug</Configuration>
<Platform>Win32</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Release|Win32">
<Configuration>Release</Configuration>
<Platform>Win32</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Debug|x64">
<Configuration>Debug</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Release|x64">
<Configuration>Release</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
</ItemGroup>
<PropertyGroup Label="Globals">
<VCProjectVersion>15.0</VCProjectVersion>
<ProjectGuid>{9A61CB06-1A97-440C-A931-508C9019F0FB}</ProjectGuid>
<RootNamespace>autoupdate</RootNamespace>
<WindowsTargetPlatformVersion>10.0</WindowsTargetPlatformVersion>
<ProjectName>auto_update_helper</ProjectName>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'" Label="Configuration">
<ConfigurationType>Application</ConfigurationType>
<UseDebugLibraries>true</UseDebugLibraries>
<PlatformToolset>v142</PlatformToolset>
<CharacterSet>MultiByte</CharacterSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'" Label="Configuration">
<ConfigurationType>Application</ConfigurationType>
<UseDebugLibraries>false</UseDebugLibraries>
<PlatformToolset>v142</PlatformToolset>
<WholeProgramOptimization>true</WholeProgramOptimization>
<CharacterSet>MultiByte</CharacterSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'" Label="Configuration">
<ConfigurationType>Application</ConfigurationType>
<UseDebugLibraries>true</UseDebugLibraries>
<PlatformToolset>v142</PlatformToolset>
<CharacterSet>MultiByte</CharacterSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'" Label="Configuration">
<ConfigurationType>Application</ConfigurationType>
<UseDebugLibraries>false</UseDebugLibraries>
<PlatformToolset>v142</PlatformToolset>
<WholeProgramOptimization>true</WholeProgramOptimization>
<CharacterSet>MultiByte</CharacterSet>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
<ImportGroup Label="ExtensionSettings">
</ImportGroup>
<ImportGroup Label="Shared">
</ImportGroup>
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<PropertyGroup Label="UserMacros" />
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
<IntDir>$(Platform)\$(Configuration)\auto_update_helper\</IntDir>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
<IntDir>$(Platform)\$(Configuration)\auto_update_helper\</IntDir>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
<IntDir>$(Platform)\$(Configuration)\auto_update_helper\</IntDir>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<IntDir>$(Platform)\$(Configuration)\auto_update_helper\</IntDir>
</PropertyGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
<ClCompile>
<WarningLevel>Level3</WarningLevel>
<Optimization>Disabled</Optimization>
<SDLCheck>true</SDLCheck>
<ConformanceMode>true</ConformanceMode>
</ClCompile>
<Link>
<SubSystem>Console</SubSystem>
</Link>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<ClCompile>
<WarningLevel>Level3</WarningLevel>
<Optimization>Disabled</Optimization>
<SDLCheck>true</SDLCheck>
<ConformanceMode>true</ConformanceMode>
</ClCompile>
<Link>
<SubSystem>Console</SubSystem>
</Link>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
<ClCompile>
<WarningLevel>Level3</WarningLevel>
<Optimization>MaxSpeed</Optimization>
<FunctionLevelLinking>true</FunctionLevelLinking>
<IntrinsicFunctions>true</IntrinsicFunctions>
<SDLCheck>true</SDLCheck>
<ConformanceMode>true</ConformanceMode>
</ClCompile>
<Link>
<SubSystem>Console</SubSystem>
<EnableCOMDATFolding>true</EnableCOMDATFolding>
<OptimizeReferences>true</OptimizeReferences>
</Link>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
<ClCompile>
<WarningLevel>Level3</WarningLevel>
<Optimization>MaxSpeed</Optimization>
<FunctionLevelLinking>true</FunctionLevelLinking>
<IntrinsicFunctions>true</IntrinsicFunctions>
<SDLCheck>true</SDLCheck>
<ConformanceMode>true</ConformanceMode>
</ClCompile>
<Link>
<SubSystem>Console</SubSystem>
<EnableCOMDATFolding>true</EnableCOMDATFolding>
<OptimizeReferences>true</OptimizeReferences>
</Link>
</ItemDefinitionGroup>
<ItemGroup>
<ClCompile Include="auto_update_helper.c" />
</ItemGroup>
<ItemGroup>
<ClInclude Include="ipc.h" />
</ItemGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
<ImportGroup Label="ExtensionTargets">
</ImportGroup>
</Project>

View File

@ -1,625 +0,0 @@
#define _CRT_SECURE_NO_WARNINGS
#define _CRT_NONSTDC_NO_WARNINGS
#define SERVICE_LOG_INFO LOG_INFO
#define SERVICE_LOG_ERROR LOG_ERROR
#define SERVICE_LOG_LAST_ERROR LOG_LAST_ERROR
#include "service.h"
#define IPC_LOG_INFO LOG_INFO
#define IPC_LOG_ERROR LOG_ERROR
#define IPC_LOG_LAST_ERROR LOG_LAST_ERROR
#include "ipc.h"
#include <stdlib.h>
#include <stdint.h>
#include <string.h>
#include <stdio.h>
#include <time.h>
#include <windows.h>
#include <wincrypt.h>
#include <wintrust.h>
#include <Softpub.h>
#pragma comment(lib, "Shell32.lib")
#pragma comment (lib, "wintrust.lib")
#pragma comment(lib, "crypt32.lib")
#define SERVICE_NAME "symphony_sda_auto_update_service"
#define PIPE_NAME "symphony_sda_auto_update_ipc"
// The digital certificate of an installation package is validate before it is installed
// This is a list of certificate thumbprints for current and past Symphony certificates
// Whenever a new certificate is taken into use, its thumbprint needs to be added here
// or the installation packages signed with it will be rejected.
struct { BYTE hash[ 20 ]; } thumbprints[] = {
/* e846d3fb2a93007e921c3affcd7032f0186f116a */
"\xe8\x46\xd3\xfb\x2a\x93\x00\x7e\x92\x1c\x3a\xff\xcd\x70\x32\xf0\x18\x6f\x11\x6a",
/* 99b3333ac4457a4e21a527cc11040b28c15c1d3f */
"\x99\xb3\x33\x3a\xc4\x45\x7a\x4e\x21\xa5\x27\xcc\x11\x04\x0b\x28\xc1\x5c\x1d\x3f",
};
enum install_status_t {
INSTALL_STATUS_NONE,
INSTALL_STATUS_FINISHED,
INSTALL_STATUS_FAILED,
};
enum install_status_t g_status_for_last_install = INSTALL_STATUS_NONE;
// Logging
struct log_entry_t {
time_t timestamp;
char* text;
};
struct log_t {
CRITICAL_SECTION mutex;
char filename[ MAX_PATH ];
FILE* file;
int time_offset;
int count;
int capacity;
struct log_entry_t* entries;
clock_t session_end;
} g_log;
void internal_log( char const* file, int line, char const* func, char const* level, char const* format, ... ) {
EnterCriticalSection( &g_log.mutex );
char const* lastbackslash = strrchr( file, '\\' );
if( lastbackslash ) {
file = lastbackslash + 1;
}
time_t rawtime;
struct tm* info;
time( &rawtime );
info = localtime( &rawtime );
int offset = g_log.time_offset;
int offs_s = offset % 60;
offset -= offs_s;
int offs_m = ( offset % (60 * 60) ) / 60;
offset -= offs_m * 60;
int offs_h = offset / ( 60 * 60 );
if( g_log.file ) {
fprintf( g_log.file, "%d-%02d-%02d %02d:%02d:%02d:025 %+02d:%02d | %s | %s(%d) | %s: ", info->tm_year + 1900, info->tm_mon + 1,
info->tm_mday, info->tm_hour, info->tm_min, info->tm_sec, offs_h, offs_m, level, file, line, func );
va_list args;
va_start( args, format );
vfprintf( g_log.file, format, args );
va_end( args );
fflush( g_log.file );
}
size_t len = IPC_MESSAGE_MAX_LENGTH;
char* buffer = (char*) malloc( len + 1 );
size_t count = snprintf( buffer, len, "%d-%02d-%02d %02d:%02d:%02d:025 %+02d:%02d | %s | %s(%d) | %s: ", info->tm_year + 1900, info->tm_mon + 1,
info->tm_mday, info->tm_hour, info->tm_min, info->tm_sec, offs_h, offs_m, level, file, line, func );
buffer[ count ] = '\0';
va_list args;
va_start( args, format );
count += vsnprintf( buffer + count, len - count, format, args );
buffer[ count ] = '\0';
va_end( args );
if( g_log.count >= g_log.capacity ) {
g_log.capacity *= 2;
g_log.entries = (struct log_entry_t*) realloc( g_log.entries, g_log.capacity * sizeof( struct log_entry_t ) );
}
g_log.entries[ g_log.count ].timestamp = clock();
g_log.entries[ g_log.count ].text = buffer;
++g_log.count;
LeaveCriticalSection( &g_log.mutex );
}
void internal_log_last_error( char const* file, int line, char const* func, char const* level, char const* message ) {
EnterCriticalSection( &g_log.mutex );
DWORD error = GetLastError();
LPSTR buffer = NULL;
size_t size = FormatMessageA(
FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS,
NULL, error, MAKELANGID( LANG_NEUTRAL, SUBLANG_DEFAULT ), (LPSTR)&buffer, 0, NULL);
internal_log( file, line, func, level, "%s: LastError == %u \"%s\"", message, error, buffer ? buffer : "" );
if( buffer ) {
LocalFree( buffer );
}
LeaveCriticalSection( &g_log.mutex );
}
#define LOG_INFO( format, ... ) internal_log( __FILE__, __LINE__, __func__, "info", format "\n", __VA_ARGS__ )
#define LOG_ERROR( format, ... ) internal_log( __FILE__, __LINE__, __func__, "error", format "\n", __VA_ARGS__ )
#define LOG_LAST_ERROR( message ) internal_log_last_error( __FILE__, __LINE__, __func__, "error", message )
void log_init( bool usestdout ) {
InitializeCriticalSection( &g_log.mutex );
if( usestdout ) {
g_log.file = stdout;
} else {
char path[ MAX_PATH ];
ExpandEnvironmentStringsA( "%LOCALAPPDATA%\\SdaAutoUpdate", path, MAX_PATH );
CreateDirectory( path, NULL );
sprintf( g_log.filename, "%s\\saus_%d.log", path, (int) time( NULL ) );
g_log.file = fopen( g_log.filename, "w" );
}
time_t rawtime = time( NULL );
struct tm* ptm = gmtime( &rawtime );
time_t gmt = mktime( ptm );
g_log.time_offset = (int)difftime( rawtime, gmt );
g_log.count = 0;
g_log.capacity = 256;
g_log.entries = (struct log_entry_t*) malloc( g_log.capacity * sizeof( struct log_entry_t ) );
LOG_INFO( "Log file created" );
}
void retrieve_status( char* response, size_t capacity ) {
if( g_status_for_last_install == INSTALL_STATUS_FINISHED ) {
strcpy( response, "FINISHED" );
} else if( g_status_for_last_install == INSTALL_STATUS_FAILED ) {
strcpy( response, "FAILED" );
} else {
strcpy( response, "INVALID" );
}
}
void retrieve_buffered_log_line( char* response, size_t capacity ) {
EnterCriticalSection( &g_log.mutex );
if( g_log.session_end == 0 ) {
g_log.session_end = clock();
}
if( g_log.count > 0 && g_log.entries[ 0 ].timestamp <= g_log.session_end ) {
strncpy( response, g_log.entries[ 0 ].text, capacity );
free( g_log.entries[ 0 ].text );
--g_log.count;
memmove( g_log.entries, g_log.entries + 1, g_log.count * sizeof( *g_log.entries ) );
} else {
g_log.session_end = 0;
strcpy( response, "" );
}
LeaveCriticalSection( &g_log.mutex );
}
// This is Microsofts code for verifying the digital signature of a file, taken from here:
// https://docs.microsoft.com/en-us/windows/win32/seccrypto/example-c-program--verifying-the-signature-of-a-pe-file
BOOL VerifyEmbeddedSignature( LPCWSTR pwszSourceFile ) {
LONG lStatus;
DWORD dwLastError;
// Initialize the WINTRUST_FILE_INFO structure.
WINTRUST_FILE_INFO FileData;
memset( &FileData, 0, sizeof( FileData ) );
FileData.cbStruct = sizeof( WINTRUST_FILE_INFO );
FileData.pcwszFilePath = pwszSourceFile;
FileData.hFile = NULL;
FileData.pgKnownSubject = NULL;
/*
WVTPolicyGUID specifies the policy to apply on the file
WINTRUST_ACTION_GENERIC_VERIFY_V2 policy checks:
1) The certificate used to sign the file chains up to a root
certificate located in the trusted root certificate store. This
implies that the identity of the publisher has been verified by
a certification authority.
2) In cases where user interface is displayed (which this example
does not do), WinVerifyTrust will check for whether the
end entity certificate is stored in the trusted publisher store,
implying that the user trusts content from this publisher.
3) The end entity certificate has sufficient permission to sign
code, as indicated by the presence of a code signing EKU or no
EKU.
*/
GUID WVTPolicyGUID = WINTRUST_ACTION_GENERIC_VERIFY_V2;
WINTRUST_DATA WinTrustData;
// Initialize the WinVerifyTrust input data structure.
// Default all fields to 0.
memset( &WinTrustData, 0, sizeof( WinTrustData ) );
WinTrustData.cbStruct = sizeof( WinTrustData );
// Use default code signing EKU.
WinTrustData.pPolicyCallbackData = NULL;
// No data to pass to SIP.
WinTrustData.pSIPClientData = NULL;
// Disable WVT UI.
WinTrustData.dwUIChoice = WTD_UI_NONE;
// No revocation checking.
WinTrustData.fdwRevocationChecks = WTD_REVOKE_NONE;
// Verify an embedded signature on a file.
WinTrustData.dwUnionChoice = WTD_CHOICE_FILE;
// Verify action.
WinTrustData.dwStateAction = WTD_STATEACTION_VERIFY;
// Verification sets this value.
WinTrustData.hWVTStateData = NULL;
// Not used.
WinTrustData.pwszURLReference = NULL;
// This is not applicable if there is no UI because it changes
// the UI to accommodate running applications instead of
// installing applications.
WinTrustData.dwUIContext = 0;
// Set pFile.
WinTrustData.pFile = &FileData;
// WinVerifyTrust verifies signatures as specified by the GUID
// and Wintrust_Data.
lStatus = WinVerifyTrust( NULL, &WVTPolicyGUID, &WinTrustData );
BOOL result = FALSE;
switch( lStatus ) {
case ERROR_SUCCESS:
/*
Signed file:
- Hash that represents the subject is trusted.
- Trusted publisher without any verification errors.
- UI was disabled in dwUIChoice. No publisher or
time stamp chain errors.
- UI was enabled in dwUIChoice and the user clicked
"Yes" when asked to install and run the signed
subject.
*/
LOG_INFO( "The file is signed and the signature was verified." );
result = TRUE;
break;
case TRUST_E_NOSIGNATURE:
// The file was not signed or had a signature
// that was not valid.
// Get the reason for no signature.
dwLastError = GetLastError();
if( TRUST_E_NOSIGNATURE == dwLastError || TRUST_E_SUBJECT_FORM_UNKNOWN == dwLastError || TRUST_E_PROVIDER_UNKNOWN == dwLastError ) {
// The file was not signed.
LOG_ERROR( "The file is not signed." );
} else {
// The signature was not valid or there was an error
// opening the file.
LOG_LAST_ERROR( "An unknown error occurred trying to verify the signature of the file." );
}
break;
case TRUST_E_EXPLICIT_DISTRUST:
// The hash that represents the subject or the publisher
// is not allowed by the admin or user.
LOG_ERROR( "The signature is present, but specifically disallowed." );
break;
case TRUST_E_SUBJECT_NOT_TRUSTED:
// The user clicked "No" when asked to install and run.
LOG_ERROR( "The signature is present, but not trusted." );
break;
case CRYPT_E_SECURITY_SETTINGS:
/*
The hash that represents the subject or the publisher
was not explicitly trusted by the admin and the
admin policy has disabled user trust. No signature,
publisher or time stamp errors.
*/
LOG_ERROR( "CRYPT_E_SECURITY_SETTINGS - The hash "
"representing the subject or the publisher wasn't "
"explicitly trusted by the admin and admin policy "
"has disabled user trust. No signature, publisher "
"or timestamp errors.");
break;
default:
// The UI was disabled in dwUIChoice or the admin policy
// has disabled user trust. lStatus contains the
// publisher or time stamp chain error.
LOG_ERROR( "Error is: 0x%x.", lStatus );
break;
}
// Any hWVTStateData must be released by a call with close.
WinTrustData.dwStateAction = WTD_STATEACTION_CLOSE;
lStatus = WinVerifyTrust( NULL, &WVTPolicyGUID, &WinTrustData);
return result;
}
// Checks that the embedded signature is valid, and also checks that it is specifically
// a known Symphony certificate, by looking for any of the certificate thumbprints listed
// in the `thumbprints` array defined at the top of this file.
BOOL validate_installer( char const* filename ) {
size_t length = 0;
mbstowcs_s( &length, NULL, 0, filename, _TRUNCATE );
wchar_t* wfilename = (wchar_t*) malloc( sizeof( wchar_t* ) * ( length + 1 ) );
mbstowcs_s( &length, wfilename, length, filename, strlen( filename ) );
BOOL signature_valid = VerifyEmbeddedSignature( wfilename );
if( !signature_valid ) {
LOG_ERROR( "The installer was not signed with a valid certificate" );
free( wfilename );
return FALSE;
}
DWORD dwEncoding, dwContentType, dwFormatType;
HCERTSTORE hStore = NULL;
HCRYPTMSG hMsg = NULL;
BOOL result = CryptQueryObject( CERT_QUERY_OBJECT_FILE,
wfilename,
CERT_QUERY_CONTENT_FLAG_PKCS7_SIGNED_EMBED,
CERT_QUERY_FORMAT_FLAG_BINARY,
0,
&dwEncoding,
&dwContentType,
&dwFormatType,
&hStore,
&hMsg,
NULL );
free( wfilename );
if( !result ) {
LOG_LAST_ERROR( "CryptQueryObject failed" );
return FALSE;
}
BOOL found = FALSE;
for( int i = 0; i < sizeof( thumbprints ) / sizeof( *thumbprints ); ++i ) {
CRYPT_HASH_BLOB hash_blob = {
sizeof( thumbprints[ i ].hash ) / sizeof( *(thumbprints[ i ].hash) ),
thumbprints[ i ].hash
};
CERT_CONTEXT const* cert_context = CertFindCertificateInStore(
hStore,
(X509_ASN_ENCODING | PKCS_7_ASN_ENCODING),
0,
CERT_FIND_HASH,
&hash_blob,
NULL );
if( cert_context ) {
found = TRUE;
CertFreeCertificateContext( cert_context );
}
}
if( !found ) {
LOG_ERROR( "The installer was not signed with a known Symphony certificate" );
}
CryptMsgClose( hMsg );
CertCloseStore( hStore, CERT_CLOSE_STORE_FORCE_FLAG );
return found;
}
void merge_msiexec_log( char const* logfile ) {
wchar_t wlogfile[ MAX_PATH ];
MultiByteToWideChar( CP_ACP, 0, logfile, -1, wlogfile, sizeof( wlogfile ) / sizeof( *wlogfile ) );
FILE* fp = _wfopen( wlogfile, L"r, ccs=UTF-16LE");
if( !fp ) {
LOG_ERROR( "Failed to read msiexec log file" );
}
fseek( fp, 2, SEEK_SET );
static wchar_t wline[ 4096 ] = { 0 };
static char line[ 4096 ] = { 0 };
while( fgetws( wline, sizeof( wline ) / sizeof( *wline ), fp ) != NULL ) {
WideCharToMultiByte( CP_ACP, 0, (wchar_t*) wline, -1, line, sizeof( line ), NULL, NULL );
size_t len = strlen( line );
if( len > 0 ) {
char* end = ( line + len ) - 1;
if( *end == '\n' ) *end = '\0';
}
LOG_INFO( "MSIEXEC: %s", line );
memset( wline, 0, sizeof( wline ) );
memset( line, 0, sizeof( line ) );
}
fclose( fp );
}
// Runs msiexec with the supplied filename
bool run_installer( char const* filename ) {
// Reject installers which are not signed with a Symphony certificate
if( !validate_installer( filename ) ) {
LOG_ERROR( "The signature of %s could is not a valid Symphony signature" );
return false;
}
LOG_INFO( "Signature of installer successfully validated" );
char logfile[ MAX_PATH ];
ExpandEnvironmentStringsA( "%LOCALAPPDATA%\\SdaAutoUpdate\\msiexec.log", logfile, MAX_PATH );
DeleteFileA( logfile );
char command[ 512 ];
sprintf( command, "/i %s /q LAUNCH_ON_INSTALL=\"false\" /l*v \"%s\" ", filename, logfile );
LOG_INFO( "MSI command: %s", command );
SHELLEXECUTEINFO ShExecInfo = { 0 };
ShExecInfo.cbSize = sizeof( SHELLEXECUTEINFO );
ShExecInfo.fMask = SEE_MASK_NOCLOSEPROCESS;
ShExecInfo.hwnd = NULL;
ShExecInfo.lpVerb = "open";
ShExecInfo.lpFile = "msiexec";
ShExecInfo.lpParameters = command;
ShExecInfo.lpDirectory = NULL;
ShExecInfo.nShow = SW_SHOW;
ShExecInfo.hInstApp = NULL;
char statusfile[ MAX_PATH ];
ExpandEnvironmentStringsA( "%LOCALAPPDATA%\\SdaAutoUpdate\\status.sau", statusfile, MAX_PATH );
FILE* fp = fopen( statusfile, "w" );
fprintf( fp, "PENDING" );
fclose( fp );
if( ShellExecuteEx( &ShExecInfo ) ) {
fp = fopen( statusfile, "w" );
fprintf( fp, "SUCCESS" );
fclose( fp );
LOG_INFO( "ShellExecuteEx successful, waiting to finish" );
WaitForSingleObject( ShExecInfo.hProcess, INFINITE );
LOG_INFO( "ShellExecuteEx finished" );
DWORD exitCode = 0;
GetExitCodeProcess( ShExecInfo.hProcess, &exitCode );
LOG_INFO( "ShellExecuteEx exit code: %d", exitCode );
CloseHandle( ShExecInfo.hProcess );
merge_msiexec_log( logfile );
return exitCode == 0 ? true : false;
} else {
DeleteFileA( statusfile );
g_status_for_last_install = INSTALL_STATUS_FAILED;
LOG_LAST_ERROR( "Failed to run installer" );
return false;
}
}
// Called by IPC server when a request comes in. Installs the package and returns a resul
// Also detects disconnects
void ipc_handler( char const* request, void* user_data, char* response, size_t capacity ) {
LOG_INFO( "IPC handler invoked for request: %s", request );
bool* is_connected = (bool*) user_data;
if( !request ) {
LOG_INFO( "Empty request, disconnection requested" );
*is_connected = false;
service_cancel_sleep();
return;
}
// identify command
if( strlen( request ) > 5 && strnicmp( request, "msi ", 4 ) == 0 ) {
// "msi" - run installer
LOG_INFO( "MSI command, running installer" );
if( run_installer( request + 4 ) ) {
LOG_INFO( "Response: OK" );
strcpy( response, "OK" );
} else {
LOG_INFO( "Response: ERROR" );
strcpy( response, "ERROR" );
}
} else if( strlen( request ) == 6 && stricmp( request, "status" ) == 0 ) {
// "status" - return result of last install
LOG_INFO( "STATUS command, reading status file" );
retrieve_status( response, capacity );
LOG_INFO( "Status: %s", response );
} else if( strlen( request ) == 3 && stricmp( request, "log" ) == 0 ) {
// "log" - send log line
LOG_INFO( "LOG command, returning next log line" );
retrieve_buffered_log_line( response, capacity );
} else {
LOG_INFO( "Unknown command \"%s\", ignored", request );
strcpy( response, "ERROR" );
}
}
// Service main function. Keeps an IPC server running - if it gets disconnected it starts it
// up again. Install requests are handled by ipc_handler above
void service_main( bool* service_is_running ) {
LOG_INFO( "Service main function running" );
char statusfile[ MAX_PATH ];
ExpandEnvironmentStringsA( "%LOCALAPPDATA%\\SdaAutoUpdate\\status.sau", statusfile, MAX_PATH );
char status[ 32 ] = { 0 };
FILE* fp = fopen( statusfile, "r" );
if( fp ) {
fgets( status, sizeof( status ), fp );
fclose( fp );
}
if( stricmp( status, "PENDING" ) == 0 || stricmp( status, "SUCCESS" ) == 0 ) {
g_status_for_last_install = INSTALL_STATUS_FINISHED;
}
while( *service_is_running ) {
bool is_connected = true;
LOG_INFO( "Starting IPC server" );
ipc_server_t* server = ipc_server_start( PIPE_NAME, ipc_handler, &is_connected );
while( is_connected && *service_is_running ) {
service_sleep();
}
LOG_INFO( "IPC server disconnected" );
ipc_server_stop( server );
}
LOG_INFO( "Leaving service main function" );
}
int main( int argc, char** argv ) {
if( argc >= 2 && stricmp( argv[ 1 ], "test" ) == 0 ) {
log_init( true );
} else {
log_init( false );
}
// Debug helpers for install/uninstall
if( argc >= 2 && stricmp( argv[ 1 ], "install" ) == 0 ) {
if( service_install( SERVICE_NAME ) ) {
printf("Service installed successfully\n");
return EXIT_SUCCESS;
} else {
printf("Service failed to install\n");
return EXIT_FAILURE;
}
}
if( argc >= 2 && stricmp( argv[ 1 ], "uninstall" ) == 0 ) {
if( service_uninstall( SERVICE_NAME ) ) {
printf("Service uninstalled successfully\n");
return EXIT_SUCCESS;
} else {
printf("Service failed to uninstall\n");
return EXIT_FAILURE;
}
}
if( argc >= 2 && stricmp( argv[ 1 ], "test" ) == 0 ) {
// run the ipc server as a normal exe, not as an installed service, for faster turnaround and debugging when testing
bool running = true;
return service_main( &running );
}
// Run the service - called by the Windows Services system
LOG_INFO( "Starting service" );
service_run( SERVICE_NAME, service_main );
return EXIT_SUCCESS;
}
#define SERVICE_IMPLEMENTATION
#include "service.h"
#define IPC_IMPLEMENTATION
#include "ipc.h"

View File

@ -1,34 +0,0 @@
#define IDR_ICON 101
IDR_ICON ICON "icon.ico"
#define VER_MAJOR 1
#define VER_MINOR 0
#define VER_REVISION 0
#define INNER_TO_STR(x) #x
#define TO_STR(x) INNER_TO_STR(x)
#define VER_STRING TO_STR(VER_MAJOR) "." TO_STR(VER_MINOR) "." TO_STR(VER_REVISION) ".0"
#define VS_VERSION_INFO 0x1L
VS_VERSION_INFO VERSIONINFO
FILEVERSION VER_MAJOR,VER_MINOR,VER_REVISION
BEGIN
BLOCK "StringFileInfo"
BEGIN
BLOCK "040904E4"
BEGIN
VALUE "FileDescription", "Symphony Auto Update Service"
VALUE "ProductName", "Symphony Auto Update Service"
VALUE "ProductVersion", VER_STRING
VALUE "LegalCopyright", "Copyright 2021 Symphony OSS"
END
END
BLOCK "VarFileInfo"
BEGIN
VALUE "Translation", 0x409, 1252
END
END

View File

@ -1,146 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup Label="ProjectConfigurations">
<ProjectConfiguration Include="Debug|Win32">
<Configuration>Debug</Configuration>
<Platform>Win32</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Release|Win32">
<Configuration>Release</Configuration>
<Platform>Win32</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Debug|x64">
<Configuration>Debug</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Release|x64">
<Configuration>Release</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
</ItemGroup>
<PropertyGroup Label="Globals">
<VCProjectVersion>15.0</VCProjectVersion>
<ProjectGuid>{7E157BD0-86F0-4325-848C-BA1C87D9C99C}</ProjectGuid>
<RootNamespace>autoupdateservice</RootNamespace>
<WindowsTargetPlatformVersion>10.0</WindowsTargetPlatformVersion>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'" Label="Configuration">
<ConfigurationType>Application</ConfigurationType>
<UseDebugLibraries>true</UseDebugLibraries>
<PlatformToolset>v142</PlatformToolset>
<CharacterSet>MultiByte</CharacterSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'" Label="Configuration">
<ConfigurationType>Application</ConfigurationType>
<UseDebugLibraries>false</UseDebugLibraries>
<PlatformToolset>v142</PlatformToolset>
<WholeProgramOptimization>true</WholeProgramOptimization>
<CharacterSet>MultiByte</CharacterSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'" Label="Configuration">
<ConfigurationType>Application</ConfigurationType>
<UseDebugLibraries>true</UseDebugLibraries>
<PlatformToolset>v142</PlatformToolset>
<CharacterSet>MultiByte</CharacterSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'" Label="Configuration">
<ConfigurationType>Application</ConfigurationType>
<UseDebugLibraries>false</UseDebugLibraries>
<PlatformToolset>v142</PlatformToolset>
<WholeProgramOptimization>true</WholeProgramOptimization>
<CharacterSet>MultiByte</CharacterSet>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
<ImportGroup Label="ExtensionSettings">
</ImportGroup>
<ImportGroup Label="Shared">
</ImportGroup>
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<PropertyGroup Label="UserMacros" />
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
<IntDir>$(Platform)\$(Configuration)\auto_update_service\</IntDir>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
<IntDir>$(Platform)\$(Configuration)\auto_update_service\</IntDir>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<IntDir>$(Platform)\$(Configuration)\auto_update_service\</IntDir>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
<IntDir>$(Platform)\$(Configuration)\auto_update_service\</IntDir>
</PropertyGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
<ClCompile>
<WarningLevel>Level3</WarningLevel>
<Optimization>Disabled</Optimization>
<SDLCheck>true</SDLCheck>
<ConformanceMode>true</ConformanceMode>
</ClCompile>
<Link>
<SubSystem>Console</SubSystem>
</Link>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<ClCompile>
<WarningLevel>Level3</WarningLevel>
<Optimization>Disabled</Optimization>
<SDLCheck>true</SDLCheck>
<ConformanceMode>true</ConformanceMode>
</ClCompile>
<Link>
<SubSystem>Console</SubSystem>
</Link>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
<ClCompile>
<WarningLevel>Level3</WarningLevel>
<Optimization>MaxSpeed</Optimization>
<FunctionLevelLinking>true</FunctionLevelLinking>
<IntrinsicFunctions>true</IntrinsicFunctions>
<SDLCheck>true</SDLCheck>
<ConformanceMode>true</ConformanceMode>
</ClCompile>
<Link>
<SubSystem>Console</SubSystem>
<EnableCOMDATFolding>true</EnableCOMDATFolding>
<OptimizeReferences>true</OptimizeReferences>
</Link>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
<ClCompile>
<WarningLevel>Level3</WarningLevel>
<Optimization>MaxSpeed</Optimization>
<FunctionLevelLinking>true</FunctionLevelLinking>
<IntrinsicFunctions>true</IntrinsicFunctions>
<SDLCheck>true</SDLCheck>
<ConformanceMode>true</ConformanceMode>
</ClCompile>
<Link>
<SubSystem>Console</SubSystem>
<EnableCOMDATFolding>true</EnableCOMDATFolding>
<OptimizeReferences>true</OptimizeReferences>
</Link>
</ItemDefinitionGroup>
<ItemGroup>
<ClCompile Include="auto_update_service.c" />
</ItemGroup>
<ItemGroup>
<ClInclude Include="ipc.h" />
<ClInclude Include="service.h" />
</ItemGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
<ImportGroup Label="ExtensionTargets">
</ImportGroup>
</Project>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 302 KiB

View File

@ -1,821 +0,0 @@
#ifndef ipc_h
#define ipc_h
#include <stdbool.h>
#include <stddef.h>
#define IPC_MESSAGE_MAX_LENGTH 4096
// client
typedef struct ipc_client_t ipc_client_t;
ipc_client_t* ipc_client_connect( char const* pipe_name );
void ipc_client_disconnect( ipc_client_t* connection );
typedef enum ipc_receive_status_t {
IPC_RECEIVE_STATUS_DONE,
IPC_RECEIVE_STATUS_MORE_DATA,
IPC_RECEIVE_STATUS_ERROR,
} ipc_receive_status_t;
ipc_receive_status_t ipc_client_receive( ipc_client_t* connection, char* output, int output_size, int* received_size );
bool ipc_client_send( ipc_client_t* connection, char const* message );
// server
typedef struct ipc_server_t ipc_server_t;
typedef void (*ipc_request_handler_t)( char const* request, void* user_data, char* response, size_t capacity );
ipc_server_t* ipc_server_start( char const* pipe_name, ipc_request_handler_t request_handler, void* user_data );
void ipc_server_stop( ipc_server_t* server );
#endif /* ipc_h */
#ifdef IPC_IMPLEMENTATION
#undef IPC_IMPLEMENTATION
#include <stdio.h>
#include <windows.h>
#include <aclapi.h>
#pragma comment(lib, "advapi32.lib")
#ifndef IPC_LOG_INFO
#define IPC_LOG_INFO printf( "\n" ), printf
#endif
#ifndef IPC_LOG_ERROR
#define IPC_LOG_ERROR printf( "\n" ), printf
#endif
#ifndef IPC_LOG_LAST_ERROR
#define IPC_LOG_LAST_ERROR printf( "\nLastError=%d : ", GetLastError() ), printf
#endif
// Named pipes are on the form "\\.\pipe\name" but we don't want the user to have
// to specify all that, so we expand what they pass in from "name" to "\\.\pipe\name"
bool expand_pipe_name( char const* pipe_name, char* buffer, size_t capacity ) {
int result = snprintf( buffer, capacity, "\\\\.\\pipe\\%s", pipe_name );
return result >= 0 && result < (int) capacity;
}
// Returns true if a pipe of the specified name exists, false if none exists
bool pipe_exists( const char* pipe_name ) {
IPC_LOG_INFO( "Checking if pipe exists: %s", pipe_name );
WIN32_FIND_DATAA data;
memset( &data, 0, sizeof( data ) );
HANDLE hfind = FindFirstFileA( "\\\\.\\pipe\\*", &data );
if( hfind != INVALID_HANDLE_VALUE ) {
do {
char const* filename = data.cFileName;
if( _stricmp( filename, pipe_name ) == 0 ) {
FindClose( hfind );
IPC_LOG_INFO( "Pipe found: %s", filename );
return true;
}
} while( FindNextFileA( hfind, &data ) );
FindClose( hfind );
}
IPC_LOG_ERROR( "Pipe not found" );
return false;
}
// This holds data related to a single client instance
struct ipc_client_t {
HANDLE pipe; // The named pipe to communicate over
};
// Establishes a connection to the specified named pipe
// Returns NULL if a connection could not be established
ipc_client_t* ipc_client_connect( char const* pipe_name ) {
IPC_LOG_INFO( "Connecting to named pipe: %s", pipe_name );
// Make sure a pipe with the specified name exists
if( !pipe_exists( pipe_name ) ) {
// Retry once if pipe was not found - this would be very rare, but will make it more robust
IPC_LOG_INFO( "Pipe was not found, waiting a little and trying again" );
Sleep( 1000 );
if( !pipe_exists( pipe_name ) ) {
IPC_LOG_INFO( "Pipe was still not found after waiting" );
IPC_LOG_ERROR( "Named pipe does not exist" );
return NULL;
}
}
// Expand the pipe name to the valid form eg. "\\.\pipe\name"
char expanded_pipe_name[ MAX_PATH ];
IPC_LOG_INFO( "Expanding to fully qualified pipe name: %s", pipe_name );
if( !expand_pipe_name( pipe_name, expanded_pipe_name, sizeof( expanded_pipe_name ) ) ) {
IPC_LOG_ERROR( "Pipe name too long" );
return NULL;
}
IPC_LOG_INFO( "Expanded pipe name: %s", expanded_pipe_name );
// A named pipe has a maximum number of connections. When a client disconnect, it
// can take a while for the disconnect to register on the server side, so we need
// to handle the case where the pipe is busy. In practice, this should be rare,
// but for robustness we handle it anyway.
HANDLE pipe = NULL;
for( ; ; ) { // This loop will typically not run more than two iterations, due to multiple exit points
IPC_LOG_INFO( "Try to create connection" );
pipe = CreateFileA(
expanded_pipe_name, // pipe name
GENERIC_READ | // read and write access
GENERIC_WRITE,
0, // no sharing
NULL, // default security attributes
OPEN_EXISTING, // opens existing pipe
0, // default attributes
NULL ); // no template file
// Break if the pipe handle is valid - a connection is now established
if( pipe != INVALID_HANDLE_VALUE ) {
IPC_LOG_INFO( "Connection attempt succeeded" );
break;
}
// Retry once if pipe was not found. Very rare that this would happen, but we're going for stability
if( GetLastError() == ERROR_FILE_NOT_FOUND ) {
IPC_LOG_INFO( "The pipe was not found, which was unexpected at this point, so we wait a little and try again" );
Sleep( 1000 );
pipe = CreateFileA(
expanded_pipe_name, // pipe name
GENERIC_READ | // read and write access
GENERIC_WRITE,
0, // no sharing
NULL, // default security attributes
OPEN_EXISTING, // opens existing pipe
0, // default attributes
NULL ); // no template file
// Break if the pipe handle is valid - a connection is now established
if( pipe != INVALID_HANDLE_VALUE ) {
IPC_LOG_INFO( "Second connection attempt succeeded" );
break;
}
}
// If we get an error other than ERROR_PIPE_BUSY, we fail to establish a connection.
// In the case of ERROR_PIPE_BUSY we will wait for the pipe not to be busy (see below)
if( GetLastError() != ERROR_PIPE_BUSY ) {
IPC_LOG_LAST_ERROR( "Could not open pipe: " );
return NULL;
}
// All pipe instances are busy, so wait for 20 seconds.
IPC_LOG_INFO( "All pipe instances are busy, so we wait for 20 seconds and then try again" );
if( !WaitNamedPipeA( expanded_pipe_name, 20000 ) ) {
IPC_LOG_INFO( "Wait failed" );
// In the specific case of getting an ERROR_FILE_NOT_FOUND, we try doing the
// wait one more time. The reason this would happen is if the server was just restarting
// at the start of the call to ipc_client_connect, and thus the check if the pipe exist
// passed, but when we got to the wait, the pipe was closed down and not yet started up
// again. Waiting briefly and then trying again will ensure that we handle this rare case
// of the server being restarted, but it will be very very rare.
if( GetLastError() == ERROR_FILE_NOT_FOUND ) {
// retry once just in case pipe was not created yet
IPC_LOG_INFO( "Try the wait again after a short pause, in case it was just being created" );
Sleep(1000);
if( !WaitNamedPipeA( expanded_pipe_name, 20000 ) ) {
IPC_LOG_LAST_ERROR( "Could not open pipe on second attempt: 20 second wait timed out: " );
return NULL;
}
} else {
IPC_LOG_LAST_ERROR( "Could not open pipe: 20 second wait timed out: " );
return NULL;
}
}
}
// A fully working connection has been set up, return it to the caller
IPC_LOG_INFO( "Connection successful" );
ipc_client_t* connection = (ipc_client_t*) malloc( sizeof( ipc_client_t ) );
connection->pipe = pipe;
return connection;
}
// Disconnect the client from the server, and release the resources used by it
// This will allow the server to eventually recycle and reuse that connection slot,
// but in some cases it can take a brief period of time for that to happen
void ipc_client_disconnect( ipc_client_t* connection ) {
IPC_LOG_INFO( "Disconnecting client" );
FlushFileBuffers( connection->pipe );
DisconnectNamedPipe( connection->pipe );
CloseHandle( connection->pipe );
free( connection );
IPC_LOG_INFO( "Disconnection complete" );
}
// Wait for data to be available on the named pipe, and once it is, read it into the
// provided buffer. Returns a status enum for success or failure, or for the case
// where more data was cued up on the server side than could be received in one call,
// in which case the ipc_client_receive function should be called again to complete
// the retrieval of the message. The function will wait indefinitely, until either
// a message is available, or the pipe is closed.
// TODO: consider a timeout for the wait, to allow for more robust client implementations
ipc_receive_status_t ipc_client_receive( ipc_client_t* connection, char* output, int output_size, int* received_size ) {
IPC_LOG_INFO( "Reading data" );
DWORD size_read = 0;
BOOL success = ReadFile(
connection->pipe, // pipe handle
output, // buffer to receive reply
output_size, // size of buffer
&size_read, // number of bytes read
NULL ); // not overlapped
IPC_LOG_INFO( "Read returned %s", success ? "true" : "false" );
if( !success && GetLastError() != ERROR_MORE_DATA ) {
IPC_LOG_LAST_ERROR( "ReadFile from pipe failed: " );
return IPC_RECEIVE_STATUS_ERROR;
}
IPC_LOG_INFO( "Data size received: %u", size_read );
if( received_size ) {
*received_size = size_read;
}
if( success ) {
IPC_LOG_INFO( "Read done" );
return IPC_RECEIVE_STATUS_DONE;
} else {
IPC_LOG_INFO( "More data to be read" );
return IPC_RECEIVE_STATUS_MORE_DATA;
}
}
// Sends the specified message (as a zero-terminated string) to the server
// Will wait for the server to receive the message, and how long that wait
// is will depend on if the server is busy when the message is sent.
// TODO: consider a timeout for the wait, to allow for more robust client implementations
bool ipc_client_send( ipc_client_t* connection, char const* message ) {
// Send a message to the pipe server.
IPC_LOG_INFO( "Sending data" );
DWORD written = 0;
BOOL success = WriteFile(
connection->pipe, // pipe handle
message, // message
(DWORD) strlen( message ) + 1, // message length
&written, // bytes written
NULL ); // not overlapped
IPC_LOG_INFO( "Write returned %s", success ? "true" : "false" );
if( !success ) {
IPC_LOG_LAST_ERROR( "WriteFile to pipe failed: " );
return false;
}
return true;
}
// This holds the data for a single server-side client thread
typedef struct ipc_client_thread_t {
BOOL recycle; // When a client disconnect, this flag is set to TRUE so the slot can be reused
ipc_request_handler_t request_handler; // When a request is recieved from a client, the server calls this handler
void* user_data; // When the request_handler is called, this user_data field is passed along with it
int exit_flag; // Set by the server to signal that the client thread should exit
HANDLE thread; // Handle to this client thread, used by server to wait for it to exit (on server shutdown)
HANDLE pipe; // The named pipe instance allocated to this client
OVERLAPPED io; // We are using non-blocking I/O so the server can cancel pending read/write operations on shutdown
} ipc_client_thread_t;
// Typically, we should only ever have one connections, so this is probably overkill, but
// it doesn't hurt
#define MAX_CLIENT_CONNECTIONS 32
// This holds the data for an ipc server instance
struct ipc_server_t {
char expanded_pipe_name[ MAX_PATH ]; // Holds the result of expanding from the "name" form to the "\\.\pipe\name" form
HANDLE thread; // Handle to the main server thread, used to wait for thread exit on server shutdown
HANDLE thread_started_event; // When the main server thread is started, ipc_server_start needs to wait until it is ready to accept connections before returning to the caller
HANDLE pipe; // The server pipe instance currently used to listen for connections, will be handed to client thread when connection is made
OVERLAPPED io; // We are using non-blocking I/O so the server can cancel pending ConnectNamedPipe operations on shutdown
int exit_flag; // Set by the ipc_server_stop to signal that the main server thread should exit
ipc_request_handler_t request_handler; // When a request is recieved from a client, the server calls this handler
void* user_data; // When the request_handler is called, this user_data field is passed along with it
ipc_client_thread_t client_threads[ MAX_CLIENT_CONNECTIONS ]; // Array of client instances, an instance is only in use if its `recycle` flag is FALSE
int client_threads_count; // Number of slots used on the `client_threads` array (but a slot may or may not be in use depending on its `recycle` flag)
};
// When a client connects to the server, the server creates a new thread to handle that connection,
// and this is the function running on that thread. It basically just sits in a loop, doing a Read
// from the pipe and waiting until it gets a message. Then it will call the user supplied request
// handler, and then it does a Write on the pipe to send the response it got from the request handler
// to the pipe.
DWORD WINAPI ipc_client_thread( LPVOID param ) {
IPC_LOG_INFO( "[%u] Client thread started", GetCurrentThreadId() );
ipc_client_thread_t* context = (ipc_client_thread_t*) param;
// Create the event used to wait for Read/Write operations to complete
HANDLE io_event = CreateEvent(
NULL, // default security attributes
TRUE, // manual-reset event
FALSE, // initial state is nonsignaled
NULL // object name
);
// Main request-response loop. Will run until exit requested or an eror occurs
IPC_LOG_INFO( "[%u] Enter client thread main loop", GetCurrentThreadId() );
while( !context->exit_flag ) {
// Read loop, keeps trying to read until data arrives or an error occurs (including a shutdown
// cancelling the read operation)
char request[ IPC_MESSAGE_MAX_LENGTH ]; // buffer to hold incoming data
DWORD bytes_read = 0;
BOOL success = FALSE;
bool read_pending = true;
IPC_LOG_INFO( "[%u] Starting read loop", GetCurrentThreadId() );
while( read_pending ) {
// Set up non-blocking I/O
memset( &context->io, 0, sizeof( context->io ) );
ResetEvent( io_event );
context->io.hEvent = io_event;
// Read client requests from the pipe in a non-blocking call
IPC_LOG_INFO( "[%u] Reading from pipe", GetCurrentThreadId() );
success = ReadFile(
context->pipe, // handle to pipe
request, // buffer to receive data
IPC_MESSAGE_MAX_LENGTH, // size of buffer
&bytes_read, // number of bytes read
&context->io ); // overlapped I/O
IPC_LOG_INFO( "[%u] Read returned: %s", GetCurrentThreadId(), success ? "true" : "false" );
// Check if the Read operation is in progress (ReadFile returns FALSE and the error is ERROR_IO_PENDING )
if( !success && GetLastError() == ERROR_IO_PENDING ) {
IPC_LOG_INFO( "[%u] Pipe is in IO_PENDING state, read is in progress", GetCurrentThreadId() );
// Wait for the event to be triggered, but timeout after half a second and re-issue the Read
// This is so the re-issued Read can detect if the pipe have been closed, and thus exit the thread
IPC_LOG_INFO( "[%u] Wait for read to complete", GetCurrentThreadId() );
if( WaitForSingleObject( io_event, 500 ) == WAIT_TIMEOUT ) {
IPC_LOG_INFO( "[%u] Read timed out, try again", GetCurrentThreadId() );
CancelIoEx( context->pipe, &context->io );
continue; // Make another Read call
}
// The wait did not timeout, so the Read operation should now be completed (or failed)
IPC_LOG_INFO( "[%u] Read completed, checking result", GetCurrentThreadId() );
success = GetOverlappedResult(
context->pipe, // handle to pipe
&context->io, // OVERLAPPED structure
&bytes_read, // bytes transferred
FALSE ); // don't wait
IPC_LOG_INFO( "[%u] Read was %s", GetCurrentThreadId(), success ? "successful" : "unsuccessful" );
IPC_LOG_INFO( "[%u] Bytes read %u", GetCurrentThreadId(), bytes_read );
}
// The read have completed (successfully or not) so exit the read loop
read_pending = false;
}
IPC_LOG_INFO( "[%u] Finished read loop, result was %s", GetCurrentThreadId(), success ? "success" : "failure" );
// If the Read was unsuccessful (or read no data), log the error and exit the thread
// TODO: consider if there are better ways to deal with the error. There might not be,
// but then the user-code calling client send/receive might need some robust retry code
if( !success || bytes_read == 0 ) {
if( GetLastError() == ERROR_BROKEN_PIPE ) {
IPC_LOG_INFO( "[%u] Client disconnected", GetCurrentThreadId() );
} else {
IPC_LOG_LAST_ERROR( "ReadFile failed: " );
}
break;
}
// Check if a server shutdown have requested this thread to be terminated, and exit if that's the case
if( context->exit_flag ) {
IPC_LOG_INFO( "[%u] Server shutdown requested, terminating thread", GetCurrentThreadId() );
break;
}
IPC_LOG_INFO( "[%u] Incoming message: %s", GetCurrentThreadId(), request );
// Process the incoming message by calling the user-supplied request handler function
char response[ IPC_MESSAGE_MAX_LENGTH ];
memset( response, 0, sizeof( response ) );
IPC_LOG_INFO( "[%u] Processing message", GetCurrentThreadId() );
context->request_handler( request, context->user_data, response, sizeof( response ) );
response[ sizeof( response ) - 1 ] = '\0'; // Force zero termination (truncate string)
IPC_LOG_INFO( "[%u] Outgoing response: \"%.32s%s\"", GetCurrentThreadId(), response, strlen( response ) > 32 ? "..." : "" );
DWORD response_length = (DWORD)strlen( response ) + 1;
IPC_LOG_INFO( "[%u] Response length: %u", GetCurrentThreadId(), response_length );
// Write the reply to the pipe
DWORD bytes_written = 0;
IPC_LOG_INFO( "[%u] Sending response", GetCurrentThreadId() );
success = WriteFile(
context->pipe, // handle to pipe
response, // buffer to write from
response_length, // number of bytes to write
&bytes_written, // number of bytes written
&context->io ); // overlapped I/O
IPC_LOG_INFO( "[%u] Write returned: %s", GetCurrentThreadId(), success ? "true" : "false" );
// If the write operation is in progress, we wait until it is done, or aborted due to server shutdown
if( success || GetLastError() == ERROR_IO_PENDING ) {
IPC_LOG_INFO( "[%u] Pipe is in IO_PENDING state, write is in progress", GetCurrentThreadId() );
success = GetOverlappedResult(
context->pipe, // handle to pipe
&context->io, // OVERLAPPED structure
&bytes_written, // bytes transferred
TRUE ); // wait
IPC_LOG_INFO( "[%u] Write was %s", GetCurrentThreadId(), success ? "successful" : "unsuccessful" );
IPC_LOG_INFO( "[%u] Bytes written %u", GetCurrentThreadId(), bytes_written );
}
// If the Write was unsuccessful (or didn't manage to write the whole buffer), log the error and exit the thread
if( !success || bytes_written != response_length ) {
IPC_LOG_LAST_ERROR( "WriteFile failed: " );
break;
}
}
IPC_LOG_INFO( "[%u] Finished client thread main loop", GetCurrentThreadId() );
// Signal that a disconnect has happened
context->request_handler( NULL, context->user_data, NULL, 0 );
// Flush the pipe to allow the client to read the pipe's contents
// before disconnecting. Then disconnect the pipe, and close the
// handle to this pipe instance.
CloseHandle( io_event );
FlushFileBuffers( context->pipe );
DisconnectNamedPipe( context->pipe );
CloseHandle( context->pipe );
// Mark this client slot for recycling for new connections
context->pipe = INVALID_HANDLE_VALUE;
context->recycle = TRUE;
IPC_LOG_INFO( "[%u] Client thread terminated", GetCurrentThreadId() );
return EXIT_SUCCESS;
}
// When the `ipc_server_start` is called, it creates this thread which sits in a loop
// and listens for new client connections, until exit is requested by a call to
// `ipc_server_stop`. When a new connection is made, it will start another thread to
// handle the I/O for that specific client. Then it will open a new listening pipe
// instance for further connections.
DWORD WINAPI ipc_server_thread( LPVOID param ) {
IPC_LOG_INFO( "[%u] Server thread started", GetCurrentThreadId() );
ipc_server_t* server = (ipc_server_t*) param;
// Create security attribs, we need this so the server can run in session 0
// while client runs as a normal user session
SID_IDENTIFIER_AUTHORITY auth = { SECURITY_WORLD_SID_AUTHORITY };
PSID sid;
if( !AllocateAndInitializeSid( &auth, 1, SECURITY_WORLD_RID, 0, 0, 0, 0, 0, 0, 0, &sid ) ) {
IPC_LOG_LAST_ERROR( "AllocateAndInitializeSid failed: " );
IPC_LOG_INFO( "[%u] Server thread terminated", GetCurrentThreadId() );
return EXIT_FAILURE;
}
EXPLICIT_ACCESS access = { 0 };
access.grfAccessPermissions = FILE_ALL_ACCESS;
access.grfAccessMode = SET_ACCESS;
access.grfInheritance = NO_INHERITANCE;
access.Trustee.TrusteeForm = TRUSTEE_IS_SID;
access.Trustee.TrusteeType = TRUSTEE_IS_WELL_KNOWN_GROUP;
access.Trustee.ptstrName = (LPTSTR)sid;
PACL acl;
if( SetEntriesInAcl(1, &access, NULL, &acl) != ERROR_SUCCESS ) {
IPC_LOG_LAST_ERROR( "SetEntriesInAcl failed: " );
FreeSid(sid);
IPC_LOG_INFO( "[%u] Server thread terminated", GetCurrentThreadId() );
return EXIT_FAILURE;
}
PSECURITY_DESCRIPTOR sd = (PSECURITY_DESCRIPTOR)LocalAlloc( LPTR, SECURITY_DESCRIPTOR_MIN_LENGTH );
if( !sd ) {
IPC_LOG_ERROR( "LocalAlloc failed" );
FreeSid( sid );
IPC_LOG_ERROR( "[%u] Server thread terminated", GetCurrentThreadId() );
return EXIT_FAILURE;
}
if( !InitializeSecurityDescriptor( sd, SECURITY_DESCRIPTOR_REVISION ) ) {
IPC_LOG_LAST_ERROR( "InitializeSecurityDescriptor failed: " );
LocalFree(sd);
FreeSid(sid);
IPC_LOG_ERROR( "[%u] Server thread terminated", GetCurrentThreadId() );
return EXIT_FAILURE;
}
if( !SetSecurityDescriptorDacl( sd, TRUE, acl, FALSE ) ) {
IPC_LOG_LAST_ERROR( "SetSecurityDescriptorDacl failed: " );
LocalFree( sd );
FreeSid( sid );
IPC_LOG_ERROR( "[%u] Server thread terminated", GetCurrentThreadId() );
return EXIT_FAILURE;
}
SECURITY_ATTRIBUTES attribs;
attribs.nLength = sizeof( SECURITY_ATTRIBUTES );
attribs.lpSecurityDescriptor = sd;
attribs.bInheritHandle = -1;
// Create the event used to wait for ConnectNamedPipe operations to complete
HANDLE io_event = CreateEvent(
NULL, // default security attributes
TRUE, // manual-reset event
FALSE, // initial state is nonsignaled
NULL // object name
);
// The main loop creates an instance of the named pipe and then waits for a client
// to connect to it. When the client connects, a thread is created to handle
// communications with that client, and this loop is free to wait for the next client
// connect request
IPC_LOG_INFO( "[%u] Enter server thread main loop", GetCurrentThreadId() );
bool event_raised = false; // We make sure to only raise the server-thread-is-ready event once
while( !server->exit_flag ) {
// Create a pipe instance to listen for connections
IPC_LOG_INFO( "[%u] Creating named pipe listening for connections", GetCurrentThreadId() );
server->pipe = CreateNamedPipeA(
server->expanded_pipe_name,// pipe name
PIPE_ACCESS_DUPLEX | // read/write access
FILE_FLAG_OVERLAPPED, // we use async I/O so that we can cancel ConnectNamedPipe operations
PIPE_TYPE_MESSAGE | // message type pipe
PIPE_READMODE_MESSAGE | // message-read mode
PIPE_WAIT, // blocking mode
MAX_CLIENT_CONNECTIONS, // max. instances
IPC_MESSAGE_MAX_LENGTH, // output buffer size
IPC_MESSAGE_MAX_LENGTH, // input buffer size
0, // client time-out
&attribs ); // default security attribute
// If we failed to create the pipe, we log the error and exit
// TODO: Should we handle this some other way? perhaps report the error back to user
if( server->pipe == INVALID_HANDLE_VALUE ) {
// If the failure was due to pipe busy, try again in a bit
if( GetLastError() == ERROR_PIPE_BUSY ) {
IPC_LOG_LAST_ERROR( "CreateNamedPipe failed: " );
IPC_LOG_INFO( "[%u] Pipe was busy, waiting a bit then trying once more.", GetCurrentThreadId() );
Sleep(1000);
server->pipe = CreateNamedPipeA(
server->expanded_pipe_name,// pipe name
PIPE_ACCESS_DUPLEX | // read/write access
FILE_FLAG_OVERLAPPED, // we use async I/O so that we can cancel ConnectNamedPipe operations
PIPE_TYPE_MESSAGE | // message type pipe
PIPE_READMODE_MESSAGE | // message-read mode
PIPE_WAIT, // blocking mode
MAX_CLIENT_CONNECTIONS, // max. instances
IPC_MESSAGE_MAX_LENGTH, // output buffer size
IPC_MESSAGE_MAX_LENGTH, // input buffer size
0, // client time-out
&attribs ); // default security attribute
}
if( server->pipe == INVALID_HANDLE_VALUE ) {
IPC_LOG_LAST_ERROR( "CreateNamedPipe failed: " );
LocalFree( acl );
LocalFree( sd );
FreeSid( sid );
IPC_LOG_ERROR( "[%u] Server thread terminated", GetCurrentThreadId() );
return EXIT_FAILURE;
}
}
// Signal to `ipc_server_start` that the server thread is now fully up and
// running and accepting connections
if( !event_raised ) {
IPC_LOG_INFO( "[%u] Signaling to ipc_server_start that init is complete and we are now listening", GetCurrentThreadId() );
SetEvent( server->thread_started_event );
event_raised = true; // Make sure we don't signal the event again
}
// Wait for the client to connect, using async I/O operation, so ConnectNamedPipe returns immediately
memset( &server->io, 0, sizeof( server->io ) );
server->io.hEvent = io_event;
IPC_LOG_INFO( "[%u] Wait for client to connect", GetCurrentThreadId() );
ConnectNamedPipe( server->pipe, &server->io );
if( GetLastError() == ERROR_IO_PENDING ) {
for( ; ; ) {
if( WaitForSingleObject( server->io.hEvent, 100 ) == WAIT_OBJECT_0 ) {
IPC_LOG_INFO( "[%u] Connection completed", GetCurrentThreadId() );
break;
}
if( server->exit_flag ) {
IPC_LOG_INFO( "[%u] Server shutdown requested", GetCurrentThreadId() );
break;
}
}
} else if( GetLastError() != ERROR_PIPE_CONNECTED ) {
if( GetLastError() != ERROR_OPERATION_ABORTED || server->exit_flag == 0 ) {
// The client could not connect, so close the pipe.
IPC_LOG_LAST_ERROR( "Connection failed: " );
break;
}
}
// Check if a server shutdown have requested this thread to be terminated, and exit if that's the case
if( server->exit_flag ) {
IPC_LOG_INFO( "[%u] Server shutdown requested, breaking main loop", GetCurrentThreadId() );
break;
}
IPC_LOG_INFO( "[%u] Client connected, setting up client thread to handle it", GetCurrentThreadId() );
// Find a free client slot to recycle for this new client connection
ipc_client_thread_t* context = NULL;
for( int i = 0; i < server->client_threads_count; ++i ) {
if( server->client_threads[ i ].recycle ) {
context = &server->client_threads[ i ];
}
}
// If there is no free slot to recycle, use a new slot if available
if( !context ) {
IPC_LOG_INFO( "[%u] No free slot to recycle, allocating a new slot", GetCurrentThreadId() );
if( server->client_threads_count < MAX_CLIENT_CONNECTIONS ) {
context = &server->client_threads[ server->client_threads_count++ ];
} else {
IPC_LOG_ERROR( "[%u] Maximum number of connectsions reached - client should have been held in wait state, and this error should never have been triggered", GetCurrentThreadId() );
// If we already reached the maximum number of connections, we have to bail out
// This shouldn't really happen though, as the client should be kept in the wait
// state by the pipe itself which is specified to accept only the same number of
// connections
// TODO: Perhaps better to just silently refuse the connection but stay alive?
// or maybe kill the connection that has been idle for the longest time?
IPC_LOG_INFO( "[%u] Too many connections", GetCurrentThreadId() );
LocalFree( acl );
LocalFree( sd );
FreeSid( sid );
if( server->pipe != INVALID_HANDLE_VALUE ) {
CloseHandle( server->pipe );
server->pipe = INVALID_HANDLE_VALUE;
}
CloseHandle( io_event );
IPC_LOG_ERROR( "[%u] Server thread terminated", GetCurrentThreadId() );
return EXIT_FAILURE;
}
}
// Initialize the client slot
IPC_LOG_INFO( "[%u] Initializing client slot", GetCurrentThreadId() );
memset( context, 0, sizeof( *context ) );
context->request_handler = server->request_handler;
context->user_data = server->user_data;
context->pipe = server->pipe;
// We are handing the pipe over to the client thread, but will be creating a new one on
// the next iteration through the loop
server->pipe = INVALID_HANDLE_VALUE;
// Create a dedicated thread to handle this connection
IPC_LOG_INFO( "[%u] Creating the client thread", GetCurrentThreadId() );
context->thread = CreateThread(
NULL, // no security attribute
0, // default stack size
ipc_client_thread, // thread proc
(LPVOID) context, // thread parameter
0, // not suspended
NULL ); // returns thread ID
// If we failed to create thread, something's gone very wrong, so we need to bail
if( context->thread == NULL ) {
IPC_LOG_LAST_ERROR( "CreateThread failed: " );
LocalFree( acl );
LocalFree( sd );
FreeSid( sid );
if( server->pipe != INVALID_HANDLE_VALUE ) {
CloseHandle( server->pipe );
server->pipe = INVALID_HANDLE_VALUE;
}
CloseHandle( io_event );
IPC_LOG_ERROR( "[%u] Server thread terminated", GetCurrentThreadId() );
return EXIT_FAILURE;
}
}
IPC_LOG_INFO( "[%u] Finished server thread main loop", GetCurrentThreadId() );
// Cleanup thread resources before we exit
LocalFree( acl );
LocalFree( sd );
FreeSid( sid );
if( server->pipe != INVALID_HANDLE_VALUE ) {
CloseHandle( server->pipe );
server->pipe = INVALID_HANDLE_VALUE;
}
CloseHandle( io_event );
IPC_LOG_INFO( "[%u] Server thread terminated by request", GetCurrentThreadId() );
return EXIT_SUCCESS;
}
// Starts a named pipe server with the specified pipe name, and starts listening for
// client connections on a separate thread, so will return immediately. The server
// thread will keep listening for connections until `ipc_server_stop` is called.
ipc_server_t* ipc_server_start( char const* pipe_name, ipc_request_handler_t request_handler, void* user_data ) {
IPC_LOG_INFO( "Starting named pipe server: %s", pipe_name );
// Allocate the server instance and initialize it
ipc_server_t* server = (ipc_server_t*) malloc( sizeof( ipc_server_t ) );
memset( server, 0, sizeof( ipc_server_t ) );
server->pipe = INVALID_HANDLE_VALUE;
server->request_handler = request_handler;
server->user_data = user_data;
// Expand the pipe name to the valid form eg. "\\.\pipe\name"
IPC_LOG_INFO( "Expanding to fully qualified pipe name: %s", pipe_name );
if( !expand_pipe_name( pipe_name, server->expanded_pipe_name, sizeof( server->expanded_pipe_name ) ) ) {
IPC_LOG_ERROR( "Pipe name too long" );
free( server );
return NULL;
}
IPC_LOG_INFO( "Expanded pipe name: %s", server->expanded_pipe_name );
// Create the event used by the server thread to signal that it is up and running and accepting connections
server->thread_started_event = CreateEvent(
NULL, // default security attributes
TRUE, // manual-reset event
FALSE, // initial state is nonsignaled
NULL // object name
);
// Start the server thread which accepts connections and starts dedicated client threads for each new connection
IPC_LOG_INFO( "Starting server thread" );
server->thread = CreateThread(
NULL, // default security attributes
0, // use default stack size
ipc_server_thread, // thread function name
server, // argument to thread function
0, // use default creation flags
NULL ); // returns the thread identifier
// If thread creation failed, return error
if( server->thread == NULL ) {
IPC_LOG_LAST_ERROR( "Failed to create server thread: " );
CloseHandle( server->thread_started_event );
free( server );
return NULL;
}
// Wait for the server thread to be up and running and accepting connections
IPC_LOG_INFO( "Waiting for server thread to be initialized" );
if( WaitForSingleObject( server->thread_started_event, 10000 ) != WAIT_OBJECT_0 ) {
// If it takes more than 10 seconds for the server thread to start up, something
// has gone very wrong so we abort and return an error
IPC_LOG_LAST_ERROR( "Timeout waiting for client thread to start: " );
CloseHandle( server->thread_started_event );
TerminateThread( server->thread, EXIT_FAILURE );
free( server );
return NULL;
}
IPC_LOG_INFO( "Server up and running" );
// Return the fully set up and ready server instance
return server;
}
// Signals the server thread to stop, cancels all pending I/O operations on all
// client threads, and release the resources used by the server
void ipc_server_stop( ipc_server_t* server ) {
IPC_LOG_INFO( "Stopping named pipe server" );
server->exit_flag = 1; // Signal server thread top stop
if( server->pipe != INVALID_HANDLE_VALUE ) {
CancelIoEx( server->pipe, &server->io ); // Cancel pending ConnectNamedPipe operatios, if any
}
IPC_LOG_INFO( "Waiting for server thread to exit" );
WaitForSingleObject( server->thread, INFINITE ); // Wait for server thread to exit
IPC_LOG_INFO( "Server thread stopped" );
// Loop over all clients and terminate each one
IPC_LOG_INFO( "Terminating client connections" );
for( int i = 0; i < server->client_threads_count; ++i ) {
ipc_client_thread_t* client = &server->client_threads[ i ];
if( !client->recycle ) { // A slot is only valid if `recycle` is FALSE
client->exit_flag = 1; // Tell client thread to exit
CancelIoEx( client->pipe, &client->io ); // Cancel any pending Read/Write operation
WaitForSingleObject( client->thread, INFINITE ); // Wait for client thread to exit
}
}
// Free server resources
CloseHandle( server->thread_started_event );
free( server );
IPC_LOG_INFO( "Server stopped and terminated" );
}
#endif /* IPC_IMPLEMENTATION */

View File

@ -1,10 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
<security>
<requestedPrivileges>
<requestedExecutionLevel level="asInvoker"/>
</requestedPrivileges>
</security>
</trustInfo>
</assembly>

View File

@ -1,10 +0,0 @@
{
"name": "auto_update",
"version": "0.0.1",
"description": "SDA Auto Update",
"scripts": {
"test": "cl tests.cpp /O2 /MTd /D_DEBUG /D_CRTDBG_MAP_ALLOC /nologo /link /SUBSYSTEM:CONSOLE && tests.exe",
"preinstall": "npm run test && npm run build",
"build": "rc.exe /nologo auto_update_helper.rc & cl auto_update_helper.c auto_update_helper.res /O2 /MT /nologo /link /SUBSYSTEM:CONSOLE /MANIFESTINPUT:manifest.xml /MANIFEST:EMBED & rc.exe /nologo auto_update_service.rc & cl auto_update_service.c auto_update_service.res /O2 /MT /nologo /link /SUBSYSTEM:CONSOLE /MANIFESTINPUT:manifest.xml /MANIFEST:EMBED"
}
}

View File

@ -1,17 +0,0 @@
@echo off
echo runs the tests in an infinite loop
echo intended to be run manually and left running for a long time,
echo as a final sanity check to make sure the tests are stable
echo will exit if any tests fail
set started=%date% %time%
:loop
echo INITIATED AT %started%
echo CURRENTLY AT %date% %time%
call npm run test
if %ERRORLEVEL% NEQ 0 (
echo.
echo INITIATED AT %started%
echo TERMINATED AT %date% %time%
goto :eof
)
goto :loop

View File

@ -1,323 +0,0 @@
#ifndef service_h
#define service_h
#include <stdbool.h>
bool service_install( char const* service_name );
bool service_uninstall( char const* service_name );
typedef void (*service_main_func_t)( bool* is_running );
void service_run( char const* service_name, service_main_func_t main_func );
void service_sleep( void );
void service_cancel_sleep( void );
#endif /* service_h */
#ifdef SERVICE_IMPLEMENTATION
#undef SERVICE_IMPLEMENTATION
#include <windows.h>
#include <stdio.h>
// Get an errror description for the last error from Windows, for logging purposes
// TODO: thread safe return value
char const* error_message( void ) {
DWORD error = GetLastError();
if( error == 0 ) {
return "";
}
LPSTR buffer = NULL;
size_t size = FormatMessageA(
FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS,
NULL, error, MAKELANGID( LANG_NEUTRAL, SUBLANG_DEFAULT ), (LPSTR)&buffer, 0, NULL);
static char message[ 1024 ] = "";
if( buffer ) {
strncpy( message, buffer, sizeof( message ) );
message[ sizeof( message ) - 1 ] = '\0';
LocalFree(buffer);
}
return message;
}
// Helper function to install the service, used for debugging (for production, service is set up by installer).
bool service_install( char const* service_name ) {
// Get the path to the running executable
TCHAR path[ MAX_PATH ];
if( !GetModuleFileName( NULL, path, MAX_PATH ) ) {
printf("Cannot install service: LastError = %d %s\n", GetLastError(), error_message() );
return false;
}
// Get a handle to the SCM database.
SC_HANDLE scm = OpenSCManager(
NULL, // local computer
NULL, // ServicesActive database
SC_MANAGER_ALL_ACCESS ); // full access rights
if( !scm ) {
printf( "OpenSCManager failed: LastError = %d %s\n", GetLastError(), error_message() );
return false;
}
// Create the service
SC_HANDLE service = CreateService(
scm, // SCM database
service_name, // name of service
service_name, // service name to display
SERVICE_ALL_ACCESS, // desired access
SERVICE_WIN32_OWN_PROCESS, // service type
SERVICE_DEMAND_START, // start type
SERVICE_ERROR_NORMAL, // error control type
path, // path to service's binary
NULL, // no load ordering group
NULL, // no tag identifier
NULL, // no dependencies
NULL, // LocalSystem account
NULL); // no password
if( !service ) {
printf( "CreateService failed: LastError = %d %s\n", GetLastError(), error_message() );
CloseServiceHandle( scm );
return false;
}
CloseServiceHandle( service );
CloseServiceHandle( scm );
return true;
}
// Helper function to uninstall the service, used for debugging (for production, service is set up by installer).
bool service_uninstall( char const* service_name ) {
// Get a handle to the SCM database.
SC_HANDLE scm = OpenSCManager(
NULL, // local computer
NULL, // ServicesActive database
SC_MANAGER_ALL_ACCESS ); // full access rights
if( !scm ) {
printf( "OpenSCManager failed: LastError = %d %s\n", GetLastError(), error_message() );
return false;
}
// Get a handle to the service.
SC_HANDLE service = OpenService(
scm, // SCM database
service_name, // name of service
DELETE); // need delete access
if( !service ) {
printf( "OpenService failed: LastError = %d %s\n", GetLastError(), error_message() );
CloseServiceHandle( scm );
return false;
}
// Delete the service.
if( !DeleteService( service ) ) {
printf( "DeleteService failed: LastError = %d %s\n", GetLastError(), error_message() );
CloseServiceHandle( service );
CloseServiceHandle( scm );
return false;
}
CloseServiceHandle( service );
CloseServiceHandle( scm );
return true;
}
// Global state for the service
struct {
char const* service_name;
service_main_func_t main_func;
bool is_running;
HANDLE stop_event;
SERVICE_STATUS service_status;
SERVICE_STATUS_HANDLE status_handle;
HANDLE sleep_event;
} g_service = { FALSE };
// Thread proc for service. Just calls the user-provided main func
DWORD WINAPI service_main_thread( LPVOID param ) {
SERVICE_LOG_INFO( "Starting service main thread" );
g_service.main_func( (bool*) param );
SERVICE_LOG_INFO( "Terminating service main thread" );
return EXIT_SUCCESS;
}
// Helper function to report the status of the service. The service needs to
// notify the windows service manager of its status correctly, otherwise it
// will be considerered unresponsive and closed
VOID report_service_status( DWORD current_state, DWORD exit_code, DWORD wait_hint ) {
SERVICE_LOG_INFO( "Reporting service status, current_state=%u, exit_code=%u, wait_hint=%u", current_state, exit_code, wait_hint );
static DWORD check_point = 1;
g_service.service_status.dwServiceType = SERVICE_WIN32_OWN_PROCESS;
g_service.service_status.dwServiceSpecificExitCode = 0;
g_service.service_status.dwCurrentState = current_state;
g_service.service_status.dwWin32ExitCode = exit_code;
g_service.service_status.dwWaitHint = wait_hint;
if( current_state == SERVICE_START_PENDING ) {
g_service.service_status.dwControlsAccepted = 0;
} else {
g_service.service_status.dwControlsAccepted = SERVICE_ACCEPT_STOP;
}
if( current_state == SERVICE_RUNNING || current_state == SERVICE_STOPPED ) {
g_service.service_status.dwCheckPoint = 0;
} else {
g_service.service_status.dwCheckPoint = check_point++;
}
// Report the status of the service to the SCM.
SetServiceStatus( g_service.status_handle, &g_service.service_status );
SERVICE_LOG_INFO( "Service statua reported" );
}
// Service control handler, used to signal the service to stop (if the user stops it
// in the Services control panel)
VOID WINAPI service_ctrl_handler( DWORD ctrl ) {
SERVICE_LOG_INFO( "Service control handler, ctrl=%u", ctrl );
switch( ctrl ) {
case SERVICE_CONTROL_STOP: {
SERVICE_LOG_INFO( "SERVICE_CONTROL_STOP" );
SERVICE_LOG_INFO( "Reporting service status SERVICE_STOP_PENDING" );
report_service_status( SERVICE_STOP_PENDING, NO_ERROR, 0 );
// Signal the service to stop.
SERVICE_LOG_INFO( "Signaling service to stop" );
SetEvent( g_service.stop_event );
SERVICE_LOG_INFO( "Reporting service status %u", g_service.service_status.dwCurrentState );
report_service_status(g_service.service_status.dwCurrentState, NO_ERROR, 0);
return;
} break;
case SERVICE_CONTROL_INTERROGATE: {
SERVICE_LOG_INFO( "SERVICE_CONTROL_INTERROGATE" );
SERVICE_LOG_INFO( "Reporting service status SERVICE_STOP_PENDING" );
report_service_status( g_service.service_status.dwCurrentState, NO_ERROR, 0 );
} break;
default:
break;
}
}
// TODO: better logging once we have a final solution for logging (another jira ticket)
// Main function of the service. Starts a thread to run the user-provided service function
VOID WINAPI service_proc( DWORD argc, LPSTR *argv ) {
SERVICE_LOG_INFO( "Starting service proc" );
// Register the handler function for the service
g_service.status_handle = RegisterServiceCtrlHandler( g_service.service_name, service_ctrl_handler );
if( !g_service.status_handle ) {
SERVICE_LOG_ERROR( "Failed to register the service control handler" );
return;
}
// Report initial status to the SCM
report_service_status( SERVICE_START_PENDING, NO_ERROR, 3000 );
// Create the event used to signal the service to stop
g_service.stop_event = CreateEvent(
NULL, // default security attributes
TRUE, // manual reset event
FALSE, // not signaled
NULL); // no name
if ( g_service.stop_event == NULL ) {
SERVICE_LOG_ERROR( "No event" );
report_service_status( SERVICE_STOPPED, NO_ERROR, 0 );
return;
}
// Start the thread which runs the user-provided main function
SERVICE_LOG_INFO( "Creating service main thread" );
HANDLE thread = CreateThread(
NULL, // no security attribute
0, // default stack size
service_main_thread, // thread proc
&g_service.is_running, // thread parameter
0, // not suspended
NULL ); // returns thread ID
// Report to the SCM that the service is now up and running
report_service_status( SERVICE_RUNNING, NO_ERROR, 0 );
SERVICE_LOG_INFO( "Service started" );
// Wait until the service is stopped
WaitForSingleObject( g_service.stop_event, INFINITE );
SERVICE_LOG_INFO( "Service stop requested" );
// Flag the user-provided main func to exit
g_service.is_running = false;
SetEvent( g_service.sleep_event );
// Wait until user-provided main func has exited
SERVICE_LOG_INFO( "Wait for service main thread to exit" );
WaitForSingleObject( thread, INFINITE );
SERVICE_LOG_INFO( "Service main thread exited" );
// Report to the SCM that the service has stopped
report_service_status( SERVICE_STOPPED, NO_ERROR, 0 );
SERVICE_LOG_INFO( "Service proc terminated" );
}
// Run the service with the specified name, and invoke the main_func function
// The main_func provided should exit if is_running becomes false
void service_run( char const* service_name, service_main_func_t main_func ) {
SetLastError( 0 );
SERVICE_LOG_INFO( "Starting service" );
// Initialize global state
g_service.service_name = service_name;
g_service.main_func = main_func;
g_service.is_running = true;
g_service.sleep_event = CreateEvent(
NULL, // default security attributes
TRUE, // manual reset event
FALSE, // not signaled
NULL); // no name
// Launch service
SERVICE_TABLE_ENTRY dispatch_table[] = {
{ (LPSTR) service_name, (LPSERVICE_MAIN_FUNCTION) service_proc },
{ NULL, NULL }
};
if( !StartServiceCtrlDispatcher( dispatch_table ) ) {
SERVICE_LOG_LAST_ERROR( "StartServiceCtrlDispatcher" );
}
// Cleanup
CloseHandle( g_service.sleep_event );
SERVICE_LOG_INFO( "Service stopped" );
}
// Sleeps until service_cancel_sleep is called, either manually by the user or
// because the serice received a stop request
void service_sleep( void ) {
ResetEvent( g_service.sleep_event );
WaitForSingleObject( g_service.sleep_event, INFINITE );
}
// Can be used to cancel a pending service sleep
void service_cancel_sleep( void ) {
SetEvent( g_service.sleep_event );
}
#endif /* SERVICE_IMPLEMENTATION */

View File

@ -1,546 +0,0 @@
/*
------------------------------------------------------------------------------
Licensing information can be found at the end of the file.
------------------------------------------------------------------------------
testfw.h - v1.1 - Basic test framwework for C/C++.
Do this:
#define TESTFW_IMPLEMENTATION
before you include this file in *one* C/C++ file to create the implementation.
*/
#ifndef testfw_h
#define testfw_h
#define TESTFW_INIT() testfw_init()
#define TESTFW_SUMMARY() testfw_summary( __FILE__, __func__, __LINE__ )
#if defined( _WIN32 ) && !defined( TESTFW_NO_SEH )
#define TESTFW_TEST_BEGIN( desc ) testfw_test_begin( desc, __FILE__, __func__, __LINE__ ); __try {
#define TESTFW_TEST_END() } __except( EXCEPTION_EXECUTE_HANDLER ) { testfw_exception( GetExceptionCode() ); } \
testfw_test_end( __FILE__, __func__, __LINE__ )
#else
#define TESTFW_TEST_BEGIN( desc ) testfw_test_begin( desc, __FILE__, __func__, __LINE__ )
#define TESTFW_TEST_END() testfw_test_end( __FILE__, __func__, __LINE__ )
#endif
#define TESTFW_EXPECTED( expression ) testfw_expected( (expression) ? 1 : 0, #expression, __FILE__, __func__, __LINE__ )
void testfw_init();
int testfw_summary( char const* filename, char const* funcname, int line );
void testfw_test_begin( char const* desc, char const* filename, char const* funcname, int line );
void testfw_test_end( char const* filename, char const* funcname, int line );
void testfw_expected( int expression, char const* expression_str, char const* filename, char const* funcname, int line );
void testfw_print_test_desc();
void testfw_print_failure( char const* filename, int line );
void testfw_assertion_count_inc();
void testfw_current_test_assertion_failed();
#if defined( _WIN32 ) && !defined( TESTFW_NO_SEH )
void testfw_exception( unsigned int exception_code );
#endif
#endif /* testfw_h */
#ifdef TESTFW_IMPLEMENTATION
#undef TESTFW_IMPLEMENTATION
#define _CRT_NONSTDC_NO_DEPRECATE
#define _CRT_SECURE_NO_WARNINGS
#include <string.h>
#ifdef _WIN32
#pragma warning( push )
#pragma warning( disable: 4619 ) // pragma warning : there is no warning number 'number'
#pragma warning( disable: 4668 ) // 'symbol' is not defined as a preprocessor macro, replacing with '0'
#include <crtdbg.h>
#pragma warning( pop )
#endif /* _WIN32 */
#ifndef TESTFW_PRINTF
#define _CRT_NONSTDC_NO_DEPRECATE
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#define TESTFW_PRINTF printf
#endif
#ifdef TESTFW_NO_ANSI
#define TESTFW_ANSI_BLACK ""
#define TESTFW_ANSI_BLUE ""
#define TESTFW_ANSI_GREEN ""
#define TESTFW_ANSI_CYAN ""
#define TESTFW_ANSI_RED ""
#define TESTFW_ANSI_MAGENTA ""
#define TESTFW_ANSI_BROWN ""
#define TESTFW_ANSI_LIGHT_GREY ""
#define TESTFW_ANSI_GREY ""
#define TESTFW_ANSI_LIGHT_BLUE ""
#define TESTFW_ANSI_LIGHT_GREEN ""
#define TESTFW_ANSI_LIGHT_CYAN ""
#define TESTFW_ANSI_LIGHT_RED ""
#define TESTFW_ANSI_LIGHT_MAGENTA ""
#define TESTFW_ANSI_YELLOW ""
#define TESTFW_ANSI_WHITE ""
#define TESTFW_ANSI_RESET ""
#else
#ifndef TESTFW_ANSI_BLACK
#define TESTFW_ANSI_BLACK "\x1b[30m"
#endif
#ifndef TESTFW_ANSI_BLUE
#define TESTFW_ANSI_BLUE "\x1b[34m"
#endif
#ifndef TESTFW_ANSI_GREEN
#define TESTFW_ANSI_GREEN "\x1b[32m"
#endif
#ifndef TESTFW_ANSI_CYAN
#define TESTFW_ANSI_CYAN "\x1b[36m"
#endif
#ifndef TESTFW_ANSI_RED
#define TESTFW_ANSI_RED "\x1b[31m"
#endif
#ifndef TESTFW_ANSI_MAGENTA
#define TESTFW_ANSI_MAGENTA "\x1b[35m"
#endif
#ifndef TESTFW_ANSI_BROWN
#define TESTFW_ANSI_BROWN "\x1b[33m"
#endif
#ifndef TESTFW_ANSI_LIGHT_GREY
#define TESTFW_ANSI_LIGHT_GREY "\x1b[37m"
#endif
#ifndef TESTFW_ANSI_GREY
#define TESTFW_ANSI_GREY "\x1b[30;1m"
#endif
#ifndef TESTFW_ANSI_LIGHT_BLUE
#define TESTFW_ANSI_LIGHT_BLUE "\x1b[34;1m"
#endif
#ifndef TESTFW_ANSI_LIGHT_GREEN
#define TESTFW_ANSI_LIGHT_GREEN "\x1b[32;1m"
#endif
#ifndef TESTFW_ANSI_LIGHT_CYAN
#define TESTFW_ANSI_LIGHT_CYAN "\x1b[36;1m"
#endif
#ifndef TESTFW_ANSI_LIGHT_RED
#define TESTFW_ANSI_LIGHT_RED "\x1b[31;1m"
#endif
#ifndef TESTFW_ANSI_LIGHT_MAGENTA
#define TESTFW_ANSI_LIGHT_MAGENTA "\x1b[35;1m"
#endif
#ifndef TESTFW_ANSI_YELLOW
#define TESTFW_ANSI_YELLOW "\x1b[33;1m"
#endif
#ifndef TESTFW_ANSI_WHITE
#define TESTFW_ANSI_WHITE "\x1b[37;1m"
#endif
#ifndef TESTFW_ANSI_RESET
#define TESTFW_ANSI_RESET "\x1b[0m"
#endif
#endif
struct testfw_internal_current_test_state_t
{
int is_active;
char const* file;
char const* func;
int line;
char const* desc;
int desc_printed;
int counted_as_failed;
};
static struct
{
int tests_total;
int tests_failed;
int assertions_total;
int assertions_failed;
testfw_internal_current_test_state_t current_test;
#if defined( _WIN32 ) && defined( _DEBUG )
int total_leaks;
#endif
} testfw_internal_state = {};
static void testfw_internal_print_progress_divider( char ch, int fail, int total )
{
int width = 79;
int first = (int)( ( width * fail ) / total );
int second = width - first;
if( fail > 0 && first == 0 )
{
++first;
--second;
}
TESTFW_PRINTF( "%s", TESTFW_ANSI_LIGHT_RED );
for( int i = 0; i < first; ++i ) TESTFW_PRINTF( "%c", ch );
TESTFW_PRINTF( "%s", TESTFW_ANSI_LIGHT_GREEN );
for( int i = 0; i < second; ++i ) TESTFW_PRINTF( "%c", ch );
TESTFW_PRINTF( "%s", TESTFW_ANSI_RESET );
TESTFW_PRINTF( "\n" );
}
#if defined( _WIN32 ) && defined( _DEBUG )
static int testfw_internal_debug_report_hook( int report_type, char* message, int* return_value )
{
*return_value = 0; // Don't break to debugger
if( stricmp( message, "Detected memory leaks!\n" ) == 0 )
{
TESTFW_PRINTF( "-------------------------------------------------------------------------------\n" );
TESTFW_PRINTF( "%sMEMORY CHECKS FAILED:%s Detected memory leaks\n", TESTFW_ANSI_LIGHT_RED,
TESTFW_ANSI_RESET );
return 1; // Tell CRT not to print the message
}
else if( stricmp( message, "Dumping objects ->\n" ) == 0 )
{
TESTFW_PRINTF( "-------------------------------------------------------------------------------\n" );
TESTFW_PRINTF( "\n" );
return 1; // Tell CRT not to print the message
}
else if( stricmp( message, "Object dump complete.\n" ) == 0 )
{
TESTFW_PRINTF( "\n" );
_CrtMemState state;
_CrtMemCheckpoint( &state );
static char const* const block_use_names[ 5 ] = { "Free", "Normal", "CRT", "Ignore", "Client", };
for( unsigned int i = 0; i < 5; ++i )
{
if( i != 2 && state.lCounts[ i ] )
TESTFW_PRINTF( "%lld bytes in %lld %hs Blocks.\n", (long long) state.lSizes[ i ],
(long long) state.lCounts[ i ], block_use_names[ i ] );
}
TESTFW_PRINTF( "\n" );
TESTFW_PRINTF( "High water mark: %lld bytes.\n", (long long)state.lHighWaterCount);
TESTFW_PRINTF( "All allocations: %lld bytes.\n", (long long)state.lTotalCount);
TESTFW_PRINTF( "\n" );
TESTFW_PRINTF( "===============================================================================\n" );
TESTFW_PRINTF( "%sMEMORY LEAKS: %d%s\n\n", TESTFW_ANSI_LIGHT_RED, testfw_internal_state.total_leaks, TESTFW_ANSI_RESET );
return 1; // Tell CRT not to print the message
}
else if( strnicmp( message, " Data: <", 8 ) == 0 )
{
++testfw_internal_state.total_leaks;
return 1; // Tell CRT not to print the message
}
return 0; // Tell CRT it can print the message, as we didn't handle it
}
#endif /* _WIN32 && _DEBUG */
void testfw_init()
{
memset( &testfw_internal_state, 0, sizeof( testfw_internal_state ) );
#ifdef _WIN32
int flag = _CrtSetDbgFlag( _CRTDBG_REPORT_FLAG ); // Get current flag
flag ^= _CRTDBG_LEAK_CHECK_DF; // Turn on leak-checking bit
_CrtSetDbgFlag( flag ); // Set flag to the new value
_CrtSetReportMode( _CRT_WARN, _CRTDBG_MODE_FILE );
_CrtSetReportFile( _CRT_WARN, _CRTDBG_FILE_STDOUT );
_CrtSetReportHook2( _CRT_RPTHOOK_INSTALL, testfw_internal_debug_report_hook );
#endif /* _WIN32 */
}
static int testfw_internal_must_be_in_test()
{
if( !testfw_internal_state.current_test.is_active )
TESTFW_PRINTF( "\n\n%sEXPECTED TO BE IN AN ACTIVE TEST, BUT NO TEST IS CURRENTLY ACTIVE.%s\n\n",
TESTFW_ANSI_LIGHT_RED, TESTFW_ANSI_RESET );
return !testfw_internal_state.current_test.is_active;
}
static int testfw_internal_can_not_be_in_test()
{
if( testfw_internal_state.current_test.is_active )
TESTFW_PRINTF( "\n\n%sEXPECTED TO NOT BE IN AN ACTIVE TEST, BUT A TEST IS CURRENTLY ACTIVE.%s\n\n",
TESTFW_ANSI_LIGHT_RED, TESTFW_ANSI_RESET );
return testfw_internal_state.current_test.is_active;
}
void testfw_print_test_desc()
{
if( testfw_internal_must_be_in_test() ) return;
if( !testfw_internal_state.current_test.desc_printed )
{
testfw_internal_state.current_test.desc_printed = 1;
TESTFW_PRINTF( "\n" );
TESTFW_PRINTF( "-------------------------------------------------------------------------------\n" );
TESTFW_PRINTF( "%s\n", testfw_internal_state.current_test.desc ? testfw_internal_state.current_test.desc :
"<NO DESCRIPTION>" );
TESTFW_PRINTF( "-------------------------------------------------------------------------------\n" );
TESTFW_PRINTF( "%s%s(%d): %s%s\n", TESTFW_ANSI_GREY, testfw_internal_state.current_test.file,
testfw_internal_state.current_test.line, TESTFW_ANSI_RESET, testfw_internal_state.current_test.func );
TESTFW_PRINTF( "%s", TESTFW_ANSI_GREY );
TESTFW_PRINTF( "...............................................................................\n" );
TESTFW_PRINTF( "%s", TESTFW_ANSI_RESET );
}
}
int testfw_summary( char const* filename, char const* funcname, int line )
{
if( testfw_internal_can_not_be_in_test() ) return -1;
TESTFW_PRINTF( "\n" );
testfw_internal_print_progress_divider( '=', testfw_internal_state.tests_failed, testfw_internal_state.tests_total );
if( testfw_internal_state.tests_failed == 0 && testfw_internal_state.assertions_failed == 0 )
{
TESTFW_PRINTF( "%sAll tests passed%s (%d assertions in %d test cases)\n", TESTFW_ANSI_LIGHT_GREEN,
TESTFW_ANSI_RESET, testfw_internal_state.assertions_total, testfw_internal_state.tests_total );
}
else
{
TESTFW_PRINTF( "test cases: %4d |%s %4d passed %s|%s %4d failed%s\n", testfw_internal_state.tests_total,
TESTFW_ANSI_LIGHT_GREEN, testfw_internal_state.tests_total - testfw_internal_state.tests_failed,
TESTFW_ANSI_RESET, TESTFW_ANSI_LIGHT_RED, testfw_internal_state.tests_failed, TESTFW_ANSI_RESET );
TESTFW_PRINTF( "assertions: %4d |%s %4d passed %s|%s %4d failed%s\n", testfw_internal_state.assertions_total,
TESTFW_ANSI_LIGHT_GREEN, testfw_internal_state.assertions_total - testfw_internal_state.assertions_failed,
TESTFW_ANSI_RESET, TESTFW_ANSI_LIGHT_RED, testfw_internal_state.assertions_failed, TESTFW_ANSI_RESET );
}
TESTFW_PRINTF( "\n\n" );
#if defined( _WIN32 ) && defined( _DEBUG )
int result = _CrtDumpMemoryLeaks();
testfw_internal_state.tests_failed += result ? 1 : 0;
#endif
return testfw_internal_state.tests_failed;
}
void testfw_test_begin( char const* desc, char const* filename, char const* funcname, int line )
{
if( testfw_internal_can_not_be_in_test() ) return;
memset( &testfw_internal_state.current_test, 0, sizeof( testfw_internal_state.current_test ) );
testfw_internal_state.current_test.is_active = 1;
testfw_internal_state.current_test.file = filename;
testfw_internal_state.current_test.func = funcname;
testfw_internal_state.current_test.line = line;
testfw_internal_state.current_test.desc = desc;
testfw_internal_state.current_test.desc_printed = 0;
testfw_internal_state.current_test.counted_as_failed = 0;
++testfw_internal_state.tests_total;
}
void testfw_test_end( char const* filename, char const* funcname, int line )
{
if( testfw_internal_must_be_in_test() ) return;
memset( &testfw_internal_state.current_test, 0, sizeof( testfw_internal_state.current_test ) );
}
#if defined( _WIN32 ) && !defined( TESTFW_NO_SEH )
void testfw_exception( unsigned int exception_code )
{
if( testfw_internal_must_be_in_test() ) return;
if( !testfw_internal_state.current_test.counted_as_failed )
{
testfw_internal_state.current_test.counted_as_failed = 1;
++testfw_internal_state.tests_failed;
}
char exception_str[ 64 ];
switch( exception_code )
{
case EXCEPTION_ACCESS_VIOLATION:
strcpy( exception_str, "EXCEPTION_ACCESS_VIOLATION" );
break;
case EXCEPTION_DATATYPE_MISALIGNMENT:
strcpy( exception_str, "EXCEPTION_DATATYPE_MISALIGNMENT" );
break;
case EXCEPTION_BREAKPOINT:
strcpy( exception_str, "EXCEPTION_BREAKPOINT" );
break;
case EXCEPTION_SINGLE_STEP:
strcpy( exception_str, "EXCEPTION_SINGLE_STEP" );
break;
case EXCEPTION_ARRAY_BOUNDS_EXCEEDED:
strcpy( exception_str, "EXCEPTION_ARRAY_BOUNDS_EXCEEDED" );
break;
case EXCEPTION_FLT_DENORMAL_OPERAND:
strcpy( exception_str, "EXCEPTION_FLT_DENORMAL_OPERAND" );
break;
case EXCEPTION_FLT_DIVIDE_BY_ZERO:
strcpy( exception_str, "EXCEPTION_FLT_DIVIDE_BY_ZERO" );
break;
case EXCEPTION_FLT_INEXACT_RESULT:
strcpy( exception_str, "EXCEPTION_FLT_INEXACT_RESULT" );
break;
case EXCEPTION_FLT_INVALID_OPERATION:
strcpy( exception_str, "EXCEPTION_FLT_INVALID_OPERATION" );
break;
case EXCEPTION_FLT_OVERFLOW:
strcpy( exception_str, "EXCEPTION_FLT_OVERFLOW" );
break;
case EXCEPTION_FLT_STACK_CHECK:
strcpy( exception_str, "EXCEPTION_FLT_STACK_CHECK" );
break;
case EXCEPTION_FLT_UNDERFLOW:
strcpy( exception_str, "EXCEPTION_FLT_UNDERFLOW" );
break;
case EXCEPTION_INT_DIVIDE_BY_ZERO:
strcpy( exception_str, "EXCEPTION_INT_DIVIDE_BY_ZERO" );
break;
case EXCEPTION_INT_OVERFLOW:
strcpy( exception_str, "EXCEPTION_INT_OVERFLOW" );
break;
case EXCEPTION_PRIV_INSTRUCTION:
strcpy( exception_str, "EXCEPTION_PRIV_INSTRUCTION" );
break;
case EXCEPTION_IN_PAGE_ERROR:
strcpy( exception_str, "EXCEPTION_IN_PAGE_ERROR" );
break;
case EXCEPTION_ILLEGAL_INSTRUCTION:
strcpy( exception_str, "EXCEPTION_ILLEGAL_INSTRUCTION" );
break;
case EXCEPTION_NONCONTINUABLE_EXCEPTION:
strcpy( exception_str, "EXCEPTION_NONCONTINUABLE_EXCEPTION" );
break;
case EXCEPTION_STACK_OVERFLOW:
strcpy( exception_str, "EXCEPTION_STACK_OVERFLOW" );
break;
case EXCEPTION_INVALID_DISPOSITION:
strcpy( exception_str, "EXCEPTION_INVALID_DISPOSITION" );
break;
case EXCEPTION_GUARD_PAGE:
strcpy( exception_str, "EXCEPTION_GUARD_PAGE" );
break;
case EXCEPTION_INVALID_HANDLE:
strcpy( exception_str, "EXCEPTION_INVALID_HANDLE" );
break;
default:
sprintf( exception_str, "%X", exception_code );
}
testfw_print_test_desc();
TESTFW_PRINTF( "\n%s%s(%d): %sFAILED:%s\n", TESTFW_ANSI_GREY, testfw_internal_state.current_test.file, testfw_internal_state.current_test.line, TESTFW_ANSI_LIGHT_RED,
TESTFW_ANSI_RESET );
TESTFW_PRINTF( "\n %sEXCEPTION( %s%s%s )%s\n", TESTFW_ANSI_CYAN, TESTFW_ANSI_WHITE, exception_str,
TESTFW_ANSI_CYAN, TESTFW_ANSI_RESET );
}
#endif
void testfw_current_test_assertion_failed()
{
if( testfw_internal_must_be_in_test() ) return;
++testfw_internal_state.assertions_failed;
if( !testfw_internal_state.current_test.counted_as_failed )
{
testfw_internal_state.current_test.counted_as_failed = 1;
++testfw_internal_state.tests_failed;
}
}
void testfw_assertion_count_inc()
{
if( testfw_internal_must_be_in_test() ) return;
++testfw_internal_state.assertions_total;
}
void testfw_print_failure( char const* filename, int line )
{
TESTFW_PRINTF( "\n%s%s(%d): %sFAILED:%s\n", TESTFW_ANSI_GREY, filename, line, TESTFW_ANSI_LIGHT_RED,
TESTFW_ANSI_RESET );
}
void testfw_expected( int expression, char const* expression_str, char const* filename,
char const* funcname, int line )
{
if( testfw_internal_must_be_in_test() ) return;
testfw_assertion_count_inc();
if( !expression )
{
testfw_current_test_assertion_failed();
testfw_print_test_desc();
testfw_print_failure( filename, line );
TESTFW_PRINTF( "\n %sTESTFW_EXPECTED( %s%s%s )%s\n", TESTFW_ANSI_CYAN, TESTFW_ANSI_WHITE, expression_str,
TESTFW_ANSI_CYAN, TESTFW_ANSI_RESET );
}
}
#endif /* TESTFW_IMPLEMENTATION */
/*
------------------------------------------------------------------------------
This software is available under 2 licenses - you may choose the one you like.
------------------------------------------------------------------------------
ALTERNATIVE A - MIT License
Copyright (c) 2019 Mattias Gustavsson
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
------------------------------------------------------------------------------
ALTERNATIVE B - Public Domain (www.unlicense.org)
This is free and unencumbered software released into the public domain.
Anyone is free to copy, modify, publish, use, compile, sell, or distribute this
software, either in source code form or as a compiled binary, for any purpose,
commercial or non-commercial, and by any means.
In jurisdictions that recognize copyright laws, the author or authors of this
software dedicate any and all copyright interest in the software to the public
domain. We make this dedication for the benefit of the public at large and to
the detriment of our heirs and successors. We intend this dedication to be an
overt act of relinquishment in perpetuity of all present and future rights to
this software under copyright law.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
------------------------------------------------------------------------------
*/

View File

@ -1,241 +0,0 @@
#define _CRT_SECURE_NO_WARNINGS
#define _CRT_NONSTDC_NO_WARNINGS
#if defined( _WIN32 ) && defined( _DEBUG )
#include <crtdbg.h>
#endif
#include "testfw.h"
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <windows.h>
void disable_log( char const*, ... ) { };
#define IPC_LOG_INFO disable_log
#define IPC_LOG_ERROR disable_log
#define IPC_LOG_LAST_ERROR disable_log
#include "ipc.h"
bool pipe_exists( const char* pipe_name );
void test_fw_ok() {
TESTFW_TEST_BEGIN( "Checking that test framework is ok" );
TESTFW_EXPECTED( true );
TESTFW_TEST_END();
}
void ipc_tests() {
{
TESTFW_TEST_BEGIN( "Check that IPC server is not already running" );
TESTFW_EXPECTED( !pipe_exists( "test_pipe" ) );
TESTFW_TEST_END();
}
{
TESTFW_TEST_BEGIN( "Can start IPC server" );
ipc_server_t* server = ipc_server_start( "test_pipe",
[]( char const*, void*, char*, size_t ) { }, NULL );
TESTFW_EXPECTED( server != NULL );
ipc_server_stop( server );
TESTFW_TEST_END();
}
{
TESTFW_TEST_BEGIN( "Can stop IPC server" );
ipc_server_t* server = ipc_server_start( "test_pipe",
[]( char const*, void*, char*, size_t ) { }, NULL );
TESTFW_EXPECTED( server != NULL );
ipc_server_stop( server );
TESTFW_EXPECTED( !pipe_exists( "test_pipe" ) );
TESTFW_TEST_END();
}
{
TESTFW_TEST_BEGIN( "Can connect multiple IPC clients" );
ipc_server_t* server = ipc_server_start( "test_pipe",
[]( char const*, void*, char*, size_t ) { }, NULL );
TESTFW_EXPECTED( server != NULL );
ipc_client_t* clients[ 32 ];
for( int i = 0; i < sizeof( clients ) / sizeof( *clients ); ++i ) {
clients[ i ] = ipc_client_connect( "test_pipe" );
TESTFW_EXPECTED( clients[ i ] );
}
for( int i = 0; i < sizeof( clients ) / sizeof( *clients ); ++i ) {
ipc_client_disconnect( clients[ i ] );
}
ipc_server_stop( server );
TESTFW_TEST_END();
}
{
TESTFW_TEST_BEGIN( "Can connect multiple IPC clients multiple times" );
ipc_server_t* server = ipc_server_start( "test_pipe",
[]( char const*, void*, char*, size_t ) { }, NULL );
TESTFW_EXPECTED( server != NULL );
for( int j = 0; j < 10; ++j ) {
ipc_client_t* clients[ 32 ];
for( int i = 0; i < sizeof( clients ) / sizeof( *clients ); ++i ) {
clients[ i ] = ipc_client_connect( "test_pipe" );
TESTFW_EXPECTED( clients[ i ] );
}
for( int i = 0; i < sizeof( clients ) / sizeof( *clients ); ++i ) {
ipc_client_disconnect( clients[ i ] );
}
}
ipc_server_stop( server );
TESTFW_TEST_END();
}
{
TESTFW_TEST_BEGIN( "Can connect IPC client" );
ipc_server_t* server = ipc_server_start( "test_pipe",
[]( char const*, void*, char*, size_t ) { }, NULL );
TESTFW_EXPECTED( server != NULL );
ipc_client_t* client = ipc_client_connect( "test_pipe" );
TESTFW_EXPECTED( client != NULL );
ipc_client_disconnect( client );
ipc_server_stop( server );
TESTFW_TEST_END();
}
{
TESTFW_TEST_BEGIN( "Can send IPC message from client to server" );
bool message_received = false;
ipc_server_t* server = ipc_server_start( "test_pipe",
[]( char const* message, void* user_data, char*, size_t ) {
if( !message ) return; // client disconnect
bool* message_received = (bool*) user_data;
*message_received = true;
TESTFW_EXPECTED( strcmp( message, "Test message" ) == 0 );
}, &message_received );
TESTFW_EXPECTED( server != NULL );
ipc_client_t* client = ipc_client_connect( "test_pipe" );
TESTFW_EXPECTED( client != NULL );
TESTFW_EXPECTED( ipc_client_send( client, "Test message" ) == true );
char temp[ IPC_MESSAGE_MAX_LENGTH ];
int size = 0;
ipc_client_receive( client, temp, sizeof( temp ), &size );
TESTFW_EXPECTED( message_received == true );
ipc_client_disconnect( client );
ipc_server_stop( server );
TESTFW_TEST_END();
}
{
TESTFW_TEST_BEGIN( "Can receive IPC response from server" );
ipc_server_t* server = ipc_server_start( "test_pipe",
[]( char const* message, void* user_data, char* response, size_t ) {
if( !message ) return; // client disconnect
strcpy( response, "Test response" );
}, NULL );
TESTFW_EXPECTED( server != NULL );
ipc_client_t* client = ipc_client_connect( "test_pipe" );
TESTFW_EXPECTED( client != NULL );
TESTFW_EXPECTED( ipc_client_send( client, "Test message" ) == true );
char response[ IPC_MESSAGE_MAX_LENGTH ];
int size = 0;
TESTFW_EXPECTED( ipc_client_receive( client, response, sizeof( response ), &size ) == IPC_RECEIVE_STATUS_DONE );
TESTFW_EXPECTED( size == strlen( "Test response" ) + 1 );
TESTFW_EXPECTED( strcmp( response, "Test response" ) == 0 );
ipc_client_disconnect( client );
ipc_server_stop( server );
TESTFW_TEST_END();
}
{
TESTFW_TEST_BEGIN( "Can send and receive long IPC messages" );
ipc_server_t* server = ipc_server_start( "test_pipe",
[]( char const* message, void* user_data, char* response, size_t capacity ) {
if( !message ) return; // client disconnect
char expected_message[ IPC_MESSAGE_MAX_LENGTH ];
for( int i = 0; i < IPC_MESSAGE_MAX_LENGTH - 1; ++i ) {
expected_message[ i ] = 'A' + ( i % ( 'Z' - 'A' + 1 ) );
}
expected_message[ IPC_MESSAGE_MAX_LENGTH - 1 ] = '\0';
TESTFW_EXPECTED( strcmp( message, expected_message ) == 0 );
for( int i = 0; i < (int) capacity - 1; ++i ) {
response[ i ] = 'a' + ( i % ( 'z' - 'a' + 1 ) );
}
response[ capacity - 1 ] = '\0';
}, NULL );
TESTFW_EXPECTED( server != NULL );
ipc_client_t* client = ipc_client_connect( "test_pipe" );
TESTFW_EXPECTED( client != NULL );
char message[ IPC_MESSAGE_MAX_LENGTH ];
for( int i = 0; i < IPC_MESSAGE_MAX_LENGTH - 1; ++i ) {
message[ i ] = 'A' + ( i % ( 'Z' - 'A' + 1 ) );
}
message[ IPC_MESSAGE_MAX_LENGTH - 1 ] = '\0';
TESTFW_EXPECTED( ipc_client_send( client, message ) == true );
char response[ IPC_MESSAGE_MAX_LENGTH ];
int size = 0;
TESTFW_EXPECTED( ipc_client_receive( client, response, sizeof( response ), &size ) == IPC_RECEIVE_STATUS_DONE );
char expected_response[ IPC_MESSAGE_MAX_LENGTH ];
for( int i = 0; i < IPC_MESSAGE_MAX_LENGTH - 1; ++i ) {
expected_response[ i ] = 'a' + ( i % ( 'z' - 'a' + 1 ) );
}
expected_response[ IPC_MESSAGE_MAX_LENGTH - 1 ] = '\0';
TESTFW_EXPECTED( size == IPC_MESSAGE_MAX_LENGTH );
TESTFW_EXPECTED( strcmp( response, expected_response ) == 0 );
ipc_client_disconnect( client );
ipc_server_stop( server );
TESTFW_TEST_END();
}
{
TESTFW_TEST_BEGIN( "Can send and receive multiple IPC messages" );
int received_count = 0;
ipc_server_t* server = ipc_server_start( "test_pipe",
[]( char const* message, void* user_data, char* response, size_t ) {
if( !message ) return; // client disconnect
int* received_count = (int*) user_data;
char expected_message[ IPC_MESSAGE_MAX_LENGTH ];
sprintf( expected_message, "Test message %d", *received_count );
TESTFW_EXPECTED( strcmp( message, expected_message ) == 0 );
sprintf( response, "Test response %d", *received_count );
*received_count = *received_count + 1;
}, &received_count );
TESTFW_EXPECTED( server != NULL );
ipc_client_t* client = ipc_client_connect( "test_pipe" );
TESTFW_EXPECTED( client != NULL );
for( int i = 0; i < 64; ++i ) {
char message[ IPC_MESSAGE_MAX_LENGTH ];
sprintf( message, "Test message %d", i );
TESTFW_EXPECTED( ipc_client_send( client, message ) == true );
char response[ IPC_MESSAGE_MAX_LENGTH ];
int size = 0;
TESTFW_EXPECTED( ipc_client_receive( client, response, sizeof( response ), &size ) == IPC_RECEIVE_STATUS_DONE );
char expected_response[ IPC_MESSAGE_MAX_LENGTH ];
sprintf( expected_response, "Test response %d", i );
TESTFW_EXPECTED( size == strlen( expected_response ) + 1 );
TESTFW_EXPECTED( strcmp( response, expected_response ) == 0 );
}
TESTFW_EXPECTED( received_count == 64 );
ipc_client_disconnect( client );
ipc_server_stop( server );
TESTFW_TEST_END();
}
}
int main( int argc, char** argv ) {
TESTFW_INIT();
test_fw_ok();
ipc_tests();
int result = TESTFW_SUMMARY();
return result == 0 ? EXIT_SUCCESS : EXIT_FAILURE;
}
#define IPC_IMPLEMENTATION
#include "ipc.h"
#define TESTFW_IMPLEMENTATION
#define TESTFW_NO_ANSI
#include "testfw.h"

View File

@ -1,155 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup Label="ProjectConfigurations">
<ProjectConfiguration Include="Debug|Win32">
<Configuration>Debug</Configuration>
<Platform>Win32</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Release|Win32">
<Configuration>Release</Configuration>
<Platform>Win32</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Debug|x64">
<Configuration>Debug</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Release|x64">
<Configuration>Release</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
</ItemGroup>
<PropertyGroup Label="Globals">
<VCProjectVersion>16.0</VCProjectVersion>
<Keyword>Win32Proj</Keyword>
<ProjectGuid>{98a68237-c5f6-487d-8d49-346d6991f2c5}</ProjectGuid>
<RootNamespace>tests</RootNamespace>
<WindowsTargetPlatformVersion>10.0</WindowsTargetPlatformVersion>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'" Label="Configuration">
<ConfigurationType>Application</ConfigurationType>
<UseDebugLibraries>true</UseDebugLibraries>
<PlatformToolset>v142</PlatformToolset>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'" Label="Configuration">
<ConfigurationType>Application</ConfigurationType>
<UseDebugLibraries>false</UseDebugLibraries>
<PlatformToolset>v142</PlatformToolset>
<WholeProgramOptimization>true</WholeProgramOptimization>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'" Label="Configuration">
<ConfigurationType>Application</ConfigurationType>
<UseDebugLibraries>true</UseDebugLibraries>
<PlatformToolset>v142</PlatformToolset>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'" Label="Configuration">
<ConfigurationType>Application</ConfigurationType>
<UseDebugLibraries>false</UseDebugLibraries>
<PlatformToolset>v142</PlatformToolset>
<WholeProgramOptimization>true</WholeProgramOptimization>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
<ImportGroup Label="ExtensionSettings">
</ImportGroup>
<ImportGroup Label="Shared">
</ImportGroup>
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<PropertyGroup Label="UserMacros" />
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
<LinkIncremental>true</LinkIncremental>
<IntDir>$(Platform)\$(Configuration)\tests\</IntDir>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
<LinkIncremental>false</LinkIncremental>
<IntDir>$(Platform)\$(Configuration)\tests\</IntDir>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<LinkIncremental>true</LinkIncremental>
<IntDir>$(Platform)\$(Configuration)\tests\</IntDir>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
<LinkIncremental>false</LinkIncremental>
<IntDir>$(Platform)\$(Configuration)\tests\</IntDir>
</PropertyGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
<ClCompile>
<WarningLevel>Level3</WarningLevel>
<SDLCheck>true</SDLCheck>
<PreprocessorDefinitions>_CRTDBG_MAP_ALLOC;WIN32;_DEBUG;_CONSOLE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode>
</ClCompile>
<Link>
<SubSystem>Console</SubSystem>
<GenerateDebugInformation>true</GenerateDebugInformation>
</Link>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
<ClCompile>
<WarningLevel>Level3</WarningLevel>
<FunctionLevelLinking>true</FunctionLevelLinking>
<IntrinsicFunctions>true</IntrinsicFunctions>
<SDLCheck>true</SDLCheck>
<PreprocessorDefinitions>_CRTDBG_MAP_ALLOC;WIN32;NDEBUG;_CONSOLE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode>
</ClCompile>
<Link>
<SubSystem>Console</SubSystem>
<EnableCOMDATFolding>true</EnableCOMDATFolding>
<OptimizeReferences>true</OptimizeReferences>
<GenerateDebugInformation>true</GenerateDebugInformation>
</Link>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<ClCompile>
<WarningLevel>Level3</WarningLevel>
<SDLCheck>true</SDLCheck>
<PreprocessorDefinitions>_CRTDBG_MAP_ALLOC;_DEBUG;_CONSOLE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode>
</ClCompile>
<Link>
<SubSystem>Console</SubSystem>
<GenerateDebugInformation>true</GenerateDebugInformation>
</Link>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
<ClCompile>
<WarningLevel>Level3</WarningLevel>
<FunctionLevelLinking>true</FunctionLevelLinking>
<IntrinsicFunctions>true</IntrinsicFunctions>
<SDLCheck>true</SDLCheck>
<PreprocessorDefinitions>_CRTDBG_MAP_ALLOC;NDEBUG;_CONSOLE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode>
</ClCompile>
<Link>
<SubSystem>Console</SubSystem>
<EnableCOMDATFolding>true</EnableCOMDATFolding>
<OptimizeReferences>true</OptimizeReferences>
<GenerateDebugInformation>true</GenerateDebugInformation>
</Link>
</ItemDefinitionGroup>
<ItemGroup>
<ClInclude Include="ipc.h" />
<ClInclude Include="testfw.h" />
</ItemGroup>
<ItemGroup>
<ClCompile Include="tests.cpp" />
</ItemGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
<ImportGroup Label="ExtensionTargets">
</ImportGroup>
</Project>

26
build/installer.nsh Normal file
View File

@ -0,0 +1,26 @@
!include LogicLib.nsh
; Uninstall existing Symphony before installing a new version
!macro uninstallSymphony
StrCpy $0 0
SetRegView 64
loop:
EnumRegKey $1 HKLM "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall" $0
StrCmp $1 "" done
ReadRegStr $2 HKLM "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\$1" "DisplayName"
${If} $2 == "Symphony"
ReadRegStr $3 HKLM "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\$1" "UninstallString"
ExecWait '$3 /qn'
${EndIf}
IntOp $0 $0 + 1
Goto loop
done:
!macroend
; Preinstall script
!macro preInit
SetRegView 64
!insertmacro uninstallSymphony
WriteRegExpandStr HKLM "${INSTALL_REGISTRY_KEY}" InstallLocation "$PROGRAMFILES64\Symphony\Symphony"
WriteRegExpandStr HKCU "${INSTALL_REGISTRY_KEY}" InstallLocation "$PROGRAMFILES64\Symphony\Symphony"
!macroend

View File

@ -1,5 +1,9 @@
{
"url":"https://my.symphony.com",
"autoUpdateUrl": "",
"autoUpdateChannel": "general",
"isAutoUpdateEnabled": true,
"autoUpdateCheckInterval": "30",
"overrideUserAgent": false,
"minimizeOnClose" : "ENABLED",
"launchOnStartup" : "ENABLED",

View File

@ -29,32 +29,6 @@ class Script
var userDataPathArgument = "--userDataPath=\"[USER_DATA_PATH]\"";
//Disabling auto-update service
// File updateService = new File(@"..\..\..\dist\win-unpacked\resources\app.asar.unpacked\node_modules\auto-update\auto_update_service.exe");
// updateService.ServiceInstaller = new ServiceInstaller
// {
// Name = "auto_update_service",
// DisplayName = "SDA Auto Update Service",
// Description = "Symphony Desktop Application Auto Update Service",
// FirstFailureActionType = FailureActionType.restart,
// SecondFailureActionType = FailureActionType.restart,
// ThirdFailureActionType = FailureActionType.restart,
// RestartServiceDelayInSeconds = 60,
// ResetPeriodInDays = 1,
// ServiceSid = ServiceSid.none,
// ConfigureServiceTrigger = ConfigureServiceTrigger.Install,
// Type = SvcType.ownProcess,
// Vital = true,
// ErrorControl = SvcErrorControl.normal,
// Start = SvcStartType.auto,
// EraseDescription = false,
// Interactive = false,
// StartOn = SvcEvent.Install,
// StopOn = SvcEvent.InstallUninstall_Wait,
// RemoveOn = SvcEvent.Uninstall_Wait,
// };
// Create a wixsharp project instance and assign the project name to it, and a hierarchy of all files to include
// Files are taken from multiple locations, and not all files in each location should be included, which is why
// the file list is rather long and explicit. At some point we might make the `dist` folder match exactly the
@ -75,7 +49,6 @@ class Script
}
),
// updateService,
// new File(@"..\..\..\dist\win-unpacked\resources\app.asar.unpacked\node_modules\auto-update\auto_update_helper.exe"),
new File(@"..\..\..\dist\win-unpacked\chrome_100_percent.pak"),
new File(@"..\..\..\dist\win-unpacked\chrome_200_percent.pak"),
new File(@"..\..\..\dist\win-unpacked\d3dcompiler_47.dll"),
@ -514,6 +487,7 @@ public class CustomActions
{
key.DeleteValue("Symphony", false);
key.DeleteValue("com.symphony.electron-desktop", false);
key.DeleteValue("com.symphony.electron_desktop", false);
key.DeleteValue("electron.app.Symphony", false);
}
}

209
package-lock.json generated
View File

@ -17,6 +17,7 @@
"electron-dl": "3.0.0",
"electron-fetch": "1.4.0",
"electron-log": "4.0.7",
"electron-updater": "^5.0.1",
"ffi-napi": "^4.0.3",
"filesize": "6.1.0",
"lazy-brush": "^1.0.1",
@ -2641,6 +2642,12 @@
"@types/node": "*"
}
},
"node_modules/@types/semver": {
"version": "7.3.9",
"resolved": "https://repo.symphony.com/artifactory/api/npm/npm-virtual-dev/@types/semver/-/semver-7.3.9.tgz",
"integrity": "sha1-FSxsIKdojDC5Z+wYQdMazlaYY/w=",
"license": "MIT"
},
"node_modules/@types/sinon": {
"version": "7.5.2",
"resolved": "https://repo.symphony.com/artifactory/api/npm/npm-virtual-dev/@types/sinon/-/sinon-7.5.2.tgz",
@ -9387,6 +9394,103 @@
"dev": true,
"license": "ISC"
},
"node_modules/electron-updater": {
"version": "5.0.1",
"resolved": "https://repo.symphony.com/artifactory/api/npm/npm-virtual-dev/electron-updater/-/electron-updater-5.0.1.tgz",
"integrity": "sha1-MnkV8bLlc9rxzh2BAryrk/SIDBo=",
"license": "MIT",
"dependencies": {
"@types/semver": "^7.3.6",
"builder-util-runtime": "9.0.0",
"fs-extra": "^10.0.0",
"js-yaml": "^4.1.0",
"lazy-val": "^1.0.5",
"lodash.escaperegexp": "^4.1.2",
"lodash.isequal": "^4.5.0",
"semver": "^7.3.5"
}
},
"node_modules/electron-updater/node_modules/argparse": {
"version": "2.0.1",
"resolved": "https://repo.symphony.com/artifactory/api/npm/npm-virtual-dev/argparse/-/argparse-2.0.1.tgz",
"integrity": "sha1-JG9Q88p4oyQPbJl+ipvR6sSeSzg=",
"license": "Python-2.0"
},
"node_modules/electron-updater/node_modules/builder-util-runtime": {
"version": "9.0.0",
"resolved": "https://repo.symphony.com/artifactory/api/npm/npm-virtual-dev/builder-util-runtime/-/builder-util-runtime-9.0.0.tgz",
"integrity": "sha1-OkC6c4JxLM2yRHFWf5HXwWfgCDA=",
"license": "MIT",
"dependencies": {
"debug": "^4.3.2",
"sax": "^1.2.4"
},
"engines": {
"node": ">=12.0.0"
}
},
"node_modules/electron-updater/node_modules/fs-extra": {
"version": "10.1.0",
"resolved": "https://repo.symphony.com/artifactory/api/npm/npm-virtual-dev/fs-extra/-/fs-extra-10.1.0.tgz",
"integrity": "sha1-Aoc8+8QITd4SfqpfmQXu8jJdGr8=",
"license": "MIT",
"dependencies": {
"graceful-fs": "^4.2.0",
"jsonfile": "^6.0.1",
"universalify": "^2.0.0"
},
"engines": {
"node": ">=12"
}
},
"node_modules/electron-updater/node_modules/js-yaml": {
"version": "4.1.0",
"resolved": "https://repo.symphony.com/artifactory/api/npm/npm-virtual-dev/js-yaml/-/js-yaml-4.1.0.tgz",
"integrity": "sha1-wftl+PUBeQHN0slRhkuhhFihBgI=",
"license": "MIT",
"dependencies": {
"argparse": "^2.0.1"
},
"bin": {
"js-yaml": "bin/js-yaml.js"
}
},
"node_modules/electron-updater/node_modules/jsonfile": {
"version": "6.1.0",
"resolved": "https://repo.symphony.com/artifactory/api/npm/npm-virtual-dev/jsonfile/-/jsonfile-6.1.0.tgz",
"integrity": "sha1-vFWyY0eTxnnsZAMJTrE2mKbsCq4=",
"license": "MIT",
"dependencies": {
"universalify": "^2.0.0"
},
"optionalDependencies": {
"graceful-fs": "^4.1.6"
}
},
"node_modules/electron-updater/node_modules/semver": {
"version": "7.3.7",
"resolved": "https://repo.symphony.com/artifactory/api/npm/npm-virtual-dev/semver/-/semver-7.3.7.tgz",
"integrity": "sha1-EsW2Sa/b+QSXB3luIqQCiBTOUj8=",
"license": "ISC",
"dependencies": {
"lru-cache": "^6.0.0"
},
"bin": {
"semver": "bin/semver.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/electron-updater/node_modules/universalify": {
"version": "2.0.0",
"resolved": "https://repo.symphony.com/artifactory/api/npm/npm-virtual-dev/universalify/-/universalify-2.0.0.tgz",
"integrity": "sha1-daSYTv7cSwiXXFrrc/Uw0C3yVxc=",
"license": "MIT",
"engines": {
"node": ">= 10.0.0"
}
},
"node_modules/electron/node_modules/@types/node": {
"version": "16.11.33",
"resolved": "https://repo.symphony.com/artifactory/api/npm/npm-virtual-dev/@types/node/-/node-16.11.33.tgz",
@ -15835,7 +15939,6 @@
"version": "1.0.5",
"resolved": "https://repo.symphony.com/artifactory/api/npm/npm-virtual-dev/lazy-val/-/lazy-val-1.0.5.tgz",
"integrity": "sha1-bPO59bwxzufuPjacCDK3WD3Nkj0=",
"dev": true,
"license": "MIT"
},
"node_modules/lazystream": {
@ -16160,6 +16263,12 @@
"dev": true,
"license": "MIT"
},
"node_modules/lodash.escaperegexp": {
"version": "4.1.2",
"resolved": "https://repo.symphony.com/artifactory/api/npm/npm-virtual-dev/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz",
"integrity": "sha1-ZHYsSGGAglGKw99Mz11YhtriA0c=",
"license": "MIT"
},
"node_modules/lodash.flatten": {
"version": "4.4.0",
"resolved": "https://repo.symphony.com/artifactory/api/npm/npm-virtual-dev/lodash.flatten/-/lodash.flatten-4.4.0.tgz",
@ -16184,7 +16293,6 @@
"version": "4.5.0",
"resolved": "https://repo.symphony.com/artifactory/api/npm/npm-virtual-dev/lodash.isequal/-/lodash.isequal-4.5.0.tgz",
"integrity": "sha1-QVxEePK8wwEgwizhDtMib30+GOA=",
"dev": true,
"license": "MIT"
},
"node_modules/lodash.islength": {
@ -16356,7 +16464,6 @@
"version": "6.0.0",
"resolved": "https://repo.symphony.com/artifactory/api/npm/npm-virtual-dev/lru-cache/-/lru-cache-6.0.0.tgz",
"integrity": "sha1-bW/mVw69lqr5D8rR2vo7JWbbOpQ=",
"dev": true,
"license": "ISC",
"dependencies": {
"yallist": "^4.0.0"
@ -20824,7 +20931,6 @@
"version": "1.2.4",
"resolved": "https://repo.symphony.com/artifactory/api/npm/npm-virtual-dev/sax/-/sax-1.2.4.tgz",
"integrity": "sha1-KBYjTiN4vdxOU1T6tcqold9xANk=",
"dev": true,
"license": "ISC"
},
"node_modules/saxes": {
@ -25039,7 +25145,6 @@
"version": "4.0.0",
"resolved": "https://repo.symphony.com/artifactory/api/npm/npm-virtual-dev/yallist/-/yallist-4.0.0.tgz",
"integrity": "sha1-m7knkNnA7/7GO+c1GeEaNQGaOnI=",
"dev": true,
"license": "ISC"
},
"node_modules/yaml": {
@ -27134,6 +27239,11 @@
"@types/node": "*"
}
},
"@types/semver": {
"version": "7.3.9",
"resolved": "https://repo.symphony.com/artifactory/api/npm/npm-virtual-dev/@types/semver/-/semver-7.3.9.tgz",
"integrity": "sha1-FSxsIKdojDC5Z+wYQdMazlaYY/w="
},
"@types/sinon": {
"version": "7.5.2",
"resolved": "https://repo.symphony.com/artifactory/api/npm/npm-virtual-dev/@types/sinon/-/sinon-7.5.2.tgz",
@ -32256,6 +32366,77 @@
"integrity": "sha1-TQBHkFWnKCzdGxnK7Antd3lSlkA=",
"dev": true
},
"electron-updater": {
"version": "5.0.1",
"resolved": "https://repo.symphony.com/artifactory/api/npm/npm-virtual-dev/electron-updater/-/electron-updater-5.0.1.tgz",
"integrity": "sha1-MnkV8bLlc9rxzh2BAryrk/SIDBo=",
"requires": {
"@types/semver": "^7.3.6",
"builder-util-runtime": "9.0.0",
"fs-extra": "^10.0.0",
"js-yaml": "^4.1.0",
"lazy-val": "^1.0.5",
"lodash.escaperegexp": "^4.1.2",
"lodash.isequal": "^4.5.0",
"semver": "^7.3.5"
},
"dependencies": {
"argparse": {
"version": "2.0.1",
"resolved": "https://repo.symphony.com/artifactory/api/npm/npm-virtual-dev/argparse/-/argparse-2.0.1.tgz",
"integrity": "sha1-JG9Q88p4oyQPbJl+ipvR6sSeSzg="
},
"builder-util-runtime": {
"version": "9.0.0",
"resolved": "https://repo.symphony.com/artifactory/api/npm/npm-virtual-dev/builder-util-runtime/-/builder-util-runtime-9.0.0.tgz",
"integrity": "sha1-OkC6c4JxLM2yRHFWf5HXwWfgCDA=",
"requires": {
"debug": "^4.3.2",
"sax": "^1.2.4"
}
},
"fs-extra": {
"version": "10.1.0",
"resolved": "https://repo.symphony.com/artifactory/api/npm/npm-virtual-dev/fs-extra/-/fs-extra-10.1.0.tgz",
"integrity": "sha1-Aoc8+8QITd4SfqpfmQXu8jJdGr8=",
"requires": {
"graceful-fs": "^4.2.0",
"jsonfile": "^6.0.1",
"universalify": "^2.0.0"
}
},
"js-yaml": {
"version": "4.1.0",
"resolved": "https://repo.symphony.com/artifactory/api/npm/npm-virtual-dev/js-yaml/-/js-yaml-4.1.0.tgz",
"integrity": "sha1-wftl+PUBeQHN0slRhkuhhFihBgI=",
"requires": {
"argparse": "^2.0.1"
}
},
"jsonfile": {
"version": "6.1.0",
"resolved": "https://repo.symphony.com/artifactory/api/npm/npm-virtual-dev/jsonfile/-/jsonfile-6.1.0.tgz",
"integrity": "sha1-vFWyY0eTxnnsZAMJTrE2mKbsCq4=",
"requires": {
"graceful-fs": "^4.1.6",
"universalify": "^2.0.0"
}
},
"semver": {
"version": "7.3.7",
"resolved": "https://repo.symphony.com/artifactory/api/npm/npm-virtual-dev/semver/-/semver-7.3.7.tgz",
"integrity": "sha1-EsW2Sa/b+QSXB3luIqQCiBTOUj8=",
"requires": {
"lru-cache": "^6.0.0"
}
},
"universalify": {
"version": "2.0.0",
"resolved": "https://repo.symphony.com/artifactory/api/npm/npm-virtual-dev/universalify/-/universalify-2.0.0.tgz",
"integrity": "sha1-daSYTv7cSwiXXFrrc/Uw0C3yVxc="
}
}
},
"elliptic": {
"version": "6.5.4",
"resolved": "https://repo.symphony.com/artifactory/api/npm/npm-virtual-dev/elliptic/-/elliptic-6.5.4.tgz",
@ -36953,8 +37134,7 @@
"lazy-val": {
"version": "1.0.5",
"resolved": "https://repo.symphony.com/artifactory/api/npm/npm-virtual-dev/lazy-val/-/lazy-val-1.0.5.tgz",
"integrity": "sha1-bPO59bwxzufuPjacCDK3WD3Nkj0=",
"dev": true
"integrity": "sha1-bPO59bwxzufuPjacCDK3WD3Nkj0="
},
"lazystream": {
"version": "1.0.0",
@ -37206,6 +37386,11 @@
"integrity": "sha1-yQRGkMIeBClL6qUXcS/e0fqI3pg=",
"dev": true
},
"lodash.escaperegexp": {
"version": "4.1.2",
"resolved": "https://repo.symphony.com/artifactory/api/npm/npm-virtual-dev/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz",
"integrity": "sha1-ZHYsSGGAglGKw99Mz11YhtriA0c="
},
"lodash.flatten": {
"version": "4.4.0",
"resolved": "https://repo.symphony.com/artifactory/api/npm/npm-virtual-dev/lodash.flatten/-/lodash.flatten-4.4.0.tgz",
@ -37226,8 +37411,7 @@
"lodash.isequal": {
"version": "4.5.0",
"resolved": "https://repo.symphony.com/artifactory/api/npm/npm-virtual-dev/lodash.isequal/-/lodash.isequal-4.5.0.tgz",
"integrity": "sha1-QVxEePK8wwEgwizhDtMib30+GOA=",
"dev": true
"integrity": "sha1-QVxEePK8wwEgwizhDtMib30+GOA="
},
"lodash.islength": {
"version": "4.0.1",
@ -37357,7 +37541,6 @@
"version": "6.0.0",
"resolved": "https://repo.symphony.com/artifactory/api/npm/npm-virtual-dev/lru-cache/-/lru-cache-6.0.0.tgz",
"integrity": "sha1-bW/mVw69lqr5D8rR2vo7JWbbOpQ=",
"dev": true,
"requires": {
"yallist": "^4.0.0"
}
@ -40655,8 +40838,7 @@
"sax": {
"version": "1.2.4",
"resolved": "https://repo.symphony.com/artifactory/api/npm/npm-virtual-dev/sax/-/sax-1.2.4.tgz",
"integrity": "sha1-KBYjTiN4vdxOU1T6tcqold9xANk=",
"dev": true
"integrity": "sha1-KBYjTiN4vdxOU1T6tcqold9xANk="
},
"saxes": {
"version": "3.1.11",
@ -43760,8 +43942,7 @@
"yallist": {
"version": "4.0.0",
"resolved": "https://repo.symphony.com/artifactory/api/npm/npm-virtual-dev/yallist/-/yallist-4.0.0.tgz",
"integrity": "sha1-m7knkNnA7/7GO+c1GeEaNQGaOnI=",
"dev": true
"integrity": "sha1-m7knkNnA7/7GO+c1GeEaNQGaOnI="
},
"yaml": {
"version": "1.10.2",

View File

@ -36,13 +36,20 @@
"copy": "run-os",
"copy:darwin": "ncp config 'node_modules/electron/dist/Electron.app/Contents/config'",
"copy:win32": "ncp config node_modules\\electron\\dist\\config",
"unpacked-mac": "npm run prebuild && npm run test && ./node_modules/.bin/electron-builder build --mac --dir",
"unpacked-win": "npm run prebuild && npm run test && node_modules\\.bin\\electron-builder build --win --x64 --dir"
"unpacked-mac": "npm run prebuild && npm run test && ./node_modules/.bin/electron-builder build --mac",
"unpacked-win": "npm run prebuild && npm run test && node_modules\\.bin\\electron-builder build --win --x64"
},
"build": {
"appId": "com.symphony.electron-desktop",
"appId": "com.symphony.electron_desktop",
"uninstallDisplayName": "${productName}",
"artifactName": "${productName}-${version}-${os}.${ext}",
"asar": true,
"asarUnpack": "**/*.node",
"nsis": {
"perMachine": true,
"allowToChangeInstallationDirectory": true,
"include": "build/installer.nsh"
},
"files": [
"!coverage/*",
"!installer/*",
@ -77,12 +84,19 @@
"library/libsymphonysearch.dylib",
"library/cryptoLib.dylib",
"library/dictionary"
],
"target": [
"dir",
"dmg",
"zip"
]
},
"win": {
"icon": "images/icon.ico",
"target": [
"squirrel"
"dir",
"nsis",
"zip"
]
},
"linux": {
@ -159,6 +173,7 @@
"electron-dl": "3.0.0",
"electron-fetch": "1.4.0",
"electron-log": "4.0.7",
"electron-updater": "^5.0.1",
"ffi-napi": "^4.0.3",
"filesize": "6.1.0",
"lazy-brush": "^1.0.1",
@ -171,7 +186,6 @@
},
"optionalDependencies": {
"@symphony/symphony-c9-shell": "3.14.99-37",
"auto-update": "file:auto_update",
"screen-share-indicator-frame": "git+https://github.com/symphonyoss/ScreenShareIndicatorFrame.git#v1.4.13",
"screen-snippet": "git+https://github.com/symphonyoss/ScreenSnippet2.git#9.2.2",
"winreg": "^1.2.4"

View File

@ -122,18 +122,6 @@ IF %errorlevel% neq 0 (
exit /b -1
)
@REM call %SIGNING_FILE_PATH% ..\..\dist\win-unpacked\resources\app.asar.unpacked\node_modules\auto-update\auto_update_service.exe
@REM IF %errorlevel% neq 0 (
@REM echo "Signing failed"
@REM exit /b -1
@REM )
@REM call %SIGNING_FILE_PATH% ..\..\dist\win-unpacked\resources\app.asar.unpacked\node_modules\auto-update\auto_update_helper.exe
@REM IF %errorlevel% neq 0 (
@REM echo "Signing failed"
@REM exit /b -1
@REM )
call %SIGNING_FILE_PATH% ..\..\dist\win-unpacked\Symphony.exe
IF %errorlevel% neq 0 (
echo "Signing failed"

View File

@ -38,6 +38,10 @@ jest.mock('../src/app/auto-launch-controller', () => {
};
});
jest.mock('../src/app/auto-update-handler', () => {
return {};
});
jest.mock('../src/app/config-handler', () => {
return {
CloudConfigDataTypes: {

View File

@ -5,6 +5,12 @@ import {
import { windowHandler } from '../src/app/window-handler';
import { BrowserWindow, dialog, ipcRenderer } from './__mocks__/electron';
jest.mock('../src/app/auto-update-handler', () => {
return {
updateAndRestart: jest.fn(),
};
});
jest.mock('fs', () => ({
writeFileSync: jest.fn(),
existsSync: jest.fn(() => true),

View File

@ -21,6 +21,12 @@ jest.mock('../src/app/protocol-handler', () => {
};
});
jest.mock('../src/app/auto-update-handler', () => {
return {
updateAndRestart: jest.fn(),
};
});
jest.mock('../src/app/screen-snippet-handler', () => {
return {
screenSnippet: {
@ -99,6 +105,16 @@ jest.mock('../src/app/config-handler', () => {
devToolsEnabled: true,
};
}),
getUserConfigFields: jest.fn(() => {
return {
url: 'https://symphony.com',
};
}),
getGlobalConfigFields: jest.fn(() => {
return {
url: 'https://symphony.com',
};
}),
},
};
});
@ -495,14 +511,14 @@ describe('main api handler', () => {
cmd: apiCmds.getNativeWindowHandle,
windowName: 'main',
});
expect(windows['main'].getNativeWindowHandle).toBeCalledTimes(1);
expect(windows.main.getNativeWindowHandle).toBeCalledTimes(1);
ipcMain.send(apiName.symphonyApi, {
cmd: apiCmds.getNativeWindowHandle,
windowName: 'popout1',
});
expect(windows['popout1'].getNativeWindowHandle).toBeCalledTimes(1);
expect(windows['popout2'].getNativeWindowHandle).toBeCalledTimes(0);
expect(windows.popout1.getNativeWindowHandle).toBeCalledTimes(1);
expect(windows.popout2.getNativeWindowHandle).toBeCalledTimes(0);
});
it('should call `connectC9Pipe` correctly', () => {

View File

@ -1,10 +1,11 @@
import {
calculatePercentage,
compareVersions,
formatString,
getCommandLineArgs,
getGuid,
isUrl,
throttle,
calculatePercentage,
} from '../src/common/utils';
describe('utils', () => {
@ -172,4 +173,22 @@ describe('utils', () => {
expect(calculatePercentage(500, 50)).toBe(250);
});
});
describe('isURL', () => {
it('should return true for URL string', () => {
expect(isUrl('https://corporate.symphony.com')).toBe(true);
});
it('should return false for http URLs string', () => {
expect(isUrl('http://corporate.symphony.com')).toBe(false);
});
it('should return false for URL string without https', () => {
expect(isUrl('corporate.symphony.com')).toBe(false);
});
it('should return false for not a URL string', () => {
expect(isUrl('/general')).toBe(false);
});
});
});

View File

@ -34,6 +34,8 @@ import {
zoomOut,
} from './window-utils';
import { autoUpdate } from './auto-update-handler';
export const menuSections = {
about: 'about',
edit: 'edit',
@ -280,6 +282,12 @@ export class AppMenu {
windowHandler.createAboutAppWindow(windowName);
},
},
{
click: (_item) => {
autoUpdate.checkUpdates();
},
label: i18n.t('Check for updates')(),
},
this.buildSeparator(),
{ label: i18n.t('Services')(), role: 'services' },
this.buildSeparator(),

View File

@ -1,67 +1,151 @@
import { app } from 'electron';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import electronLog from 'electron-log';
import { MacUpdater, NsisUpdater } from 'electron-updater';
import { spawn } from 'child_process';
import { isDevEnv, isElectronQA, isWindowsOS } from '../common/env';
import { isMac, isWindowsOS } from '../common/env';
import { logger } from '../common/logger';
import { isUrl } from '../common/utils';
import { whitelistHandler } from '../common/whitelist-handler';
import { config } from './config-handler';
import { windowHandler } from './window-handler';
class AutoUpdate {
private readonly tempDir: string;
private filename: string;
private logFilePath: string;
private updateUtil: string;
private updateUtilArgs: ReadonlyArray<string>;
export class AutoUpdate {
public isUpdateAvailable: boolean = false;
public autoUpdater: MacUpdater | NsisUpdater | undefined = undefined;
constructor() {
if (isElectronQA) {
this.tempDir = os.tmpdir();
} else {
this.tempDir = path.join(app.getPath('userData'), 'temp');
if (!fs.existsSync(this.tempDir)) {
fs.mkdirSync(this.tempDir);
}
if (isMac) {
this.autoUpdater = new MacUpdater(this.getUpdateUrl());
} else if (isWindowsOS) {
this.autoUpdater = new NsisUpdater(this.getUpdateUrl());
}
this.filename = '';
this.logFilePath = path.join(this.tempDir, 'auto_update.log');
if (this.autoUpdater) {
this.autoUpdater.logger = electronLog;
this.autoUpdater.autoDownload = false;
this.autoUpdater.autoInstallOnAppQuit = true;
this.updateUtil = isDevEnv
? path.join(
__dirname,
'../../../node_modules/auto-update/auto_update_helper.exe',
)
: path.join(path.dirname(app.getPath('exe')), 'auto_update_helper.exe');
this.autoUpdater.on('update-not-available', () => {
const mainWebContents = windowHandler.mainWebContents;
// Display client banner
if (mainWebContents && !mainWebContents.isDestroyed()) {
mainWebContents.send('display-client-banner', {
reason: 'autoUpdate',
action: 'update-not-available',
});
}
});
this.updateUtilArgs = [];
this.autoUpdater.on('update-available', (info) => {
const mainWebContents = windowHandler.mainWebContents;
// Display client banner
if (mainWebContents && !mainWebContents.isDestroyed()) {
mainWebContents.send('display-client-banner', {
reason: 'autoUpdate',
action: 'update-available',
data: info,
});
}
});
this.autoUpdater.on('download-progress', (info) => {
const mainWebContents = windowHandler.mainWebContents;
// Display client banner
if (mainWebContents && !mainWebContents.isDestroyed()) {
mainWebContents.send('display-client-banner', {
reason: 'autoUpdate',
action: 'download-progress',
data: info,
});
}
});
this.autoUpdater.on('update-downloaded', (info) => {
this.isUpdateAvailable = true;
const mainWebContents = windowHandler.mainWebContents;
// Display client banner
if (mainWebContents && !mainWebContents.isDestroyed()) {
mainWebContents.send('display-client-banner', {
reason: 'autoUpdate',
action: 'update-downloaded',
data: info,
});
}
});
}
}
/**
* Launch the auto update helper
* Installs the latest update quits and relaunches application
*/
public async update(filename: string) {
logger.info(`auto-update-handler: Starting auto update!`);
this.filename = filename;
if (isWindowsOS) {
this.updateUtilArgs = [
this.filename,
app.getPath('exe'),
this.logFilePath,
];
logger.info(
`auto-update-handler: Running update helper${this.updateUtil} with args ${this.updateUtilArgs}!`,
);
try {
spawn(this.updateUtil, this.updateUtilArgs);
} catch (error) {
logger.error(
`auto-update-handler: auto update failed. Error: ${error}!`,
);
}
public updateAndRestart = async (): Promise<void> => {
if (!this.isUpdateAvailable) {
return;
}
}
// Handle update and restart for macOS
if (isMac) {
windowHandler.setIsAutoUpdating(true);
}
setImmediate(() => {
if (this.autoUpdater) {
this.autoUpdater.quitAndInstall();
}
});
};
/**
* Checks for the latest updates
* @return void
*/
public checkUpdates = async (): Promise<void> => {
logger.info('auto-update-handler: Checking for updates');
if (this.autoUpdater) {
const updateCheckResult = await this.autoUpdater.checkForUpdates();
logger.info('auto-update-handler: ', updateCheckResult);
}
logger.info('auto-update-handler: After checking auto update');
};
/**
* Downloads the latest update
* @return void
*/
public downloadUpdate = async (): Promise<void> => {
logger.info('auto-update-handler: download update');
if (this.autoUpdater) {
await this.autoUpdater.downloadUpdate();
}
};
/**
* Constructs the SDA auto update end point url
*
* @return string
* @example https://corporate.symphony.com/macos/general
*/
public getUpdateUrl = (): string => {
const { url: userConfigURL } = config.getUserConfigFields(['url']);
const { url: globalConfigURL } = config.getGlobalConfigFields(['url']);
const { autoUpdateUrl, autoUpdateChannel } = config.getConfigFields([
'autoUpdateChannel',
'autoUpdateUrl',
]);
if (autoUpdateUrl && isUrl(autoUpdateUrl)) {
logger.info(
`auto-update-handler: autoUpdateUrl exists so, using it`,
autoUpdateUrl,
);
return autoUpdateUrl;
}
const url = userConfigURL ? userConfigURL : globalConfigURL;
const { subdomain, domain, tld } = whitelistHandler.parseDomain(url);
const updateUrl = `https://${subdomain}.${domain}.${tld}/${autoUpdateChannel}`;
logger.info(`auto-update-handler: using generic pod url`, updateUrl);
return updateUrl;
};
}
const autoUpdate = new AutoUpdate();

View File

@ -28,6 +28,11 @@ export const ConfigFieldsToRestart = new Set([
export interface IConfig {
url: string;
autoUpdateChannel: string;
autoUpdateUrl: string;
isAutoUpdateEnabled: boolean;
autoUpdateCheckInterval: string;
lastAutoUpdateCheckDate: string;
minimizeOnClose: CloudConfigDataTypes;
launchOnStartup: CloudConfigDataTypes;
alwaysOnTop: CloudConfigDataTypes;
@ -76,6 +81,8 @@ export interface IPodLevelEntitlements {
bringToFront: CloudConfigDataTypes;
disableThrottling: CloudConfigDataTypes;
launchOnStartup: CloudConfigDataTypes;
isAutoUpdateEnabled: boolean;
autoUpdateCheckInterval: string;
memoryThreshold: string;
ctWhitelist: string[];
podWhitelist: string[];

View File

@ -22,7 +22,7 @@ app.enableSandbox();
// need to set this explicitly if using Squirrel
// https://www.electron.build/configuration/configuration#Configuration-squirrelWindows
app.setAppUserModelId('com.symphony.electron-desktop');
app.setAppUserModelId('com.symphony.electron_desktop');
// Set user data path before app ready event
if (isDevEnv) {

View File

@ -18,6 +18,7 @@ import { logger } from '../common/logger';
import { activityDetection } from './activity-detection';
import { analytics } from './analytics-handler';
import appStateHandler from './app-state-handler';
import { autoUpdate } from './auto-update-handler';
import { closeC9Pipe, connectC9Pipe, writeC9Pipe } from './c9-pipe-handler';
import { loadC9Shell } from './c9-shell-handler';
import { getCitrixMediaRedirectionStatus } from './citrix-handler';
@ -298,9 +299,6 @@ ipcMain.on(
}
}
break;
// case apiCmds.autoUpdate:
// autoUpdate.update(arg.filename);
// break;
case apiCmds.aboutAppClipBoardData:
if (arg.clipboard && arg.clipboardType) {
clipboard.write(
@ -370,6 +368,12 @@ ipcMain.on(
case apiCmds.launchCloud9:
loadC9Shell(event.sender);
break;
case apiCmds.updateAndRestart:
autoUpdate.updateAndRestart();
break;
case apiCmds.downloadUpdate:
autoUpdate.downloadUpdate();
break;
default:
break;
}

View File

@ -143,6 +143,7 @@ export class WindowHandler {
public isCustomTitleBar: boolean;
public isWebPageLoading: boolean = true;
public isLoggedIn: boolean = false;
public isAutoUpdating: boolean = false;
public screenShareIndicatorFrameUtil: string;
private readonly defaultPodUrl: string = 'https://[POD].symphony.com';
private readonly contextIsolation: boolean;
@ -631,13 +632,16 @@ export class WindowHandler {
}
const { minimizeOnClose } = config.getConfigFields(['minimizeOnClose']);
if (minimizeOnClose === CloudConfigDataTypes.ENABLED) {
if (
minimizeOnClose === CloudConfigDataTypes.ENABLED &&
!this.isAutoUpdating
) {
event.preventDefault();
this.mainWindow.minimize();
return;
}
if (isMac) {
if (isMac && !this.isAutoUpdating) {
event.preventDefault();
this.mainWindow.hide();
return;
@ -887,6 +891,15 @@ export class WindowHandler {
this.titleBarView = titleBarView;
}
/**
* Sets whether the application is Auto Updating
*
* @param isAutoUpdating
*/
public setIsAutoUpdating(isAutoUpdating: boolean): void {
this.isAutoUpdating = isAutoUpdating;
}
/**
* Closes the window from an event emitted by the render processes
*

View File

@ -20,7 +20,7 @@ import { apiName } from '../common/api-interface';
import { isDevEnv, isLinux, isMac, isWindowsOS } from '../common/env';
import { i18n, LocaleType } from '../common/i18n';
import { logger } from '../common/logger';
import { getGuid } from '../common/utils';
import { getDifferenceInDays, getGuid, getRandomTime } from '../common/utils';
import { whitelistHandler } from '../common/whitelist-handler';
import { autoLaunchInstance } from './auto-launch-controller';
import {
@ -48,7 +48,9 @@ import {
} from './window-handler';
import { notification } from '../renderer/notification';
import { autoUpdate } from './auto-update-handler';
import { mainEvents } from './main-event-handler';
interface IStyles {
name: styleNames;
content: string;
@ -66,6 +68,11 @@ const { ctWhitelist } = config.getConfigFields(['ctWhitelist']);
// Network status check variables
const networkStatusCheckInterval = 10 * 1000;
let networkStatusCheckIntervalId;
const MAX_AUTO_UPDATE_CHECK_INTERVAL = 4 * 60 * 60 * 1000; // 4hrs
const MIN_AUTO_UPDATE_CHECK_INTERVAL = 2 * 60 * 60 * 1000; // 2hrs
let autoUpdateIntervalId;
let isNetworkMonitorInitialized = false;
const styles: IStyles[] = [];
@ -1024,11 +1031,15 @@ export const updateFeaturesForCloudConfig = async (
launchOnStartup,
memoryRefresh,
memoryThreshold,
isAutoUpdateEnabled,
autoUpdateCheckInterval,
} = config.getConfigFields([
'launchOnStartup',
'alwaysOnTop',
'memoryRefresh',
'memoryThreshold',
'isAutoUpdateEnabled',
'autoUpdateCheckInterval',
]) as IConfig;
const mainWebContents = windowHandler.getMainWebContents();
@ -1077,6 +1088,47 @@ export const updateFeaturesForCloudConfig = async (
}
}
}
// SDA auto updater
logger.info(`window-utils: initiate auto update?`, isAutoUpdateEnabled);
if (isAutoUpdateEnabled) {
if (!autoUpdateIntervalId) {
// randomised to avoid having all users getting SDA update at the same time
autoUpdateIntervalId = setInterval(async () => {
const { lastAutoUpdateCheckDate } = config.getUserConfigFields([
'lastAutoUpdateCheckDate',
]);
if (!lastAutoUpdateCheckDate || lastAutoUpdateCheckDate === '') {
logger.info(
`window-utils: lastAutoUpdateCheckDate is not set in user config file so checking for updates`,
lastAutoUpdateCheckDate,
autoUpdateCheckInterval,
);
await config.updateUserConfig({
lastAutoUpdateCheckDate: new Date().toISOString(),
});
autoUpdate.checkUpdates();
return;
}
logger.info(
`window-utils: is last check date > auto update check interval?`,
lastAutoUpdateCheckDate,
autoUpdateCheckInterval,
);
// Compare the current date and user config last auto update checked date
// and if it is greater that autoUpdateCheckInterval we check for new updates
if (
getDifferenceInDays(new Date(), new Date(lastAutoUpdateCheckDate)) >
Number(autoUpdateCheckInterval)
) {
await config.updateUserConfig({
lastAutoUpdateCheckDate: new Date().toISOString(),
});
autoUpdate.checkUpdates();
}
}, getRandomTime(MIN_AUTO_UPDATE_CHECK_INTERVAL, MAX_AUTO_UPDATE_CHECK_INTERVAL));
}
}
};
/**

View File

@ -48,7 +48,6 @@ export enum apiCmds {
showNotification = 'show-notification',
closeAllWrapperWindows = 'close-all-windows',
setZoomLevel = 'set-zoom-level',
// autoUpdate = 'auto-update',
aboutAppClipBoardData = 'about-app-clip-board-data',
closeMainWindow = 'close-main-window',
minimizeMainWindow = 'minimize-main-window',
@ -69,6 +68,8 @@ export enum apiCmds {
connectCloud9Pipe = 'connect-cloud9-pipe',
writeCloud9Pipe = 'write-cloud9-pipe',
closeCloud9Pipe = 'close-cloud9-pipe',
updateAndRestart = 'update-and-restart',
downloadUpdate = 'download-update',
}
export enum apiName {

View File

@ -275,3 +275,40 @@ export const arrayEquals = (a: string[], b: string[]) => {
a.every((val, index) => val === b[index])
);
};
/**
* Returns a random number that is between (min - max)
* if min is 4hrs and max is 12hrs then the
* returned value will be a random b/w 4 - 12 hrs
*
* @param min {number} - millisecond
* @param max {number} - millisecond
*/
export const getRandomTime = (min: number, max: number): number => {
min = Math.ceil(min);
max = Math.floor(max);
return Math.floor(Math.random() * (max - min + 1)) + min;
};
/**
* Gets the difference between 2 Dates in Days
*
* @param startDate
* @param endDate
*
* @return number
*/
export const getDifferenceInDays = (startDate: Date, endDate: Date): number => {
const msInDay = 24 * 60 * 60 * 1000;
return Math.round(
Math.abs(Number(endDate.getTime()) - Number(startDate.getTime())) / msInDay,
);
};
export const isUrl = (str: string): boolean => {
try {
return Boolean(new URL(str).protocol === 'https:');
} catch (_e) {
return false;
}
};

View File

@ -271,12 +271,6 @@
</table>
<input type="button" id="restart-app" value="Restart App" />
<!-- <hr />
<h1>Auto-update Test</h1>
Full path to MSI <input type="text" id="update-file" />
<br />
<button id="run-update">Update</button> -->
<hr />
<p>Native Window Handle:</p>
<button id="get-window-handle">
@ -337,7 +331,6 @@
getCPUUsage: 'get-cpu-usage',
checkMediaPermission: 'check-media-permission',
restartApp: 'restart-app',
// autoUpdate: 'auto-update',
getNativeWindowHandle: 'get-native-window-handle',
getCitrixMediaRedirectionStatus: 'get-citrix-media-redirection-status',
};

View File

@ -31,6 +31,7 @@
"Build expired": "Build expired",
"Cancel": "Cancel",
"Certificate Error": "Certificate Error",
"Check for updates": "Check for updates",
"Clear cache and Reload": "Clear cache and Reload",
"Close": "Close",
"ContextMenu": {

View File

@ -31,6 +31,7 @@
"Build expired": "Build expired",
"Cancel": "Cancel",
"Certificate Error": "Certificate Error",
"Check for updates": "Check for updates",
"Clear cache and Reload": "Clear cache and Reload",
"Close": "Close",
"ContextMenu": {

View File

@ -31,6 +31,7 @@
"Build expired": "Construit expiré",
"Cancel": "Annuler",
"Certificate Error": "Erreur de certificat",
"Check for updates": "Vérifier les mises à jour",
"Clear cache and Reload": "Vider le cache et rafraîchir Symphony",
"Close": "Fermer",
"ContextMenu": {

View File

@ -31,6 +31,7 @@
"Build expired": "Construit expiré",
"Cancel": "Annuler",
"Certificate Error": "Erreur de certificat",
"Check for updates": "Vérifier les mises à jour",
"Clear cache and Reload": "Vider le cache et rafraîchir Symphony",
"Close": "Fermer",
"ContextMenu": {

View File

@ -31,6 +31,7 @@
"Build expired": "期限切れのビルド",
"Cancel": "キャンセル",
"Certificate Error": "証明書のエラー",
"Check for updates": "アップデートを確認",
"Clear cache and Reload": "キャッシュをクリアしてリロードする",
"Close": "閉じる",
"ContextMenu": {

View File

@ -31,6 +31,7 @@
"Build expired": "期限切れのビルド",
"Cancel": "キャンセル",
"Certificate Error": "証明書のエラー",
"Check for updates": "アップデートを確認",
"Clear cache and Reload": "キャッシュをクリアしてリロードする",
"Close": "閉じる",
"ContextMenu": {

View File

@ -94,6 +94,8 @@ if (ssfWindow.ssf) {
registerClientBanner: ssfWindow.ssf.registerClientBanner,
launchCloud9: ssfWindow.ssf.launchCloud9,
connectCloud9Pipe: ssfWindow.ssf.connectCloud9Pipe,
updateAndRestartSDA: ssfWindow.ssf.updateAndRestart,
downloadUpdate: ssfWindow.ssf.downloadUpdate,
});
}

View File

@ -69,7 +69,7 @@ export interface ILocalObject {
analyticsEventHandler?: (arg: any) => void;
restartFloater?: (arg: IRestartFloaterData) => void;
showClientBannerCallback?: Array<
(reason: string, action: ConfigUpdateType) => void
(reason: string, action: ConfigUpdateType, data?: object) => void
>;
c9PipeEventCallback?: (event: string, arg?: any) => void;
c9MessageCallback?: (status: IShellStatus) => void;
@ -854,6 +854,24 @@ export class SSFApi {
cmd: apiCmds.launchCloud9,
});
}
/**
* Allows JS to install new update and restart SDA
*/
public updateAndRestart(): void {
ipcRenderer.send(apiName.symphonyApi, {
cmd: apiCmds.updateAndRestart,
});
}
/**
* Allows JS to download the latest SDA updates
*/
public downloadUpdate(): void {
ipcRenderer.send(apiName.symphonyApi, {
cmd: apiCmds.downloadUpdate,
});
}
}
/**
@ -1091,6 +1109,10 @@ local.ipcRenderer.on('notification-actions', (_event, args) => {
local.ipcRenderer.on('display-client-banner', (_event, args) => {
if (local.showClientBannerCallback) {
for (const callback of local.showClientBannerCallback) {
if (args.data) {
callback(args.reason, args.action, args.data);
return;
}
callback(args.reason, args.action);
}
}