Fix environment variables to be consistently set when using --system or --permanent (#515)

Also switch PostMessage to SendMessageWithTimeout and only send one if
environment variables actually changed.

Also, enable and fix --permanent unit tests.
diff --git a/.circleci/config.yml b/.circleci/config.yml
index 4a2b452..6a5e16f 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -91,12 +91,7 @@
           name: --permanent test
           shell: powershell.exe
           command: |
-            try {
-              scripts/test_permanent.ps1
-            } catch {
-              Write-Host "`n Error Message: [$($_.Exception.Message)"] -ForegroundColor Red
-              Write-Host "`n Line Number: "$_.InvocationInfo.ScriptLineNumber -ForegroundColor Red
-            }
+            scripts/test_permanent.ps1
 
   build-docker-image:
     executor: bionic
diff --git a/emsdk.py b/emsdk.py
index ee5452d..5032244 100644
--- a/emsdk.py
+++ b/emsdk.py
@@ -299,6 +299,7 @@
 
 
 def win_set_environment_variable_direct(key, value, system=True):
+  folder = None
   try:
     if system:
       # Read globally from ALL USERS section.
@@ -308,23 +309,22 @@
       folder = winreg.OpenKeyEx(winreg.HKEY_CURRENT_USER, 'Environment', 0, winreg.KEY_ALL_ACCESS)
     winreg.SetValueEx(folder, key, 0, winreg.REG_EXPAND_SZ, value)
     debug_print('Set key=' + key + ' with value ' + value + ' in registry.')
+    return True
   except Exception as e:
     # 'Access is denied.'
-    if e.args[0] == 5:
+    if e.args[3] == 5:
       exit_with_error('Error! Failed to set the environment variable \'' + key + '\'! Setting environment variables permanently requires administrator access. Please rerun this command with administrative privileges. This can be done for example by holding down the Ctrl and Shift keys while opening a command prompt in start menu.')
     errlog('Failed to write environment variable ' + key + ':')
     errlog(str(e))
-    folder.Close()
-    return None
-
-  folder.Close()
-  HWND_BROADCAST = ctypes.wintypes.HWND(0xFFFF)  # win32con.HWND_BROADCAST == 65535
-  WM_SETTINGCHANGE = 0x001A  # win32con.WM_SETTINGCHANGE == 26
-  ctypes.windll.user32.SendMessageA(HWND_BROADCAST, WM_SETTINGCHANGE, 0, 'Environment')
+    return False
+  finally:
+    if folder is not None:
+      folder.Close()
 
 
-def win_get_environment_variable(key, system=True):
+def win_get_environment_variable(key, system=True, fallback=True):
   try:
+    folder = None
     try:
       if system:
         # Read globally from ALL USERS section.
@@ -334,22 +334,24 @@
         folder = winreg.OpenKey(winreg.HKEY_CURRENT_USER, 'Environment')
       value = str(winreg.QueryValueEx(folder, key)[0])
     except Exception:
-      # PyWin32 is not available - read via os.environ. This has the drawback
+      # If reading registry fails for some reason - read via os.environ. This has the drawback
       # that expansion items such as %PROGRAMFILES% will have been expanded, so
       # need to be precise not to set these back to system registry, or
       # expansion items would be lost.
-      return os.environ[key]
+      if fallback:
+        return os.environ[key]
+      return None
+    finally:
+      if folder is not None:
+        folder.Close()
+
   except Exception as e:
+    # this catch is if both the registry key threw an exception and the key is not in os.environ
     if e.args[0] != 2:
       # 'The system cannot find the file specified.'
       errlog('Failed to read environment variable ' + key + ':')
       errlog(str(e))
-    try:
-      folder.Close()
-    except Exception:
-      pass
     return None
-  folder.Close()
   return value
 
 
@@ -367,11 +369,11 @@
 
 def win_set_environment_variable(key, value, system=True):
   debug_print('set ' + str(key) + '=' + str(value) + ', in system=' + str(system), file=sys.stderr)
-  previous_value = win_get_environment_variable(key, system)
+  previous_value = win_get_environment_variable(key, system, fallback=False)
   if previous_value == value:
     debug_print('  no need to set, since same value already exists.')
     # No need to elevate UAC for nothing to set the same value, skip.
-    return
+    return False
 
   if not value:
     try:
@@ -382,13 +384,12 @@
       debug_print(str(cmd))
       value = subprocess.call(cmd, stdout=subprocess.PIPE)
     except Exception:
-      return
-    return
+      return False
+    return True
 
   try:
-    if system:
-      win_set_environment_variable_direct(key, value, system)
-      return
+    if win_set_environment_variable_direct(key, value, system):
+      return True
     # Escape % signs so that we don't expand references to environment variables.
     value = value.replace('%', '^%')
     if len(value) >= 1024:
@@ -398,15 +399,53 @@
     retcode = subprocess.call(cmd, stdout=subprocess.PIPE)
     if retcode != 0:
       errlog('ERROR! Failed to set environment variable ' + key + '=' + value + '. You may need to set it manually.')
