การแสดงวันที่สร้าง


260

ขณะนี้ฉันมีแอพที่แสดงหมายเลขบิลด์ในหน้าต่างชื่อ นั่นเป็นเรื่องดีและดียกเว้นมันไม่มีความหมายสำหรับผู้ใช้ส่วนใหญ่ที่ต้องการทราบว่าพวกเขามีงานสร้างล่าสุดหรือไม่ - พวกเขามักจะอ้างถึงว่าเป็น "วันพฤหัสบดีที่ผ่านมา" แทนที่จะสร้าง 1.0.8.4321

แผนจะวางวันที่สร้างไว้แทนดังนั้นตัวอย่างเช่น "แอพที่สร้างขึ้นเมื่อวันที่ 21/10/2552"

ฉันพยายามหาวิธีเขียนโปรแกรมเพื่อดึงวันที่สร้างออกมาเป็นสตริงข้อความสำหรับใช้งานเช่นนี้

สำหรับหมายเลขบิลด์ฉันใช้:

Assembly.GetExecutingAssembly().GetName().Version.ToString()

หลังจากกำหนดว่าสิ่งเหล่านั้นเกิดขึ้นได้อย่างไร

ฉันต้องการบางสิ่งเช่นนี้สำหรับวันรวบรวม (และเวลาสำหรับคะแนนโบนัส)

ตัวชี้ที่นี่ชื่นชมมาก (แก้ตัวถ้าเหมาะสม) หรือวิธีแก้ปัญหา neater ...


2
ฉันลองวิธีที่ให้มาเพื่อรับข้อมูลบิลด์ของแอสเซมบลีซึ่งทำงานในสถานการณ์ง่าย ๆ แต่ถ้ารวมสองแอสเซมบลีเข้าด้วยกันฉันไม่ได้สร้างเวลาที่ถูกต้องมันเป็นหนึ่งชั่วโมงในอนาคต ..

คำตอบ:


356

Jeff Atwood มีบางสิ่งที่จะพูดเกี่ยวกับเรื่องนี้ในการกำหนดวันที่สร้างวิธีที่ยาก

วิธีการที่เชื่อถือได้มากที่สุดเป็นการดึงข้อมูลตัวเชื่อมโยงเวลาจากส่วนหัว PE ที่ฝังอยู่ในไฟล์เรียกทำงาน - โค้ด C # บางตัว (โดย Joe Spivey) สำหรับสิ่งนั้นจากความคิดเห็นไปยังบทความของ Jeff:

public static DateTime GetLinkerTime(this Assembly assembly, TimeZoneInfo target = null)
{
    var filePath = assembly.Location;
    const int c_PeHeaderOffset = 60;
    const int c_LinkerTimestampOffset = 8;

    var buffer = new byte[2048];

    using (var stream = new FileStream(filePath, FileMode.Open, FileAccess.Read))
        stream.Read(buffer, 0, 2048);

    var offset = BitConverter.ToInt32(buffer, c_PeHeaderOffset);
    var secondsSince1970 = BitConverter.ToInt32(buffer, offset + c_LinkerTimestampOffset);
    var epoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);

    var linkTimeUtc = epoch.AddSeconds(secondsSince1970);

    var tz = target ?? TimeZoneInfo.Local;
    var localTime = TimeZoneInfo.ConvertTimeFromUtc(linkTimeUtc, tz);

    return localTime;
}

ตัวอย่างการใช้งาน:

var linkTimeLocal = Assembly.GetExecutingAssembly().GetLinkerTime();

UPDATE: วิธีการนี้ทำงานกับ. Net Core 1.0 แต่หยุดทำงานหลังจากการเปิดตัว. Net Core 1.1 (ให้ปีแบบสุ่มในช่วง 1900-2020)


8
ฉันเปลี่ยนโทนเสียงของฉันเกี่ยวกับเรื่องนี้บ้างฉันยังคงต้องระวังเป็นอย่างมากเมื่อขุดลงในส่วนหัว PE แบบเฉียบพลัน แต่เท่าที่ฉันสามารถบอกได้สิ่ง PE นี้มีความน่าเชื่อถือมากกว่าการใช้หมายเลขเวอร์ชันนอกจากนี้ฉันไม่ต้องการกำหนดหมายเลขรุ่นแยกจากวันที่สร้าง
John Leidegren

6
ฉันชอบสิ่งนี้และกำลังใช้งานอยู่ แต่บรรทัดที่สองถึงบรรทัดสุดท้ายที่.AddHours()ค่อนข้างแฮ็คและ (ฉันคิดว่า) จะไม่พิจารณา DST หากคุณต้องการในเวลาท้องถิ่นคุณควรใช้น้ำยาทำความสะอาดdt.ToLocalTime();แทน ส่วนตรงกลางสามารถทำให้ง่ายขึ้นอย่างมากด้วยusing()บล็อก
JLRishe

6
ใช่สิ่งนี้หยุดทำงานสำหรับฉันด้วย. net core เช่นกัน (1940, 1960, ฯลฯ )
eoleary

7
ในขณะที่การใช้งานส่วนหัว PE อาจดูเหมือนเป็นตัวเลือกที่ดีในวันนี้ แต่ก็น่าสังเกตว่า MS กำลังทดลองกับการสร้างแบบกำหนดขึ้นเอง (ซึ่งจะทำให้ส่วนหัวนี้ไร้ประโยชน์) และอาจทำให้เป็นค่าเริ่มต้นในเวอร์ชั่นคอมไพเลอร์ในอนาคต อ่านได้ดี: blog.paranoidcoding.com/2016/04/05/ …และนี่คือคำตอบที่เกี่ยวข้องกับ. NET Core (TLDR: "การออกแบบ"): developercommunity.visualstudio.com/content/problem/35873/…
Paweł Bulwan

13
สำหรับผู้ที่พบว่าใช้งานไม่ได้ปัญหาไม่ใช่ปัญหา. NET Core ดูคำตอบของฉันด้านล่างเกี่ยวกับค่าเริ่มต้นของพารามิเตอร์บิลด์ใหม่ที่เริ่มต้นด้วย Visual Studio 15.4
Tom

107

เพิ่มด้านล่างเพื่อสร้างบรรทัดคำสั่งเหตุการณ์ล่วงหน้า:

echo %date% %time% > "$(ProjectDir)\Resources\BuildDate.txt"

