11# -*- coding: utf-8 -*-
2- """
3- * TencentBlueKing is pleased to support the open source community by making 蓝鲸智云-蓝鲸 PaaS 平台(BlueKing-PaaS) available.
4- * Copyright (C) 2017-2021 THL A29 Limited, a Tencent company. All rights reserved.
5- * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License.
6- * You may obtain a copy of the License at http://opensource.org/licenses/MIT
7- * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
8- * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
9- * specific language governing permissions and limitations under the License.
10- """
2+ # TencentBlueKing is pleased to support the open source community by making
3+ # 蓝鲸智云 - PaaS 平台 (BlueKing - PaaS System) available.
4+ # Copyright (C) 2017 THL A29 Limited, a Tencent company. All rights reserved.
5+ # Licensed under the MIT License (the "License"); you may not use this file except
6+ # in compliance with the License. You may obtain a copy of the License at
7+ #
8+ # http://opensource.org/licenses/MIT
9+ #
10+ # Unless required by applicable law or agreed to in writing, software distributed under
11+ # the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
12+ # either express or implied. See the License for the specific language governing permissions and
13+ # limitations under the License.
14+ #
15+ # We undertake not to change the open source license (MIT license) applicable
16+ # to the current version of the project delivered to anyone in the future.
1117import abc
1218import fnmatch
1319import json
1622import shutil
1723import stat
1824import sys
25+ import threading
1926import time
2027from dataclasses import dataclass , field
2128from os import PathLike
@@ -56,7 +63,8 @@ class CopyLinker(FileLinker):
5663 """Directly copy"""
5764
5865 def link (self , src_file , dst_file ):
59- shutil .copyfile (src_file , dst_file , follow_symlinks = False )
66+ # Use copy2 to copy file stats such as mtime
67+ shutil .copy2 (src_file , dst_file , follow_symlinks = False )
6068
6169 def unlink (self , dst_file ):
6270 Path (dst_file ).unlink ()
@@ -76,7 +84,8 @@ def set_file_permission(self, path: Path, mode):
7684 def link (self , src_file , dst_file ):
7785 # make sure file is writable before copy
7886 self .set_file_permission (dst_file , self .writable_mode )
79- shutil .copyfile (src_file , dst_file , follow_symlinks = False )
87+ # Use copy2 to copy file stats such as mtime
88+ shutil .copy2 (src_file , dst_file , follow_symlinks = False )
8089 # make sure file is read-only to avoid unexpected modifies
8190 self .set_file_permission (dst_file , self .read_only_mode )
8291
@@ -221,7 +230,7 @@ def activate(ctx, edition_name_in_pos, edition_name, linker_type):
221230 we found that solution is too difficult and fragile to implement because we need our namespace
222231 packages to be nested.
223232
224- So currently we are using a simpler(and stupidier ) approach: sync all source files under the
233+ So currently we are using a simpler(and stupider ) approach: sync all source files under the
225234 specified edition directory to the main package.
226235 """
227236 settings = {}
@@ -233,11 +242,11 @@ def activate(ctx, edition_name_in_pos, edition_name, linker_type):
233242
234243 edition_name = edition_name_in_pos or edition_name
235244 if not edition_name :
236- logger .critical ('Must provide edtion name via position argument or "--edition-name"' )
245+ logger .critical ('Must provide edition name via position argument or "--edition-name"' )
237246 sys .exit (1 )
238247
239248 try :
240- migrator = EditionFileMigrater (config , edition_name )
249+ migrator = EditionFileMigrator (config , edition_name )
241250 migrator .migrate ()
242251 except RuntimeError :
243252 logger .exception ("unable to finish the migration" )
@@ -348,8 +357,8 @@ def list_managed_files(self) -> Iterator[Path]:
348357 yield self .metadata_path
349358
350359
351- class EditionFileMigrater :
352- """The file migrater """
360+ class EditionFileMigrator :
361+ """The file migrator """
353362
354363 def __init__ (self , config : Configuration , edition_name : str ):
355364 self .config = config
@@ -395,9 +404,7 @@ def should_reset(self, last_metadata: EditionMetaData):
395404 """If the migrator should reset all migrated states"""
396405 if last_metadata .edition_name != self .edition_name :
397406 return True
398- if last_metadata .linker_type != self .config .get_linker_type ():
399- return True
400- return False
407+ return last_metadata .linker_type != self .config .get_linker_type ()
401408
402409
403410@main .command ()
@@ -412,7 +419,7 @@ def reset_project(config: Configuration):
412419 """Reset all migration states.
413420
414421 - Remove metadata and related files
415- - Unlink all migarated files(from metadata file)
422+ - Unlink all migrated files(from metadata file)
416423 """
417424 metadata = load_current_metadata (config )
418425 if metadata is None :
@@ -421,7 +428,7 @@ def reset_project(config: Configuration):
421428
422429 logger .info ("Resetting project." )
423430 for filepath in metadata .list_managed_files ():
424- logger .debug (f"Unlinking file { filepath } ." )
431+ logger .info (f"Unlinking file { filepath } ." )
425432 try :
426433 filepath .unlink ()
427434 except FileNotFoundError :
@@ -448,7 +455,7 @@ class SyncResult:
448455class DirectorySyncer :
449456 """A simple directory syncer"""
450457
451- ignore_patterns = ("*.pyc" , "*.pyo" , "CVS" , "tmp" , ".git" , ".svn" , "__pycache__" )
458+ ignore_patterns = ("*.pyc" , "*.pyo" , "CVS" , "tmp" , ".git" , ".svn" , "__pycache__" , ".mypy_cache" )
452459
453460 def __init__ (self , file_linker : FileLinker ):
454461 self .file_linker = file_linker
@@ -491,8 +498,11 @@ def sync_files(
491498 dst_file = dst_path / filename
492499
493500 rel_file = Path (rel_path ) / filename
494- logger .debug (f"Linking file { rel_file } ..." )
495- self .file_linker .link (src_file , dst_file )
501+
502+ if not self .are_files_identical (src_file , dst_file ):
503+ logger .info (f"Linking file { rel_file } ..." )
504+ self .file_linker .link (src_file , dst_file )
505+
496506 result .added_files .add (dst_file )
497507 result .added_files_relative .add (rel_file )
498508
@@ -510,6 +520,15 @@ def sync_files(
510520 result .deleted_files .add (file_path )
511521 return result
512522
523+ @staticmethod
524+ def are_files_identical (src_file , dst_file ):
525+ """Check if two files are identical by comparing size and mtime."""
526+ if not (src_file .exists () and dst_file .exists ()):
527+ return False
528+ src_stat = src_file .stat ()
529+ dst_stat = dst_file .stat ()
530+ return src_stat .st_size == dst_stat .st_size and src_stat .st_mtime == dst_stat .st_mtime
531+
513532
514533@main .command ()
515534@click .pass_context
@@ -562,7 +581,7 @@ def help(ctx):
562581@click .pass_context
563582def develop (ctx ):
564583 """Enter develop mode, auto trigger edition activate procedure after files under current
565- edtion directory have been modified.
584+ edition directory have been modified.
566585 """
567586 config = get_configuration_or_quit (ctx .obj ["settings_path" ])
568587 project_root = config .get_project_root ()
@@ -587,25 +606,38 @@ def develop(ctx):
587606
588607
589608class EditionDevelopEventHandler (FileSystemEventHandler ):
590- """Handler will auto re-activate edition when it's content changes"""
609+ """Handler will auto re-activate edition when its content changes"""
610+
611+ debounce_delay = 0.5
591612
592613 def __init__ (self , config : Configuration , edition_name : str ):
593614 self .config = config
594615 self .edition_name = edition_name
595616 super ().__init__ ()
596617
597- def on_any_event ( self , event ):
598- logger . debug ( f"Event { event } detected, will re-activate project edtion" )
618+ # A debounce time to avoid unnecessary reactivation
619+ self . _debounce_timer : Optional [ threading . Timer ] = None
599620
600- logger .info ("Going to re-activate edtion..." )
601- migrator = EditionFileMigrater (self .config , self .edition_name )
621+ def _reactivate (self ):
622+ logger .debug ("Begin reactivate edition..." )
623+ migrator = EditionFileMigrator (self .config , self .edition_name )
602624 try :
603625 migrator .migrate ()
604626 except RuntimeError :
605627 logger .critical ("unable to finish the migration" )
606- logger .info (f"Edition { self .edition_name } re-activated, linker is { self .config .get_linker_type ()} " )
628+ logger .info (f"Edition { self .edition_name } reactivated, linker: { self .config .get_linker_type ()} " )
629+
630+ def on_any_event (self , event ):
631+ # Ignore events that are not relevant
632+ if event .event_type not in {"modified" , "created" , "deleted" }:
633+ return
634+
635+ logger .debug (f"Event { event } detected, scheduling debounce reactivation" )
636+ if self ._debounce_timer :
637+ self ._debounce_timer .cancel ()
607638
608- logger .info ("Inform project dev server to reload." )
639+ self ._debounce_timer = threading .Timer (self .debounce_delay , self ._reactivate )
640+ self ._debounce_timer .start ()
609641
610642
611643if __name__ == "__main__" :
0 commit comments