+    else:
+      return True
   except Exception as e:
     errlog('ERROR! Failed to set environment variable ' + key + '=' + value + ':')
     errlog(str(e))
     errlog('You may need to set it manually.')
 
+  return False
+
+
+def win_set_environment_variables(env_vars_to_add, system):
+  if not env_vars_to_add:
+    return
+
+  changed = False
+
+  for key, value in env_vars_to_add:
+    if win_set_environment_variable(key, value, system):
+      if not changed:
+        changed = True
+        print('Setting global environment variables:')
+
+      print(key + ' = ' + value)
+
+  if not changed:
+    print('Global environment variables up to date')
+    return
+
+  # if changes were made then we need to notify other processes
+  try:
+    HWND_BROADCAST = ctypes.wintypes.HWND(0xFFFF)  # win32con.HWND_BROADCAST == 65535
+    WM_SETTINGCHANGE = 0x001A  # win32con.WM_SETTINGCHANGE == 26
+    SMTO_BLOCK = 0x0001  # win32con.SMTO_BLOCK == 1
+    ctypes.windll.user32.SendMessageTimeoutA(
+      HWND_BROADCAST,    # hWnd: notify everyone
+      WM_SETTINGCHANGE,  # Msg: registry changed
+      0,                 # wParam: Must be 0 when setting changed is sent by users
+      'Environment',     # lParam: Specifically environment variables changed
+      SMTO_BLOCK,        # fuFlags: Wait for message to be sent or timeout
+      100)               # uTimeout: 100ms
+  except Exception as e:
+    errlog('SendMessageTimeout failed with error: ' + str(e))
+
 
 def win_delete_environment_variable(key, system=True):
   debug_print('win_delete_environment_variable(key=' + key + ', system=' + str(system) + ')')
-  win_set_environment_variable(key, None, system)
+  return win_set_environment_variable(key, None, system)
 
 
 # Returns the absolute pathname to the given path inside the Emscripten SDK.
@@ -1770,14 +1809,6 @@
           return False
     return True
 
-  def win_activate_env_vars(self, system):
-    if WINDOWS:
-      envs = self.activated_environment()
-      for env in envs:
-        key, value = parse_key_value(env)
-
-        win_set_environment_variable(key, value, system)
-
   # If this tool can be installed on this system, this function returns True.
   # Otherwise, this function returns a string that describes the reason why this
   # tool is not available.
@@ -2416,20 +2447,14 @@
   # calling shell environment.  On other platform `source emsdk_env.sh` is
   # required.
   if WINDOWS:
-    env_string = construct_env(tools_to_activate)
+    # always set local environment variables since permanently activating will only set the registry settings and
+    # will not affect the current session
+    env_vars_to_add = get_env_vars_to_add(tools_to_activate)
+    env_string = construct_env_with_vars(env_vars_to_add)
     write_set_env_script(env_string)
 
-  # Apply environment variables to global all users section.
-  if WINDOWS and permanently_activate:
-    # Individual env. vars
-    for tool in tools_to_activate:
-      tool.win_activate_env_vars(system=system)
-
-    # PATH variable
-    newpath, added_items = adjusted_path(tools_to_activate, system=system)
-    # Are there any actual changes?
-    if newpath != os.environ['PATH']:
-      win_set_environment_variable('PATH', newpath, system=system)
+    if permanently_activate:
+      win_set_environment_variables(env_vars_to_add, system)
 
   return tools_to_activate
 
@@ -2504,22 +2529,14 @@
   return (separator.join(whole_path), new_emsdk_tools)
 
 
-def construct_env(tools_to_activate):
-  env_string = ''
+def get_env_vars_to_add(tools_to_activate):
+  env_vars_to_add = []
+
   newpath, added_path = adjusted_path(tools_to_activate)
 
   # Don't bother setting the path if there are no changes.
   if os.environ['PATH'] != newpath:
-    if POWERSHELL:
-      env_string += '$env:PATH="' + newpath + '"\n'
-    elif CMD:
-      env_string += 'SET PATH=' + newpath + '\n'
-    elif CSH:
-      env_string += 'setenv PATH "' + newpath + '";\n'
-    elif BASH:
-      env_string += 'export PATH="' + newpath + '";\n'
-    else:
-      assert False
+    env_vars_to_add += [('PATH', newpath)]
 
     if added_path:
       errlog('Adding directories to PATH:')
@@ -2528,11 +2545,8 @@
       errlog('')
 
   # A core variable EMSDK points to the root of Emscripten SDK directory.
-  env_vars = [('EMSDK', to_unix_path(emsdk_path()))]
-
-  em_config_path = os.path.normpath(dot_emscripten_path())
-  if to_unix_path(os.environ.get('EM_CONFIG', '')) != to_unix_path(em_config_path):
-    env_vars += [('EM_CONFIG', em_config_path)]
+  env_vars_to_add += [('EMSDK', to_unix_path(emsdk_path()))]
+  env_vars_to_add += [('EM_CONFIG', os.path.normpath(dot_emscripten_path()))]
 
   for tool in tools_to_activate:
     config = tool.activated_config()