เพิ่มไฟล์นี้เป็นทรัพยากรตอนนี้คุณมีสตริง 'BuildDate' ในทรัพยากรของคุณ

เพื่อสร้างทรัพยากรให้ดูวิธีการสร้างและใช้ทรัพยากรใน .NET


4
+1 จากฉันเรียบง่ายและมีประสิทธิภาพ ฉันจัดการเพื่อรับค่าจากไฟล์ด้วยบรรทัดของรหัสเช่นนี้: String buildDate = <MyClassLibraryName> .Properties.Resources.BuildDate
davidfrancis

11
อีกตัวเลือกหนึ่งคือสร้างคลาส: (ต้องรวมไว้ในโครงการหลังจากที่คุณรวบรวมเป็นครั้งแรก) -> echo namespace My.app.namespace {คลาสสแตติกสาธารณะบิวด์ {สแตติกสตริงสาธารณะ = "% DATE%% TIME%" .Substring (0,16);}}> "$ (ProjectDir) \ BuildTimestamp.cs" - - - ->> จากนั้นสามารถเรียกมันด้วย Build.Timestamp
FabianSilva

9
นี่เป็นทางออกที่ยอดเยี่ยม ปัญหาเดียวคือตัวแปรบรรทัดคำสั่ง% date% และ% time% เป็นภาษาท้องถิ่นดังนั้นผลลัพธ์จะแตกต่างกันไปตามภาษา Windows ของผู้ใช้
VS

2
+1 นี่เป็นวิธีที่ดีกว่าการอ่านส่วนหัว PE - เนื่องจากมีหลายสถานการณ์ที่ไม่สามารถใช้งานได้ (ตัวอย่างเช่นแอพ Windows Phone)
แมตต์วิ ธ ฟิลด์

17
ฉลาด. นอกจากนี้คุณยังสามารถใช้ powershell เพื่อรับการควบคุมที่แม่นยำยิ่งขึ้นสำหรับรูปแบบเช่นรับรูปแบบวันที่และเวลา UTC ในรูปแบบ ISO8601: powershell -Command "((รับวันที่). ToUniversalTime ()) ToString (\" s \ ") | Out-File '$ (ProjectDir) Resources \ BuildDate.txt' "
dbruning

90

วิธีการ

ในฐานะที่เป็นแหลมออกโดย @ c00000fd ในการแสดงความคิดเห็น Microsoft กำลังเปลี่ยนแปลงสิ่งนี้ และในขณะที่หลายคนไม่ได้ใช้คอมไพเลอร์เวอร์ชันล่าสุดฉันสงสัยว่าการเปลี่ยนแปลงนี้จะทำให้วิธีการนี้ไม่ดีอย่างแน่นอน และในขณะที่มันเป็นแบบฝึกหัดที่สนุกฉันขอแนะนำให้ผู้ใช้เพียงฝังวันที่สร้างลงในไบนารีของพวกเขาด้วยวิธีการอื่นที่จำเป็นถ้ามันเป็นสิ่งสำคัญในการติดตามวันที่สร้างของตัวไบนารี

สิ่งนี้สามารถทำได้ด้วยการสร้างรหัสเล็ก ๆ น้อย ๆ ซึ่งอาจเป็นขั้นตอนแรกในการสร้างสคริปต์ของคุณแล้ว และความจริงที่ว่าเครื่องมือ ALM / Build / DevOps ช่วยได้มากในเรื่องนี้และควรเป็นที่นิยมในสิ่งอื่นใด

ฉันปล่อยคำตอบที่เหลือไว้ที่นี่เพื่อจุดประสงค์ทางประวัติศาสตร์เท่านั้น

วิธีการใหม่

ฉันเปลี่ยนใจเกี่ยวกับสิ่งนี้และใช้เคล็ดลับนี้เพื่อรับวันที่สร้างที่ถูกต้อง

#region Gets the build date and time (by reading the COFF header)

// http://msdn.microsoft.com/en-us/library/ms680313

struct _IMAGE_FILE_HEADER
{
    public ushort Machine;
    public ushort NumberOfSections;
    public uint TimeDateStamp;
    public uint PointerToSymbolTable;
    public uint NumberOfSymbols;
    public ushort SizeOfOptionalHeader;
    public ushort Characteristics;
};

static DateTime GetBuildDateTime(Assembly assembly)
{
    var path = assembly.GetName().CodeBase;
    if (File.Exists(path))
    {
        var buffer = new byte[Math.Max(Marshal.SizeOf(typeof(_IMAGE_FILE_HEADER)), 4)];
        using (var fileStream = new FileStream(path, FileMode.Open, FileAccess.Read))
        {
            fileStream.Position = 0x3C;
            fileStream.Read(buffer, 0, 4);
            fileStream.Position = BitConverter.ToUInt32(buffer, 0); // COFF header offset
            fileStream.Read(buffer, 0, 4); // "PE\0\0"
            fileStream.Read(buffer, 0, buffer.Length);
        }
        var pinnedBuffer = GCHandle.Alloc(buffer, GCHandleType.Pinned);
        try
        {
            var coffHeader = (_IMAGE_FILE_HEADER)Marshal.PtrToStructure(pinnedBuffer.AddrOfPinnedObject(), typeof(_IMAGE_FILE_HEADER));

            return TimeZone.CurrentTimeZone.ToLocalTime(new DateTime(1970, 1, 1) + new TimeSpan(coffHeader.TimeDateStamp * TimeSpan.TicksPerSecond));
        }
        finally
        {
            pinnedBuffer.Free();
        }
    }
    return new DateTime();
}

#endregion

วิธีเก่า ๆ

