using System; using System.Collections.Generic; using System.Text; using System.IO; using System.Threading; using CommonLang; using CommonLang.File; using CommonLang.Concurrent; using CommonLang.Log; namespace MPQ.Updater { public class MPQUpdater : IDisposable { private CommonLang.Log.Logger log = LoggerFactory.GetLogger("MPQUpdater"); public static string ZIP_EXT = ".z"; public static string MPQ_EXT = ".mpq"; public static string VERSION_TEXT_BEGIN = "BEGIN"; public static string VERSION_TEXT_END = "END"; private static UTF8Encoding UTF8 = new UTF8Encoding(false); private static readonly char Separator = Path.DirectorySeparatorChar; private readonly MPQDirver driver; private Uri version_url; private FileInfo version_file; private string[] url_roots; private string version_text = ""; private MPQUpdaterListener listener; private SyncMessageQueue events = new SyncMessageQueue(); private bool is_running = false; private bool is_exit = false; private long last_update_time; private long last_update_download_bytes = 0; private long last_update_unzip_bytes = 0; private long current_download_speed_BPS = 0; private long current_unzip_spped_BPS = 0; private DirectoryInfo stream_root; private DirectoryInfo save_root; private bool is_check_md5 = false; private Thread workthread; private long total_unzip_bytes = 0; private long total_download_bytes = 0; private AtomicLong current_unzip_bytes = new AtomicLong(0); private AtomicLong current_download_bytes = new AtomicLong(0); private List remoteFiles = new List(); private AtomicReference status = new AtomicReference(Status.NA); private long _mSaveAvaliable = 0L; public DirectoryInfo LocalSaveRoot { get { return save_root; } } public MPQUpdater(MPQDirver d = null) { if (d == null) { d = new MPQDirver(); } this.driver = d; } /// /// 创建自动更新程序 /// /// 远程列表文件地址(http://localhost/xxxx/version.txt) /// 远程下载地址根目录(多个备选) /// 下载资源类型后缀 /// 本地存储目录 /// 本地包内资源目录 /// 是否验证MD5 /// 监听器 public void Init( Uri remote_version_url, string[] remote_version_prefix, string version_suffix, DirectoryInfo local_save_root, DirectoryInfo local_bundle_root, string mpq_txt, bool validate_md5, MPQUpdaterListener listener) { if (remote_version_prefix.Length == 0) { throw new Exception("remote_version_prefix length is 0 !!!"); } if (local_save_root.Equals(local_bundle_root)) { throw new Exception("save root cannot be bundle root !!!"); } stream_root = local_bundle_root; this.save_root = local_save_root; this.version_file = new FileInfo(Path.GetFullPath(local_save_root.FullName + Separator + version_suffix)); this.url_roots = remote_version_prefix; this.version_url = remote_version_url; this.is_check_md5 = validate_md5; versionText = mpq_txt; this.listener = listener; this.DownloadTimeoutSEC = 15; } public void Init( string[] remote_version_prefix, string version_suffix, DirectoryInfo local_save_root, DirectoryInfo local_bundle_root, bool validate_md5, MPQUpdaterListener listener) { if (remote_version_prefix.Length == 0) { throw new Exception("remote_version_prefix length is 0 !!!"); } if (local_save_root.Equals(local_bundle_root)) { throw new Exception("save root cannot be bundle root !!!"); } this.save_root = local_save_root;// new DirectoryInfo(Path.GetFullPath(local_save_root)); this.stream_root = local_bundle_root;// new DirectoryInfo(Path.GetFullPath(local_bundle_root)); this.version_file = new FileInfo(Path.GetFullPath(local_save_root.FullName + Separator + version_suffix)); this.url_roots = remote_version_prefix; this.version_url = new Uri(url_roots[0] + "/" + version_suffix); this.is_check_md5 = validate_md5; this.listener = listener; this.DownloadTimeoutSEC = 30; } Action checkMpqUpdateCb = null; public void CheckNeedUpdate(bool userMpq, Action checkCb) { if(userMpq) { checkMpqUpdateCb = checkCb; var thread = new Thread(CheckThread); thread.IsBackground = true; status.Value = Status.Checking; thread.Start(); } else { IsLoadFinised = true; status.Value = Status.Done; checkCb(0); } } public float CheckProgress = 0f; public string versionText; public long needSize = 0; public Queue need_files = new Queue(); void CheckThread() { remoteFiles.Clear(); HashMap _zip_files = new HashMap(); HashMap _bin_files = new HashMap(); if(string.IsNullOrEmpty(versionText)) { versionText = CommonNetwork.Http.WebClient.DownloadString(version_url, UTF8); } if (versionText.StartsWith(VERSION_TEXT_BEGIN) && versionText.EndsWith(VERSION_TEXT_END)) { char[] spc = { ':' }; string[] lines = versionText.Split('\n'); int cur = 0; int len = lines.Length; foreach (string line in lines) { string[] kv = line.Split(spc, 3); if (kv.Length == 3) { string key = kv[2].Trim().Replace('\\', Separator); string md5 = kv[0].Trim(); uint fsize = uint.Parse(kv[1].Trim()); //如果peristerpath有此文件 var _savePath = Path.GetFullPath(save_root.FullName + Separator + key); FileInfo localFile = new FileInfo(_savePath); RemoteFileInfo localinf = new RemoteFileInfo(md5, fsize, key, localFile); remoteFiles.Add(localinf); if (localFile.Exists) { if (localFile.Length == fsize) { if(key.EndsWith(ZIP_EXT)) { // 如果是没有解压的zip ,就直接放在need_files 进入下一步 need_files.Enqueue(localinf); } cur++; CheckProgress = cur * 1f / len; continue; } else if (localFile.Length > fsize) { localFile.Delete(); } } if (key.ToLower().EndsWith(ZIP_EXT)) { _zip_files[localFile.Name] = localinf; needSize += localinf.size; } else if (!key.ToLower().EndsWith(MPQ_EXT)) { _bin_files[localFile.Name] = localinf; needSize += localinf.size; } else { cur++; CheckProgress = cur * 1f / len; } } } List trydownlist = new List(); trydownlist.AddRange(_zip_files.Values); trydownlist.AddRange(_bin_files.Values); foreach (RemoteFileInfo inf in trydownlist) { // 确认对应的MPQ文件是否完整 // if (inf.key.EndsWith(ZIP_EXT)) { string mpq_name = inf.key.TrimEnd('.', 'z'); var mpq_file = remoteFiles.Find(data => data.key == mpq_name); if (mpq_file != null && mpq_file.IsCompletion()) { cur++; CheckProgress = cur * 1f / len; needSize -= inf.size; inf.file.Delete(); continue; } } // 如果已经存在未下载完成的 if (inf.file.Exists) { needSize -= inf.size; needSize += inf.NeedLoadSize(); } need_files.Enqueue(inf); cur++; CheckProgress = cur * 1f / len; } } var mbytes = (float)Math.Round(needSize / (float)1048576, 2); if (checkMpqUpdateCb != null) { if (mbytes == 0) { if(need_files.Count == 0) { IsLoadFinised = true; status.Value = Status.Done; } else { IsLoadFinised = true; status.Value = Status.Unzipping; } } checkMpqUpdateCb(mbytes); } } /** * 开始自动更新 */ public void Start() { _mSaveAvaliable = this.driver.GetAvaliableSpace(this.save_root.FullName); this.workthread = new Thread(new ThreadStart(Run)); this.workthread.Name = "MPQUpdater"; this.workthread.Start(); } public bool HaveEnoughSpace(float size) { var uSize = (long)Math.Ceiling(size * 1048576); var space = driver.GetAvaliableSpace(save_root.FullName); return space > uSize; } public void Update() { long now_time = Environment.TickCount; if (Math.Abs(now_time - last_update_time) >= 1000) { double delta_time = now_time - last_update_time; double delta_download = current_download_bytes.Value - last_update_download_bytes; double delta_unzip = current_unzip_bytes.Value - last_update_unzip_bytes; this.current_download_speed_BPS = (long)(delta_download / delta_time * 1000.0); this.current_unzip_spped_BPS = (long)(delta_unzip / delta_time * 1000.0); this.last_update_time = now_time; this.last_update_download_bytes = current_download_bytes.Value; this.last_update_unzip_bytes = current_unzip_bytes.Value; } events.ProcessMessages(DoEvent); } private void QueueEvent(MPQUpdaterEvent evt) { listener.onEvent(this, evt); } public string GetThreadSattus() { log.Log(workthread.IsAlive.ToString()); return workthread.ThreadState.ToString(); } public void RunTry() { Start(); } private void DoEvent(MPQUpdaterEvent e) { listener.onEvent(this, e); } public void Run() { try { new RunTask(this).Run(); } catch (Exception err) { log.Error(err.Message, err); } } //---------------------------------------------------------------------------------------------------------- public enum Status { Error = -1, NA = 0, Checking = 1, Downloading = 2, Unzipping = 3, Done = 4, } class RunTask { private CommonLang.Log.Logger log; private readonly MPQUpdater updater; private Queue _zip_files = new Queue(); public RunTask(MPQUpdater updater) { this.log = updater.log; this.updater = updater; } public void Run() { updater.is_running = true; try { // 先删除 资源服上 没有的资源 List exists = CFiles.listAllFiles(updater.save_root); foreach (FileInfo ff in exists) { var remoteAsset = updater.remoteFiles.Find(data => data.file.Name == ff.Name); if (remoteAsset == null) { ff.Delete(); } } if (!updater.IsLoadFinised) { // 下载资源 updater.status.Value = Status.Downloading; } if (run_download_zips()) { return; } updater.IsLoadFinised = true; // 解压资源 updater.status.Value = Status.Unzipping; if (run_unzip()) { return; } updater.status.Value = Status.Done; } catch (Exception err) { log.DebugFormat(err.Message, err); updater.QueueEvent(new MPQUpdaterEvent(MPQUpdaterEvent.TYPE_ERROR, "ERROR : " + err.Message, err)); } finally { updater.is_running = false; } } private bool run_download_zips() { var needFiles = updater.need_files; while(needFiles.Count > 0) { if (updater.is_exit) return true; var inf = needFiles.Dequeue(); try { long need_bytes = inf.size; long exist_size = 0; // 如果已经存在未下载完成的 // if (inf.file.Exists) { exist_size = inf.file.Length; need_bytes = inf.size - exist_size; if (need_bytes == 0) { if (inf.key.EndsWith(ZIP_EXT)) { var str = inf.key.TrimEnd('.', 'z'); var file = updater.remoteFiles.Find(data => data.key == str); if (file != null) { updater.total_unzip_bytes += file.size; _zip_files.Enqueue(inf); } } continue; } } else { CFiles.createFile(inf.file); } // 如果需要的空间无法满足 // // long save_avaliable = updater.driver.GetAvaliableSpace(updater.save_root.FullName); long save_avaliable = updater._mSaveAvaliable; if (need_bytes >= save_avaliable) { needFiles.Enqueue(inf); updater.QueueEvent(new MPQUpdaterEvent( MPQUpdaterEvent.TYPE_NOT_ENOUGH_SPACE, "Space not available!" )); return true; } try { log.Info(string.Format("开始下载: {0} ", inf.key)); if (run_download_single(inf, exist_size, need_bytes)) { needFiles.Enqueue(inf); } else { if (inf.key.EndsWith(ZIP_EXT)) { var str = inf.key.TrimEnd('.', 'z'); var file = updater.remoteFiles.Find(data => data.key == str); if (file != null) { updater.total_unzip_bytes += file.size; _zip_files.Enqueue(inf); } } } } catch (Exception err) { log.Error(err); needFiles.Enqueue(inf); } } catch (Exception err) { needFiles.Enqueue(inf); log.ErrorFormat(err.Message, err); updater.QueueEvent( new MPQUpdaterEvent(MPQUpdaterEvent.TYPE_ERROR, string.Format("DOWNLOAD_ZIPS : {0} : {1}", inf.file, err.Message), err)); return true; } } return false; } private bool run_download_single(RemoteFileInfo inf, long exist_size, long need_bytes) { long old_current_download_bytes = updater.current_download_bytes.Value; if (updater.driver.RunDownloadSingle(updater, inf, exist_size, need_bytes, updater.current_download_bytes) == false) { return true; } if (!check_md5(inf.file, inf.md5)) return true; if ((old_current_download_bytes + need_bytes) != updater.current_download_bytes.Value) { updater.current_download_bytes.Value = old_current_download_bytes + need_bytes; } return false; } private bool run_unzip() { while(_zip_files.Count > 0 ) { var inf = _zip_files.Dequeue(); if (updater.is_exit) return true; var mpqName = inf.key.TrimEnd('.', 'z'); var info = updater.remoteFiles.Find(data => data.key == mpqName); // 如果需要的空间无法满足 // long save_avaliable = updater.driver.GetAvaliableSpace(updater.save_root.FullName); long save_avaliable = updater._mSaveAvaliable; long need_bytes = info.size; if (need_bytes >= save_avaliable) { _zip_files.Enqueue(inf); updater.QueueEvent(new MPQUpdaterEvent( MPQUpdaterEvent.TYPE_NOT_ENOUGH_SPACE, "Space not available!")); return true; } // 开始解压缩 if (inf.file.Exists) { try { log.Info(string.Format("开始解压缩: {0} -> {1}", inf.key, info.file.Extension)); if (run_unzip_single(inf, info) || !info.file.Exists) { _zip_files.Enqueue(inf); } else { inf.file.Delete(); } } catch (Exception err) { log.Error(err); _zip_files.Enqueue(inf); throw; } } } return false; } private bool run_unzip_single(RemoteFileInfo inf, RemoteFileInfo mpqf) { long need_bytes = mpqf.size; long old_current_unzip_bytes = updater.current_unzip_bytes.Value; if (updater.driver.RunUnzipSingle(updater, inf, mpqf, updater.current_unzip_bytes) == false) { return true; } if ((old_current_unzip_bytes + need_bytes) != updater.current_unzip_bytes.Value) { updater.current_unzip_bytes.Value = old_current_unzip_bytes + need_bytes; } return false; } private bool check_md5(FileInfo ff, string md5) { string fmd5; if (updater.driver.RunGetFileMD5(ff.FullName, out fmd5) == false) { return false; } if (fmd5.ToLower().Equals(md5.ToLower())) { return true; } return false; } } //---------------------------------------------------------------------------------------------------------- public void Dispose() { is_exit = true; try { if (workthread != null) { workthread.Join(); } } catch (System.Exception err) { log.Error(err.Message, err); //Console.WriteLine(e); } } public ICollection GetAllFiles() { List files = new List(); lock (remoteFiles) { foreach (RemoteFileInfo rf in remoteFiles) { files.Add(rf.file); } } return files; } public int DownloadTimeoutSEC { get; set; } public string[] UrlRoots { get { return url_roots; } } public string UrlRoot { get { return url_roots[0]; } } public string VersionText { get { return version_text; } } public long TotalDownloadBytes { get { return total_download_bytes; } } public long CurrentDownloadBytes { get { return current_download_bytes.Value; } } public long CurrentDownloadSpeed { get { return current_download_speed_BPS; } } public long TotalUnzipBytes { get { return total_unzip_bytes; } } public long CurrentUnzipBytes { get { return current_unzip_bytes.Value; } } public long CurrentUnzipSpeed { get { return current_unzip_spped_BPS; } } public bool IsRunning { get { return is_running; } } public bool IsDisposing { get { return is_running && is_exit; } } public Status CurrentStatus { get { return status.Value; } } public bool IsLoadFinised { get; private set; } //----------------------------------------------------------------------------------------------------------- //----------------------------------------------------------------------------------------------------------- public class RemoteFileInfo { private FileInfo _file; public readonly string md5; public readonly string key; public readonly uint size; public FileInfo file { get { _file.Refresh(); return _file; } } internal RemoteFileInfo(string md5, uint size, string key, FileInfo file) { this.md5 = md5; this.key = key; this.size = size; this._file = file; } internal bool IsCompletion() { if (file.Exists && file.Length == size) { return true; } return false; } internal long NeedLoadSize() { if (file.Exists) return (size - file.Length); else return size; } } } }