@@ -2540,27 +2554,40 @@
       # For older emscripten versions that don't use this default we export
       # EM_CACHE.
       em_cache_dir = os.path.join(config['EMSCRIPTEN_ROOT'], 'cache')
-      env_vars += [('EM_CACHE', em_cache_dir)]
+      env_vars_to_add += [('EM_CACHE', em_cache_dir)]
     envs = tool.activated_environment()
     for env in envs:
       key, value = parse_key_value(env)
       value = to_native_path(tool.expand_vars(value))
-      env_vars += [(key, value)]
+      env_vars_to_add += [(key, value)]
 
-  if env_vars:
+  return env_vars_to_add
+
+
+def construct_env(tools_to_activate):
+  return construct_env_with_vars(get_env_vars_to_add(tools_to_activate))
+
+
+def construct_env_with_vars(env_vars_to_add):
+  env_string = ''
+  if env_vars_to_add:
     errlog('Setting environment variables:')
-    for key, value in env_vars:
-      if POWERSHELL:
-        env_string += '$env:' + key + '="' + value + '"\n'
-      elif CMD:
-        env_string += 'SET ' + key + '=' + value + '\n'
-      elif CSH:
-        env_string += 'setenv ' + key + ' "' + value + '";\n'
-      elif BASH:
-        env_string += 'export ' + key + '="' + value + '";\n'
-      else:
-        assert False
-      if 'EMSDK_PYTHON' in env_vars:
+
+    for key, value in env_vars_to_add:
+      # Don't set env vars which are already set to the correct value.
+      if key not in os.environ or to_unix_path(os.environ[key]) != to_unix_path(value):
+        errlog(key + ' = ' + value)
+        if POWERSHELL:
+          env_string += '$env:' + key + '="' + value + '"\n'
+        elif CMD:
+          env_string += 'SET ' + key + '=' + value + '\n'
+        elif CSH:
+          env_string += 'setenv ' + key + ' "' + value + '"\n'
+        elif BASH:
+          env_string += 'export ' + key + '="' + value + '"\n'
+        else:
+          assert False
+      if 'EMSDK_PYTHON' in env_vars_to_add:
         # When using our bundled python we never want the user's
         # PYTHONHOME or PYTHONPATH
         # See https://github.com/emscripten-core/emsdk/issues/598
@@ -2579,7 +2606,6 @@
         else:
           assert False
 
-      errlog(key + ' = ' + value)
   return env_string
 
 
diff --git a/scripts/test_permanent.ps1 b/scripts/test_permanent.ps1
index 01ec1f1..4c9da19 100644
--- a/scripts/test_permanent.ps1
+++ b/scripts/test_permanent.ps1
@@ -33,24 +33,24 @@
 
 $path_split = $PATH_USER.Split(';')
 
-$EMSDK_Path_USER = $path_split | Where-Object { $_ -match 'emsdk' }
+$EMSDK_Path_USER = $path_split | Where-Object { $_ -like "$repo_root*" }
 if (!$EMSDK_Path_USER) {
     throw "No path is added!"
 }
-$EMSDK_NODE_Path_USER = $path_split | Where-Object { $_ -match 'emsdk\\node' }
+$EMSDK_NODE_Path_USER = $path_split | Where-Object { $_ -like "$repo_root\node*" }
 if (!$EMSDK_NODE_Path_USER) {
-    throw "emsdk\node is not added to path."
+    throw "$repo_root\\node is not added to path."
 }
-$EMSDK_PYTHON_Path_USER = $path_split | Where-Object { $_ -match 'emsdk\\python' }
+$EMSDK_PYTHON_Path_USER = $path_split | Where-Object { $_ -like "$repo_root\python*" }
 if (!$EMSDK_PYTHON_Path_USER) {
-    throw "emsdk\python is not added to path."
+    throw "$repo_root\\python is not added to path."
 }
-$EMSDK_JAVA_Path_USER = $path_split | Where-Object { $_ -match 'emsdk\\java' }
+$EMSDK_JAVA_Path_USER = $path_split | Where-Object { $_ -like "$repo_root\java*" }
 if (!$EMSDK_JAVA_Path_USER) {
-    throw "emsdk\java is not added to path."
+    throw "$repo_root\\java is not added to path."
 }
 
-$EMSDK_UPSTREAM_Path_USER = $path_split | Where-Object { $_ -match 'emsdk\\upstream\\emscripten' }
+$EMSDK_UPSTREAM_Path_USER = $path_split | Where-Object { $_ -like "$repo_root\upstream\emscripten*" }
 if (!$EMSDK_UPSTREAM_Path_USER) {
-    throw "emsdk\upstream\emscripten is not added to path."
+    throw "$repo_root\\upstream\emscripten is not added to path."
 }