คุณจะสร้างหมายเลขบิลด์ได้อย่างไร Visual Studio (หรือคอมไพเลอร์ C #) จะให้หมายเลขบิวด์และการแก้ไขอัตโนมัติหากคุณเปลี่ยนแอททริบิวต์ AssemblyVersion เป็นเช่น1.0.*

สิ่งที่จะเกิดขึ้นก็คือการสร้างจะเท่ากับจำนวนวันตั้งแต่วันที่ 1 มกราคม 2000 เวลาท้องถิ่นและสำหรับการแก้ไขจะเท่ากับจำนวนวินาทีตั้งแต่เวลาท้องถิ่นเที่ยงคืนหารด้วย 2

ดูเนื้อหาของชุมชนสร้างโดยอัตโนมัติและหมายเลขการแก้ไข

เช่น AssemblyInfo.cs

[assembly: AssemblyVersion("1.0.*")] // important: use wildcard for build and revision numbers!

SampleCode.cs

var version = Assembly.GetEntryAssembly().GetName().Version;
var buildDateTime = new DateTime(2000, 1, 1).Add(new TimeSpan(
TimeSpan.TicksPerDay * version.Build + // days since 1 January 2000
TimeSpan.TicksPerSecond * 2 * version.Revision)); // seconds since midnight, (multiply by 2 to get original)

3
ฉันเพิ่งเพิ่มหนึ่งชั่วโมงถ้าTimeZone.CurrentTimeZone.IsDaylightSavingTime(buildDateTime) == true
e4rthdog

2
น่าเสียดายที่ฉันใช้วิธีการนี้โดยไม่ต้องตรวจอย่างถี่ถ้วนและมันกำลังกัดเราในการผลิต ปัญหาคือเมื่อคอมไพเลอร์ JIT เตะในข้อมูลส่วนหัว PE มีการเปลี่ยนแปลง ดังนั้นการลงคะแนนเสียง ตอนนี้ฉันกำลังจะทำ 'การวิจัย' ที่ไม่จำเป็นเพื่ออธิบายว่าทำไมเราถึงเห็นวันที่ติดตั้งเป็นวันที่สร้าง
Jason D

8
@JasonD ปัญหาของคุณในจักรวาลกลายเป็นปัญหาของฉันอย่างไร? คุณจะแสดงให้เห็นถึง downvote เพียงเพราะคุณพบปัญหาที่การใช้งานนี้ไม่ได้คำนึงถึง คุณได้รับสิ่งนี้ฟรีและคุณทดสอบได้ไม่ดี นอกจากนี้สิ่งที่ทำให้คุณเชื่อว่าส่วนหัวจะถูกเขียนใหม่โดยคอมไพเลอร์ JIT? คุณกำลังอ่านข้อมูลนี้จากหน่วยความจำกระบวนการหรือจากไฟล์?
John Leidegren

6
ฉันสังเกตเห็นว่าหากคุณกำลังทำงานในเว็บแอปพลิเคชันคุณสมบัติ. Codebase ดูเหมือนจะเป็น URL (ไฟล์: // c: /path/to/binary.dll) สิ่งนี้ทำให้การเรียกใช้ File.Exists ล้มเหลว ใช้ "แอสเซมบลีตำแหน่ง" แทนคุณสมบัติ CodeBase แก้ไขปัญหาให้ฉัน
mdryden

2
@JohnLeidegren: อย่าพึ่ง Windows PE header สำหรับสิ่งนั้น เนื่องจาก Windows 10และบิลด์ที่สร้างซ้ำได้IMAGE_FILE_HEADER::TimeDateStampฟิลด์จะถูกตั้งค่าเป็นหมายเลขสุ่มและไม่ต้องประทับเวลาอีกต่อไป
c00000fd

51

เพิ่มด้านล่างเพื่อสร้างบรรทัดคำสั่งเหตุการณ์ล่วงหน้า:

echo %date% %time% > "$(ProjectDir)\Resources\BuildDate.txt"

เพิ่มไฟล์นี้เป็นทรัพยากรตอนนี้คุณมีสตริง 'BuildDate' ในทรัพยากรของคุณ

หลังจากแทรกไฟล์ลงในทรัพยากร (เป็นไฟล์ข้อความสาธารณะ) ฉันเข้าถึงมันผ่าน

string strCompTime = Properties.Resources.BuildDate;

เพื่อสร้างทรัพยากรให้ดูวิธีการสร้างและใช้ทรัพยากรใน .NET


1
@DavidGorsline - ความคิดเห็น markdown ถูกต้องตามที่มีการอ้างนี้คำตอบอื่นฉันมีชื่อเสียงไม่เพียงพอที่จะย้อนกลับการเปลี่ยนแปลงของคุณมิฉะนั้นฉันจะทำเอง
Wai Ha Lee

1
@Wai Ha Lee - a) คำตอบที่คุณอ้างไม่ได้ให้รหัสเพื่อเรียกคืนวันที่ / เวลาในการรวบรวม b) ในเวลานั้นฉันไม่มีชื่อเสียงพอที่จะเพิ่มความคิดเห็นในคำตอบนั้น (ซึ่งฉันจะทำ) เพื่อโพสต์เท่านั้น ดังนั้น c) ฉันโพสต์ให้คำตอบเต็มรูปแบบเพื่อให้ผู้คนสามารถรับรายละเอียดทั้งหมดในที่เดียว ..
brewmanz

หากคุณเห็นÚte% แทนที่จะเป็น% date% ให้ตรวจสอบที่นี่: developercommunity.visualstudio.com/content/problem/237752/… โดยสรุปให้ทำดังนี้: echo% 25 วันที่% 25% 25time% 25
Qodex

41

วิธีการหนึ่งที่ฉันประหลาดใจที่ไม่มีใครได้กล่าวถึงคือการใช้เทมเพลตข้อความ T4สำหรับการสร้างรหัส

<#@ template debug="false" hostspecific="true" language="C#" #>
<#@ assembly name="System.Core" #>
<#@ import namespace="System" #>
<#@ output extension=".g.cs" #>
using System;
namespace Foo.Bar
{
    public static partial class Constants
    {
        public static DateTime CompilationTimestampUtc { get { return new DateTime(<# Write(DateTime.UtcNow.Ticks.ToString()); #>L, DateTimeKind.Utc); } }
    }
}

ข้อดี:

  • สถานที่เกิดเหตุที่เป็นอิสระ
  • ให้มากกว่าเวลาในการรวบรวม

จุดด้อย:

  • ใช้ได้กับไลบรารีที่คุณควบคุมแหล่งข้อมูลเท่านั้น
  • ต้องมีการกำหนดค่าโครงการของคุณ (และสร้างเซิร์ฟเวอร์ถ้าไม่ได้หยิบมันขึ้นมา) เพื่อดำเนินการแม่แบบในการสร้างก่อนขั้นตอน (ดูT4 ที่ไม่มี VS )

1
ดังนั้นนี่คือคำตอบที่ดีที่สุด 324 คะแนนก่อนที่มันจะเป็นคำตอบที่ได้รับการโหวตมากที่สุด :) Stackoverflow ต้องการวิธีในการแสดงนักปีนเขาที่เร็วที่สุด
pauldendulk

1
@ pauldendulk คงไม่ช่วยอะไรมากเพราะคำตอบที่ได้รับการโหวตมากที่สุดและคำตอบที่ได้รับการยอมรับนั้นเกือบจะเร็วที่สุด คำตอบที่ยอมรับสำหรับคำถามนี้มี+ 60 / -2ตั้งแต่ฉันโพสต์คำตอบนี้
Peter Taylor

ฉันเชื่อว่าคุณต้องเพิ่ม. ToString () ใน Ticks ของคุณ (ฉันได้รับข้อผิดพลาดในการรวบรวมเป็นอย่างอื่น) ที่กล่าวว่าฉันกำลังเข้าสู่ช่วงการเรียนรู้ที่สูงชันที่นี่คุณสามารถแสดงวิธีใช้สิ่งนี้ในโปรแกรมหลักได้หรือไม่
Andy

@ แอนดี้คุณถูกต้องเกี่ยวกับ ToString () Constants.CompilationTimestampUtcการใช้งานเป็นเพียง ถ้า VS ไม่ได้สร้างไฟล์ C # ด้วยคลาสคุณต้องหาวิธีที่จะทำให้มันทำ แต่คำตอบนั้นขึ้นอยู่กับ (อย่างน้อยที่สุด) เวอร์ชัน VS และชนิดของไฟล์ csproj รายละเอียดมากเกินไปสำหรับโพสต์นี้
ปีเตอร์เทย์เลอร์

1
ในกรณีที่คนอื่นกำลังสงสัยนี่คือสิ่งที่ต้องทำเพื่อให้มันทำงานได้กับ VS 2017: ฉันต้องทำให้นี่เป็นเทมเพลต T4 ของ Design Time T4 (ใช้เวลาสักครู่กว่าจะคิดออกมาฉันได้เพิ่มเทมเพลต Preprocessor ก่อน) ฉันยังต้องรวมแอสเซมบลีนี้: Microsoft.VisualStudio.TextTemplating.Interfaces.10.0 เป็นการอ้างอิงถึงโครงการ ในที่สุดเทมเพลตของฉันต้องรวม "using System;" ก่อนเนมสเปซหรือการอ้างอิงถึง DateTime ล้มเหลว
Andy

20

เกี่ยวกับเทคนิคการดึงข้อมูลวันที่สร้าง / รุ่นจากไบต์ของส่วนหัว PE ประกอบ Microsoft ได้เปลี่ยนพารามิเตอร์การสร้างเริ่มต้นที่เริ่มต้นด้วย Visual Studio 15.4 ค่าเริ่มต้นใหม่รวมถึงการรวบรวมที่กำหนดขึ้นซึ่งทำให้การประทับเวลาที่ถูกต้องและเพิ่มหมายเลขรุ่นที่เพิ่มขึ้นโดยอัตโนมัติเป็นเรื่องในอดีต ฟิลด์การประทับเวลายังคงอยู่ แต่จะได้รับการเติมด้วยค่าถาวรที่เป็นแฮชของบางสิ่งหรืออย่างอื่น แต่ไม่มีการบ่งชี้เวลาการสร้าง

รายละเอียดพื้นหลังบางอย่างที่นี่

สำหรับผู้ที่จัดลำดับความสำคัญของการประทับเวลาที่มีประโยชน์มากกว่าการรวบรวมที่กำหนดขึ้นมีวิธีการแทนที่ค่าเริ่มต้นใหม่ คุณสามารถรวมแท็กในไฟล์. csproj ของแอสเซมบลีที่น่าสนใจดังนี้:

  <PropertyGroup>
      ...
      <Deterministic>false</Deterministic>
  </PropertyGroup>

อัปเดต: ฉันรับรองโซลูชันเทมเพลตข้อความ T4 ที่อธิบายไว้ในคำตอบอื่นที่นี่ ฉันใช้มันเพื่อแก้ปัญหาของฉันอย่างหมดจดโดยไม่เสียประโยชน์จากการรวบรวมที่กำหนดขึ้นมา ข้อควรระวังอย่างหนึ่งเกี่ยวกับมันคือ Visual Studio จะรันเฉพาะคอมไพเลอร์ T4 เมื่อไฟล์. tt ถูกบันทึกไม่ใช่เวลาบิลด์ สิ่งนี้อาจน่าอึดอัดใจหากคุณแยกผลลัพธ์. cs จากการควบคุมแหล่งที่มา (เนื่องจากคุณคาดว่าจะสร้างขึ้น) และนักพัฒนาซอฟต์แวร์รายอื่นตรวจสอบรหัส หากไม่มีการบันทึกใหม่พวกเขาจะไม่มีไฟล์. cs มีแพ็คเกจบน nuget (ฉันคิดว่าเรียกว่า AutoT4) ที่ทำให้การรวบรวม T4 เป็นส่วนหนึ่งของทุกบิลด์ ฉันยังไม่ได้เผชิญหน้ากับวิธีแก้ปัญหานี้ในระหว่างการปรับใช้การผลิต แต่ฉันคาดหวังสิ่งที่คล้ายกันเพื่อให้ถูกต้อง


สิ่งนี้แก้ปัญหาของฉันใน sln ที่ใช้คำตอบที่เก่าที่สุด
pauldendulk

ข้อควรระวังของคุณเกี่ยวกับ T4 นั้นยุติธรรมอย่างสมบูรณ์แบบ แต่โปรดทราบว่ามันมีอยู่ในคำตอบของฉันแล้ว
Peter Taylor

15

ฉันแค่ C # newbie ดังนั้นบางทีคำตอบของฉันเสียงโง่ - ฉันแสดงวันที่สร้างจากวันที่ไฟล์ปฏิบัติการถูกเขียนล่าสุดไปที่

string w_file = "MyProgram.exe"; 
string w_directory = Directory.GetCurrentDirectory();

DateTime c3 =  File.GetLastWriteTime(System.IO.Path.Combine(w_directory, w_file));
RTB_info.AppendText("Program created at: " + c3.ToString());

ฉันพยายามใช้วิธี File.GetCreationTime แต่ได้ผลลัพธ์แปลก ๆ : วันที่จากคำสั่งคือ 2012-05-29 แต่วันที่จาก Window Explorer แสดง 2012-05-23 หลังจากค้นหาความคลาดเคลื่อนนี้ฉันพบว่าไฟล์อาจถูกสร้างขึ้นใน 2012-05-23 (ดังที่แสดงโดย Windows Explorer) แต่คัดลอกไปยังโฟลเดอร์ปัจจุบันใน 2012-05-29 (ดังแสดงโดยคำสั่ง File.GetCreationTime) - ดังนั้น เป็นทางที่ปลอดภัยฉันใช้คำสั่ง File.GetLastWriteTime

Zalek


4
ฉันไม่แน่ใจว่านี่เป็นข้อพิสูจน์หัวข้อย่อยจากการคัดลอกไฟล์ปฏิบัติการข้ามไดรฟ์ / คอมพิวเตอร์ / เครือข่ายหรือไม่
Stealth Rabbi

นี่เป็นสิ่งแรกที่มาในใจ แต่คุณรู้ว่ามันไม่น่าเชื่อถือมีซอฟต์แวร์จำนวนมากที่ใช้ในการย้ายไฟล์ผ่านเครือข่ายซึ่งไม่ได้อัปเดตคุณลักษณะหลังจากดาวน์โหลดฉันจะไปกับคำตอบของ @ Abdurrahim
Mubashar

ฉันรู้ว่ามันเก่า แต่ฉันเพิ่งพบกับรหัสที่คล้ายกันว่ากระบวนการติดตั้ง (อย่างน้อยเมื่อใช้ clickonce) อัปเดตเวลาประกอบไฟล์ ไม่มีประโยชน์มาก ไม่แน่ใจว่าจะใช้กับโซลูชันนี้ได้หรือไม่
bobwki

คุณอาจต้องการจริงๆLastWriteTimeเพราะนั่นสะท้อนถึงเวลาที่ไฟล์ปฏิบัติการได้รับการปรับปรุงจริง
David R Tribble

ขออภัย แต่เวลาเขียนไฟล์ที่ปฏิบัติการได้นั้นไม่ได้เป็นตัวบ่งชี้ที่เชื่อถือได้ของเวลาบิลด์ การประทับเวลาของไฟล์สามารถเขียนใหม่ได้เนื่องจากทุกสิ่งที่อยู่นอกขอบเขตอิทธิพลของคุณ
Tom

15

คำตอบที่ดีมากมายที่นี่ แต่ฉันรู้สึกเหมือนฉันสามารถเพิ่มของตัวเองเพราะความเรียบง่ายประสิทธิภาพ (เปรียบเทียบกับโซลูชันที่เกี่ยวข้องกับทรัพยากร) ข้ามแพลตฟอร์ม (ทำงานร่วมกับ Net Core ด้วย) และหลีกเลี่ยงเครื่องมือของบุคคลที่สามใด ๆ เพียงเพิ่มเป้าหมาย msbuild นี้ไปที่ csproj

<Target Name="Date" BeforeTargets="CoreCompile">
    <WriteLinesToFile File="$(IntermediateOutputPath)gen.cs" Lines="static partial class Builtin { public static long CompileTime = $([System.DateTime]::UtcNow.Ticks) %3B }" Overwrite="true" />
    <ItemGroup>
        <Compile Include="$(IntermediateOutputPath)gen.cs" />
    </ItemGroup>
</Target>

และตอนนี้คุณมีBuiltin.CompileTimeหรือnew DateTime(Builtin.CompileTime, DateTimeKind.Utc)ถ้าคุณต้องการมันเป็นอย่างนั้น

ReSharper ไม่ชอบหรอก คุณสามารถเพิกเฉยต่อเขาหรือเพิ่มคลาสบางส่วนในโครงการได้ด้วย


ฉันสามารถสร้างด้วยสิ่งนี้และพัฒนาภายใน (เรียกใช้เว็บไซต์) ใน ASP.NET Core 2.1 แต่การปรับใช้เว็บเผยแพร่จาก VS 2017 ล้มเหลวด้วยข้อผิดพลาด "ชื่อ 'Builtin' ไม่มีอยู่ในบริบทปัจจุบัน" เพิ่มเติม: ถ้าฉันเข้าถึงBuiltin.CompileTimeจากมุมมองมีดโกน
Jeremy Cook

ในกรณีนี้ผมคิดว่าคุณเพียงแค่ต้องBeforeTargets="RazorCoreCompile"แต่ขณะนี้อยู่ในโครงการเดียวกัน
Dmitry Gusarov

เจ๋ง แต่เราจะอ้างถึงวัตถุที่สร้างขึ้นได้อย่างไร ดูเหมือนว่าฉันคำตอบจะหายไปเป็นส่วนสำคัญ ...
Matteo

1
@Matteo ตามที่ระบุไว้ในคำตอบคุณสามารถใช้ "Builtin.CompileTime" หรือ "ใหม่ DateTime (Builtin.CompileTime, DateTimeKind.Utc)" Visual Studio IntelliSense สามารถดูสิ่งนี้ได้ทันที ReSharper ตัวเก่าสามารถบ่นได้ในเวลาออกแบบ แต่ดูเหมือนว่าพวกเขาจะแก้ไขมันในเวอร์ชั่นใหม่ clip2net.com/s/46rgaaO
Dmitry Gusarov

ฉันใช้รุ่นนี้จึงไม่จำเป็นต้องใช้รหัสเพิ่มเติมเพื่อรับวันที่ resharper ยังไม่บ่นกับรุ่นล่าสุด <WriteLinesToFile File = "$ (IntermediateOutputPath) BuildInfo.cs" Lines = "โดยใช้ระบบ% 3B ส่วนคงที่ภายในบางส่วนของชั้น BuildInfo {สาธารณะคงที่ยาว DateBuiltTicks = $ ([System.DateTime] :: UtcNow.Ticks) => ใหม่ DateTime (DateBuiltTicks, DateTimeKind.Utc)% 3B} "Overwrite =" true "/>
Softlion

13

สำหรับโครงการ. NET Core ฉันปรับคำตอบของ Postlagerkarte เพื่อปรับปรุงฟิลด์ลิขสิทธิ์แอสเซมบลีด้วยวันที่สร้าง

แก้ไข csproj โดยตรง

สามารถเพิ่มต่อไปนี้ลงPropertyGroupใน csproj ได้โดยตรง:

<Copyright>Copyright © $([System.DateTime]::UtcNow.Year) Travis Troyer ($([System.DateTime]::UtcNow.ToString("s")))</Copyright>

ทางเลือก: คุณสมบัติโครงการ Visual Studio

หรือวางนิพจน์ภายในลงในฟิลด์ลิขสิทธิ์ในส่วนแพ็คเกจของคุณสมบัติโครงการใน Visual Studio:

Copyright © $([System.DateTime]::UtcNow.Year) Travis Troyer ($([System.DateTime]::UtcNow.ToString("s")))

สิ่งนี้อาจสับสนเล็กน้อยเนื่องจาก Visual Studio จะประเมินนิพจน์และแสดงค่าปัจจุบันในหน้าต่าง แต่จะปรับปรุงไฟล์โครงการอย่างเหมาะสมหลังฉาก

Solution-wide ผ่าน Directory.Build.props

คุณสามารถใส่<Copyright>องค์ประกอบด้านบนลงในDirectory.Build.propsไฟล์ในรูทโซลูชันของคุณและให้มันนำไปใช้กับโครงการทั้งหมดในไดเรกทอรีโดยอัตโนมัติโดยสมมติว่าแต่ละโครงการไม่ได้ให้ค่าลิขสิทธิ์ของตนเอง

<Project>
 <PropertyGroup>
   <Copyright>Copyright © $([System.DateTime]::UtcNow.Year) Travis Troyer ($([System.DateTime]::UtcNow.ToString("s")))</Copyright>
 </PropertyGroup>
</Project>

Directory.Build.props: ปรับแต่งบิลด์ของคุณ

เอาท์พุต

ตัวอย่างการแสดงออกจะให้ลิขสิทธิ์เช่นนี้:

Copyright © 2018 Travis Troyer (2018-05-30T14:46:23)

การแก้ไข

คุณสามารถดูข้อมูลลิขสิทธิ์จากคุณสมบัติไฟล์ใน Windows หรือคว้ามันในขณะทำงาน:

var version = FileVersionInfo.GetVersionInfo(Assembly.GetEntryAssembly().Location);

Console.WriteLine(version.LegalCopyright);

11

วิธีการข้างต้นสามารถปรับแต่งสำหรับการประกอบที่โหลดแล้วภายในกระบวนการโดยใช้ภาพของไฟล์ในหน่วยความจำ (ตรงข้ามกับการอ่านอีกครั้งจากการจัดเก็บ):

using System;
using System.Runtime.InteropServices;
using Assembly = System.Reflection.Assembly;

static class Utils
{
    public static DateTime GetLinkerDateTime(this Assembly assembly, TimeZoneInfo tzi = null)
    {
        // Constants related to the Windows PE file format.
        const int PE_HEADER_OFFSET = 60;
        const int LINKER_TIMESTAMP_OFFSET = 8;

        // Discover the base memory address where our assembly is loaded
        var entryModule = assembly.ManifestModule;
        var hMod = Marshal.GetHINSTANCE(entryModule);
        if (hMod == IntPtr.Zero - 1) throw new Exception("Failed to get HINSTANCE.");

        // Read the linker timestamp
        var offset = Marshal.ReadInt32(hMod, PE_HEADER_OFFSET);
        var secondsSince1970 = Marshal.ReadInt32(hMod, offset + LINKER_TIMESTAMP_OFFSET);

        // Convert the timestamp to a DateTime
        var epoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
        var linkTimeUtc = epoch.AddSeconds(secondsSince1970);
        var dt = TimeZoneInfo.ConvertTimeFromUtc(linkTimeUtc, tzi ?? TimeZoneInfo.Local);
        return dt;
    }
}

อันนี้ใช้งานได้ดีแม้สำหรับกรอบการใช้งาน 4.7: Utils.GetLinkerDateTime (Assembly.GetExecutingAssembly (), null))
real_yggdrasil

ใช้งานได้ดี! ขอบคุณ!
bobwki

10

สำหรับทุกคนที่ต้องการรวบรวมเวลาใน Windows 8 / Windows Phone 8:

    public static async Task<DateTimeOffset?> RetrieveLinkerTimestamp(Assembly assembly)
    {
        var pkg = Windows.ApplicationModel.Package.Current;
        if (null == pkg)
        {
            return null;
        }

        var assemblyFile = await pkg.InstalledLocation.GetFileAsync(assembly.ManifestModule.Name);
        if (null == assemblyFile)
        {
            return null;
        }

        using (var stream = await assemblyFile.OpenSequentialReadAsync())
        {
            using (var reader = new DataReader(stream))
            {
                const int PeHeaderOffset = 60;
                const int LinkerTimestampOffset = 8;

                //read first 2048 bytes from the assembly file.
                byte[] b = new byte[2048];
                await reader.LoadAsync((uint)b.Length);
                reader.ReadBytes(b);
                reader.DetachStream();

                //get the pe header offset
                int i = System.BitConverter.ToInt32(b, PeHeaderOffset);

                //read the linker timestamp from the PE header
                int secondsSince1970 = System.BitConverter.ToInt32(b, i + LinkerTimestampOffset);

                var dt = new DateTimeOffset(1970, 1, 1, 0, 0, 0, DateTimeOffset.Now.Offset) + DateTimeOffset.Now.Offset;
                return dt.AddSeconds(secondsSince1970);
            }
        }
    }

สำหรับทุกคนที่ต้องการรวบรวมเวลาใน Windows Phone 7:

    public static async Task<DateTimeOffset?> RetrieveLinkerTimestampAsync(Assembly assembly)
    {
        const int PeHeaderOffset = 60;
        const int LinkerTimestampOffset = 8;            
        byte[] b = new byte[2048];

        try
        {
            var rs = Application.GetResourceStream(new Uri(assembly.ManifestModule.Name, UriKind.Relative));
            using (var s = rs.Stream)
            {
                var asyncResult = s.BeginRead(b, 0, b.Length, null, null);
                int bytesRead = await Task.Factory.FromAsync<int>(asyncResult, s.EndRead);
            }
        }
        catch (System.IO.IOException)
        {
            return null;
        }

        int i = System.BitConverter.ToInt32(b, PeHeaderOffset);
        int secondsSince1970 = System.BitConverter.ToInt32(b, i + LinkerTimestampOffset);
        var dt = new DateTimeOffset(1970, 1, 1, 0, 0, 0, DateTimeOffset.Now.Offset) + DateTimeOffset.Now.Offset;
        dt = dt.AddSeconds(secondsSince1970);
        return dt;
    }

หมายเหตุ: ในทุกกรณีคุณกำลังทำงานในแซนด์บ็อกซ์ดังนั้นคุณจะสามารถรวบรวมเวลาแอสเซมบลีที่คุณปรับใช้กับแอปของคุณเท่านั้น (เช่นนี้จะไม่ทำงานกับอะไรใน GAC)


นี่คือวิธีที่คุณจะได้รับการประกอบใน WP 8.1:var assembly = typeof (AnyTypeInYourAssembly).GetTypeInfo().Assembly;
André Fiedler

ถ้าคุณต้องการรันโค้ดในทั้งสองระบบ? - วิธีใดวิธีหนึ่งเหล่านี้ใช้ได้กับทั้งสองแพลตฟอร์ม
bvdb

10

ในปี 2561 โซลูชันข้างต้นบางตัวไม่สามารถใช้งานได้อีกต่อไปหรือไม่สามารถทำงานกับ. NET Core ได้

ฉันใช้วิธีการต่อไปนี้ซึ่งใช้งานง่ายและใช้ได้กับโครงการ. NET Core 2.0 ของฉัน

เพิ่มสิ่งต่อไปนี้ใน. csproj ของคุณภายในกลุ่มอสังหาริมทรัพย์:

    <Today>$([System.DateTime]::Now)</Today>

สิ่งนี้จะกำหนดคุณสมบัติของฟังก์ชันที่คุณสามารถเข้าถึงได้ในคำสั่ง pre build

งานสร้างล่วงหน้าของคุณมีลักษณะเช่นนี้

echo $(today) > $(ProjectDir)BuildTimeStamp.txt

ตั้งค่าคุณสมบัติของ BuildTimeStamp.txt เป็นทรัพยากรที่ฝังตัว

ตอนนี้คุณสามารถอ่านการประทับเวลาเช่นนี้

public static class BuildTimeStamp
    {
        public static string GetTimestamp()
        {
            var assembly = Assembly.GetEntryAssembly(); 

            var stream = assembly.GetManifestResourceStream("NamespaceGoesHere.BuildTimeStamp.txt");

            using (var reader = new StreamReader(stream))
            {
                return reader.ReadToEnd();
            }
        }
    }

เพียงแค่สร้าง BuildTimeStamp.txt จากเหตุการณ์ที่สร้างไว้ล่วงหน้าโดยใช้คำสั่งสคริปต์ชุดงานเท่านั้น โปรดทราบว่าคุณทำผิดพลาดอยู่ที่นั่น: คุณควรล้อมเป้าหมายของคุณด้วยเครื่องหมายคำพูด (เช่น"$(ProjectDir)BuildTimeStamp.txt") หรือจะหยุดเมื่อมีช่องว่างในชื่อโฟลเดอร์
Nyerguds

บางทีมันสมเหตุสมผลที่จะใช้รูปแบบเวลาที่ไม่เปลี่ยนแปลงของวัฒนธรรม เช่นนี้$([System.DateTime]::Now.tostring("MM/dd/yyyy HH:mm:ss"))แทน$([System.DateTime]::Now)
Ivan Kochurkin

9

ตัวเลือกที่ไม่ได้กล่าวถึงในที่นี้คือการแทรกข้อมูลของคุณเองลงใน AssemblyInfo.cs ดูเหมือนว่าฟิลด์ "AssemblyInformationalVersion" ที่เหมาะสม - เรามีสองโครงการที่เรากำลังทำสิ่งที่คล้ายกันเป็นขั้นตอนการสร้าง (แต่ฉันไม่พอใจกับ วิธีการทำงานจึงไม่ต้องการที่จะทำซ้ำสิ่งที่เรามี)

มีบทความเกี่ยวกับหัวเรื่องของ codeproject: http://www.codeproject.com/KB/dotnet/Customizing_csproj_files.aspx


6

ฉันต้องการโซลูชันสากลที่ทำงานกับโครงการ NETStandard บนแพลตฟอร์มใด ๆ (iOS, Android และ Windows) เพื่อให้บรรลุสิ่งนี้ฉันตัดสินใจที่จะสร้างไฟล์ CS โดยอัตโนมัติผ่านสคริปต์ PowerShell นี่คือสคริปต์ PowerShell:

param($outputFile="BuildDate.cs")

$buildDate = Get-Date -date (Get-Date).ToUniversalTime() -Format o
$class = 
"using System;
using System.Globalization;

namespace MyNamespace
{
    public static class BuildDate
    {
        public const string BuildDateString = `"$buildDate`";
        public static readonly DateTime BuildDateUtc = DateTime.Parse(BuildDateString, null, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal);
    }
}"

Set-Content -Path $outputFile -Value $class

บันทึกไฟล์ PowerScript เป็น GenBuildDate.ps1 และเพิ่มโครงการของคุณ ในที่สุดเพิ่มบรรทัดต่อไปนี้ไปยังเหตุการณ์ Pre-Build ของคุณ:

powershell -File $(ProjectDir)GenBuildDate.ps1 -outputFile $(ProjectDir)BuildDate.cs

ตรวจสอบให้แน่ใจว่า BuildDate.cs รวมอยู่ในโครงการของคุณ ทำงานเหมือนแชมป์บนระบบปฏิบัติการใด ๆ !


1
คุณยังสามารถใช้สิ่งนี้เพื่อรับหมายเลขการแก้ไข SVN โดยใช้เครื่องมือบรรทัดคำสั่ง svn ฉันทำอะไรที่คล้ายกับสิ่งนี้
user169771

5

ฉันเพิ่งทำ:

File.GetCreationTime(GetType().Assembly.Location)

1
ที่น่าสนใจถ้าวิ่งออกมาจากการแก้ปัญหาวันที่ 'จริง' คือ GetLastAccessTime ()
Balint

4

คุณสามารถใช้โครงการนี้: https://github.com/dwcullop/BuildInfo

มันใช้ประโยชน์จาก T4 เพื่อให้การประทับเวลาวันที่สร้างโดยอัตโนมัติ มีหลายรุ่น (สาขาที่แตกต่างกัน) รวมถึงรุ่นที่ให้ Git Hash ของสาขาที่เช็คเอาท์ในปัจจุบันหากคุณอยู่ในประเภทนั้น

การเปิดเผย: ฉันเขียนโมดูล


3

วิธีที่แตกต่างและเป็นมิตรกับ PCL คือการใช้งาน MSBuild แบบอินไลน์เพื่อทดแทนเวลาสร้างเป็นสตริงที่ส่งคืนโดยคุณสมบัติในแอป เราใช้วิธีนี้ในแอปที่มีโครงการ Xamarin.Forms, Xamarin.Android และ Xamarin.iOS

แก้ไข:

ทำให้ง่ายขึ้นโดยการย้ายลอจิกทั้งหมดลงในSetBuildDate.targetsไฟล์และใช้Regexแทนสตริงแบบง่ายแทนที่เพื่อให้ไฟล์สามารถแก้ไขได้โดยแต่ละบิลด์โดยไม่ต้อง "รีเซ็ต"

นิยามภารกิจอินไลน์ MSBuild (บันทึกไว้ในไฟล์ SetBuildDate.targets ภายในเครื่องไปยังโครงการ Xamarin.Forms สำหรับตัวอย่างนี้):

<Project xmlns='http://schemas.microsoft.com/developer/msbuild/2003' ToolsVersion="12.0">

  <UsingTask TaskName="SetBuildDate" TaskFactory="CodeTaskFactory" 
    AssemblyFile="$(MSBuildToolsPath)\Microsoft.Build.Tasks.v12.0.dll">
    <ParameterGroup>
      <FilePath ParameterType="System.String" Required="true" />
    </ParameterGroup>
    <Task>
      <Code Type="Fragment" Language="cs"><![CDATA[

        DateTime now = DateTime.UtcNow;
        string buildDate = now.ToString("F");
        string replacement = string.Format("BuildDate => \"{0}\"", buildDate);
        string pattern = @"BuildDate => ""([^""]*)""";
        string content = File.ReadAllText(FilePath);
        System.Text.RegularExpressions.Regex rgx = new System.Text.RegularExpressions.Regex(pattern);
        content = rgx.Replace(content, replacement);
        File.WriteAllText(FilePath, content);
        File.SetLastWriteTimeUtc(FilePath, now);

   ]]></Code>
    </Task>
  </UsingTask>

</Project>

เรียกใช้งานแบบอินไลน์ข้างต้นในไฟล์ Xamarin.Forms csproj ในเป้าหมาย BeforeBuild:

  <!-- To modify your build process, add your task inside one of the targets below and uncomment it. 
       Other similar extension points exist, see Microsoft.Common.targets.  -->
  <Import Project="SetBuildDate.targets" />
  <Target Name="BeforeBuild">
    <SetBuildDate FilePath="$(MSBuildProjectDirectory)\BuildMetadata.cs" />
  </Target>

FilePathคุณสมบัติถูกกำหนดเป็นBuildMetadata.csไฟล์ในโครงการ Xamarin.Forms ที่มีระดับที่เรียบง่ายด้วยคุณสมบัติสตริงBuildDateลงไปในเวลาที่สร้างจะถูกแทน:

public class BuildMetadata
{
    public static string BuildDate => "This can be any arbitrary string";
}

เพิ่มไฟล์นี้BuildMetadata.csลงในโครงการ มันจะถูกแก้ไขโดยทุกบิลด์ แต่ในลักษณะที่อนุญาตให้บิลด์ซ้ำ (การแทนที่ซ้ำ) ดังนั้นคุณอาจรวมหรือละเว้นมันในการควบคุมซอร์สตามที่ต้องการ


2

คุณสามารถใช้เหตุการณ์ post-build โครงการเพื่อเขียนไฟล์ข้อความไปยังไดเรกทอรีเป้าหมายของคุณด้วยวันที่และเวลาปัจจุบัน จากนั้นคุณสามารถอ่านค่าในเวลาทำงาน มันแฮ็คเล็กน้อย แต่ควรใช้งานได้



2

การอัปเดตเล็กน้อยสำหรับคำตอบ "ทางใหม่" จาก Jhon

คุณต้องสร้างเส้นทางแทนการใช้สตริง CodeBase เมื่อทำงานกับ ASP.NET/MVC

    var codeBase = assembly.GetName().CodeBase;
    UriBuilder uri = new UriBuilder(codeBase);
    string path = Uri.UnescapeDataString(uri.Path);

1

คุณสามารถเปิดขั้นตอนพิเศษในกระบวนการสร้างที่เขียนประทับวันที่ลงในไฟล์ซึ่งสามารถแสดงได้

บนแท็บคุณสมบัติโครงการดูที่แท็บสร้างเหตุการณ์ มีตัวเลือกในการดำเนินการคำสั่งก่อนหรือหลังการสร้างเป็น


1

ฉันใช้คำแนะนำของ Abdurrahim อย่างไรก็ตามดูเหมือนว่าจะมีรูปแบบเวลาแปลก ๆ และยังเพิ่มตัวย่อสำหรับวันนั้นเป็นส่วนหนึ่งของวันที่สร้าง ตัวอย่าง: อา. 12/24/2017 13: 21: 05.43 ฉันต้องการแค่วันที่เท่านั้นดังนั้นฉันจึงต้องกำจัดส่วนที่เหลือโดยใช้สตริงย่อย

หลังจากเพิ่มecho %date% %time% > "$(ProjectDir)\Resources\BuildDate.txt"ไปยังเหตุการณ์ก่อนสร้างฉันเพิ่งทำสิ่งต่อไปนี้:

string strBuildDate = YourNamespace.Properties.Resources.BuildDate;
string strTrimBuildDate = strBuildDate.Substring(4).Remove(10);

ข่าวดีก็คือว่ามันทำงาน


ทางออกที่ง่ายมาก ฉันชอบมัน. และถ้ารูปแบบนั้นน่ารำคาญก็มีวิธีที่จะทำให้รูปแบบที่ดีขึ้นจากบรรทัดคำสั่ง
Nyerguds

0

หากนี่เป็นแอพ windows คุณสามารถใช้พา ธ ที่ปฏิบัติการได้ของแอปพลิเคชัน: System.IO.FileInfo (Application.ExecutablePath) .LastWriteTime.ToString ("yyyy.MM.dd") ใหม่


2
แล้วตอบรับการใช้งานนี้และยังไม่กันกระสุนแน่นอน
crashmstr

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.